From 540ea452acca2c0064ed256e2fedb908aeb17e97 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 13 Aug 2025 21:31:50 +1000 Subject: [PATCH 01/18] Setup mkdoc and improve documentation --- .github/workflows/publish-docs.yml | 36 + .gitignore | 1 + .pre-commit-config.yaml | 9 +- .vscode/settings.json | 14 +- CHANGELOG.md | 10 +- CONTRIBUTING.md | 125 +- README.md | 96 +- RELEASE.md | 2 +- docs/Makefile | 192 --- docs/api.md | 25 + docs/api/async_kernel.rst | 55 - docs/api/modules.rst | 7 - {examples => docs}/caller.ipynb | 12 +- docs/command_line.md | 73 ++ docs/conf.py | 314 ----- docs/contributing.md | 1 + {examples => docs}/execute_mode.ipynb | 5 +- docs/index.md | 1 + docs/index.rst | 23 - docs/javascripts/extra.js | 0 docs/license.md | 11 + docs/make.bat | 263 ---- docs/overrides/main.html | 14 + docs/stylesheets/extra.css | 3 + mkdocs.yml | 131 ++ pyproject.toml | 21 +- src/async_kernel/__init__.py | 11 +- src/async_kernel/__main__.py | 31 +- src/async_kernel/asyncshell.py | 10 +- src/async_kernel/caller.py | 168 +-- src/async_kernel/kernel.py | 91 +- src/async_kernel/kernelspec.py | 59 +- src/async_kernel/utils.py | 85 +- tests/{test_utils.py => test_bind_socket.py} | 8 +- tests/test_main.py | 4 +- tests/test_message_spec.py | 3 + uv.lock | 1174 ++++++++++++------ 37 files changed, 1437 insertions(+), 1651 deletions(-) create mode 100644 .github/workflows/publish-docs.yml delete mode 100644 docs/Makefile create mode 100644 docs/api.md delete mode 100644 docs/api/async_kernel.rst delete mode 100644 docs/api/modules.rst rename {examples => docs}/caller.ipynb (92%) create mode 100644 docs/command_line.md delete mode 100644 docs/conf.py create mode 100644 docs/contributing.md rename {examples => docs}/execute_mode.ipynb (96%) create mode 100644 docs/index.md delete mode 100644 docs/index.rst create mode 100644 docs/javascripts/extra.js create mode 100644 docs/license.md delete mode 100644 docs/make.bat create mode 100644 docs/overrides/main.html create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml rename tests/{test_utils.py => test_bind_socket.py} (79%) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 000000000..21fb99c61 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,36 @@ +name: docs +on: + push: + branches: + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.6" + python-version: ${{ matrix.python-version }} + - name: Checkout + uses: actions/checkout@v4 + + - name: Install the project + run: uv sync --no-dev --group docs + + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: ~/.cache + restore-keys: | + mkdocs-material- + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 1902acdc1..2c7c2e021 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ MANIFEST build cover dist +site _build docs/man/*.gz docs/source/api/generated diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 065ba22e1..5a74885ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,11 +14,11 @@ repos: - id: check-case-conflict - id: check-merge-conflict - id: check-toml + - id: mixed-line-ending - id: check-yaml + args: ["--unsafe"] - id: debug-statements - exclude: async_kernel/kernel.py - id: end-of-file-fixer - - id: trailing-whitespace - repo: https://gitlab.com/bmares/check-json5 rev: v1.0.0 @@ -35,7 +35,10 @@ repos: hooks: - id: mdformat additional_dependencies: - [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] + - mdformat-gfm + - mdformat-frontmatter + - mdformat-footnote + - mdformat-mkdocs - repo: https://github.com/pre-commit/mirrors-prettier rev: "v4.0.0-alpha.8" diff --git a/.vscode/settings.json b/.vscode/settings.json index 28994aed4..8062834ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,17 @@ "python.testing.pytestEnabled": true, "python.analysis.typeCheckingMode": "off", "editor.defaultFormatter": "charliermarsh.ruff", - "basedpyright.analysis.diagnosticMode": "workspace" + "basedpyright.analysis.diagnosticMode": "workspace", + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", + "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", + "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d060984..5d2b6637b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changes in IPython kernel +# Changes in Async kernel @@ -6,7 +6,7 @@ ([Full Changelog](<>)) -Kernel - Derived from an discontinued version branching from Ipython version 6.29.4. +Kernel - Derived from an discontinued version branching from IPyKernel version 6.29.4. ### Enhancements made @@ -20,7 +20,7 @@ Kernel - Derived from an discontinued version branching from Ipython version 6.2 ### Contributors to this release - + @@ -28,6 +28,6 @@ Kernel - Derived from an discontinued version branching from Ipython version 6.2 ([Full Changelog](<>)) -See Ipython [changelog](https://ipykernel.readthedocs.io/en/stable/changelog.html#id2) for all the hard work done to get here. +See IPyKernel [changelog](https://ipykernel.readthedocs.io/en/stable/changelog.html#id2) for all the hard work done to get here. -Forked from IPykernel commit [#8322a7684b004ee95f07b2f86f61e28146a5996d](https://github.com/ipython/ipykernel/commit/8322a7684b004ee95f07b2f86f61e28146a5996d) +Forked from IPyKernel commit [#8322a7684b004ee95f07b2f86f61e28146a5996d](https://github.com/ipython/ipykernel/commit/8322a7684b004ee95f07b2f86f61e28146a5996d) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 514cbf49e..59a2d60a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,94 +1,77 @@ # Contributing -Welcome! +This project is in development. Create an issue to provide feedback. -For contributing tips, follow the [Jupyter Contributing Guide](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html). -Please make sure to follow the [Jupyter Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). +## Development -## Installing ipykernel for development +## Installation from source -ipykernel is a pure Python package, so setting up for development is the same as most other Python projects: +```shell +git clone https://github.com/fleming79/async-kernel.git +cd async-kernel +uv venv -p python@311 +uv sync +# Activate the environment +``` + +## Running tests + +```shell +pytest +``` + +## Running tests with coverage + +We are aiming for 100% code coverage. Any new code should have meaningful tests +added to ensure reliability. -```bash -# clone the repo -git clone https://github.com/ipython/ipykernel -cd ipykernel -# do a 'development' or 'editable' install with pip: -pip install -e .[test] +```shell +pytest -vv --cov ``` ## Code Styling -`ipykernel` has adopted automatic code formatting so you shouldn't -need to worry too much about your code style. -As long as your code is valid, +`Async kernel` uses ruff for code formatting. the pre-commit hook should take care of how it should look. To install `pre-commit`, run the following:: -``` +```shell pip install pre-commit pre-commit install ``` You can invoke the pre-commit hook by hand at any time with:: -``` +```shell pre-commit run ``` -which should run any autoformatting on your code -and tell you about any errors it couldn't fix automatically. -You may also install [black integration](https://github.com/psf/black#editor-integration) -into your text editor to format code automatically. - -If you have already committed files before setting up the pre-commit -hook with `pre-commit install`, you can fix everything up using -`pre-commit run --all-files`. You need to make the fixing commit -yourself after that. - -Some of the hooks only run on CI by default, but you can invoke them by -running with the `--hook-stage manual` argument. - -## Releasing ipykernel - -Releasing ipykernel is _almost_ standard for a Python package: - -- set version for release -- make and publish tag -- publish release to PyPI -- set version back to development - -The one extra step for ipykernel is that we need to make separate wheels for Python 2 and 3 -because the bundled kernelspec has different contents for Python 2 and 3. This -affects only the 4.x branch of ipykernel as the 5+ version is only compatible -Python 3. - -The full release process is available below: - -```bash -# make sure version is set in ipykernel/_version.py -VERSION="4.9.0" -# commit the version and make a release tag -git add ipykernel/_version.py -git commit -m "release $VERSION" -git tag -am "release $VERSION" $VERSION - -# push the changes to the repo -git push -git push --tags - -# publish the release to PyPI -# note the extra `python2 setup.py bdist_wheel` for creating -# the wheel for Python 2 -pip install --upgrade twine -git clean -xfd -python3 setup.py sdist bdist_wheel -python2 setup.py bdist_wheel # the extra step for the 4.x branch. -twine upload dist/* - -# set the version back to '.dev' in ipykernel/_version.py -# e.g. 4.10.0.dev if we just released 4.9.0 -git add ipykernel/_version.py -git commit -m "back to dev" -git push +## Type checking + +Type checking is performed using [basedpyright](https://docs.basedpyright.com/). It is installed automatically. + +To run use + +```shell +basedpyright ``` + +## Documentation + +Documentation is provided my [Material for MkDocs ](https://squidfunk.github.io/mkdocs-material/). + +To install dependencies: + +```shell +uv sync --no-dev --frozen --group docs +``` + +To start the server locally: + +```shell +mkdocs serve +``` + +## Releasing Async kernel + +TODO diff --git a/README.md b/README.md index b5d9f8392..9096787f1 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,48 @@ -# Async Kernel for Jupyter +# Async kernel - +[![image](https://img.shields.io/pypi/pyversions/async-kernel.svg)](https://pypi.python.org/pypi/async-kernel) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![basedpyright - checked](https://img.shields.io/badge/basedpyright-checked-42b983)](https://docs.basedpyright.com) -Async-kernel is a python implementation of a [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages). The kernel runs inside an [anyio](https://pypi.org/project/anyio/) event loop ([asyncio](https://docs.python.org/3/library/asyncio.html#module-asyncio) / [trio](https://pypi.org/project/trio/)) running requests in a modified [IPython](https://pypi.org/project/ipython/) InteractiveShell. +Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages) that runs in an [anyio](https://pypi.org/project/anyio/) event loop. -## Features +Async kernel is designed to run execute requests in tasks separate to the shell message loop. This means the kernel won't dead locks awaiting a response that is delivered via the shell. -- [Execute-requests](#kerneljob) by default are run in a task (sequentially) without blocking shell messages. -- `stdout`(including print), `stderr` and `stdin`(input) map correctly to the execute request (see: [ContextVars](#contextvars)). -- Cell code can be run in threads or tasks by adding `##thread` or `##task` respectively as the first line in a cell (see [Execute mode](#execute-mode)). -- Provides a `Caller` class to execute code in tasks/threads with a thread safe Future providing access to the result. -- Uses the anyio function [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to await ZMQ socket messages. +Execute requests are queued for execution by default, but can also be run concurrently by including either `##task` or `##thread` at the top of the code. `##thread` will run the code in a separate `Caller` thread, that provides its own event loop. -### Execute mode +## Highlights -If you add `##` to the top of cell, the kernel will modify how the cell is run. The following execute modes are supported. +- Concurrent cell execution in tasks or cells supported [^run-concurrent] +- Comms is not blocked during cell execution[^non-blocking-execution] +- Debugger included +- Configurable backend - "asyncio" (default) or "trio backend" [^config-backend] +- [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, etc. +- No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets -- `##thread` - The code is run in a thread. -- `##task` - The code is run as a task. -- `##queue` (default behaviour) - The code is added to a queue and executed sequentially in a task. +## Installation -### ContextVars - -Execute request jobs are stored as a [ContextVar](https://docs.python.org/3/library/contextvars.html#module-contextvars) which is accessible on the kernel as the property `kernel.job`. Using a context variable makes it possible to perform concurrent execution enabling `stdio`, `stderr` and `stdin` to map back to the initial job (execute request). - -#### Example: run a cell in a thread - -This code will run the code in a thread. - -```python -##thread - -import time - -time.sleep(100) +```shell +pip install async-kernel ``` -Irrespective of the Execute mode, any code run in a cell will respect cancellation, though in this example the cancellation will only occur after the `time.sleep` call has returned. Should this have been run in the `MainThread` the time.sleep would have been cancelled immediately by means of a signal. - -## Kernel variants - -The kernel name defines the anyio backend that is used. Currently there are three KernelNames implement. - -1. async: An anyio 'asyncio' backend. -1. async-trio: An anyio 'trio' backend (requires trio - install manually). - -### Enabling / disabling kernels - -Kernels can be added/removed via the command line. - -#### Add +To add a kernel spec for `trio`. ```shell -async_kernel -add async-trio +pip install trio +async-kernel add async-trio ``` -#### Remove - -async_kernel -remove async - -# Development - -## Installation from source - -1. `git clone` -1. `cd ipykernel` -1. `uv sync"` -1. Activate the environment. - -After that, all normal `ipython` commands will use this newly-installed version of the kernel. +## Origin -## Running tests +Async-kernel started as fork of [IPyKernel](https://pypi.org/project/ipykernel/) commit [#8322a7684b004ee95f07b2f86f61e28146a5996d](https://github.com/ipython/ipykernel/commit/8322a7684b004ee95f07b2f86f61e28146a5996d). -```bash -pytest +```shell +async-kernel -a async-trio ``` -## Running tests with coverage +[^run-concurrent]: Execute requests (code cells) are run passed to a queue for execution that is run in a different task to shell message handling. This means shell messages can be processed whilst execute requests are being performed. Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. -```bash -pytest -vv -s --cov -``` +[^non-blocking-execution]: Shell messaging runs in a task separate to execute requests in the main thread. This means shell messages (including comms) can pass freely whilst an execute request is busy awaiting a result. -# Origin - -Async-kernel started as fork of [IPyKernel](https://pypi.org/project/ipykernel/) commit [#8322a7684b004ee95f07b2f86f61e28146a5996d](https://github.com/ipython/ipykernel/commit/8322a7684b004ee95f07b2f86f61e28146a5996d). +[^config-backend]: The default backend is 'asyncio'. To add a 'trio' backend, define a KernelSpec with a kernel name that includes trio in it. diff --git a/RELEASE.md b/RELEASE.md index 3370c6c36..dea44c7e9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -10,7 +10,7 @@ The recommended way to make a release is to use [`jupyter_releaser`](https://jup - Run the following: -```bash +```shell export VERSION= pip install pipx pipx run hatch version $VERSION diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 3c3067d02..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,192 +0,0 @@ -# Makefile for Sphinx documentation -# - -# 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) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - -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 " 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)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -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." - -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/Kernel.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Kernel.qhc" - -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." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Kernel" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Kernel" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -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)." - -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." - -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." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -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)." - -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." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -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." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 000000000..013cd77a8 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,25 @@ +# API + +- **[Caller](#caller)** +- **[Utils](#utils)** +- **[async-kernel (command)](#main-command-line-handler)** + +## Caller + +::: async_kernel.caller +options: +show_submodules: true + +## Kernel + +::: async_kernel.Kernel + +## Utils + +::: async_kernel.utils +options: +show_submodules: true + +## main (command line handler) + +:::async_kernel.__main__.main diff --git a/docs/api/async_kernel.rst b/docs/api/async_kernel.rst deleted file mode 100644 index bc7bb7fcf..000000000 --- a/docs/api/async_kernel.rst +++ /dev/null @@ -1,55 +0,0 @@ -async_kernel package -==================== - - -Submodules ----------- - -.. automodule:: async_kernel.comm - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.displayhook - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.iostream - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.kernel - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.kernelspec - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.utils - :members: - :undoc-members: - :show-inheritance: - - -.. automodule:: async_kernel.zmqshell - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: async_kernel - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/modules.rst b/docs/api/modules.rst deleted file mode 100644 index cbbc5a535..000000000 --- a/docs/api/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -async_kernel -========= - -.. toctree:: - :maxdepth: 4 - - async_kernel diff --git a/examples/caller.ipynb b/docs/caller.ipynb similarity index 92% rename from examples/caller.ipynb rename to docs/caller.ipynb index ad1343f20..407673779 100644 --- a/examples/caller.ipynb +++ b/docs/caller.ipynb @@ -9,7 +9,7 @@ "\n", "`Caller` is a class that makes it easy to call code in different threads/tasks. One caller instance is created per thread, and each of those instances can be retrieved by name using the `Caller.get_instance` class method or in the thread in which it is running simply by `Caller()`. \n", "\n", - "`Caller` is used by the [kernel](#usage-by-the-kernel) internally for running code and but can also be used directly by the user. Each caller starts its own iopub zmq socket.\n", + "`Caller` is used by the [kernel](#usage-by-the-kernel) internally for running code, but can also be used directly by the user. Each caller starts its own iopub zmq socket.\n", "\n", "## Threads\n", "\n", @@ -100,6 +100,14 @@ " result = await fut\n", " print(f\"Finished: {result}\", end=\"\\r\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -118,7 +126,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.8.final.0" + "version": "3.11.13.final.0" } }, "nbformat": 4, diff --git a/docs/command_line.md b/docs/command_line.md new file mode 100644 index 000000000..c88ba0d76 --- /dev/null +++ b/docs/command_line.md @@ -0,0 +1,73 @@ +# Command line + +`async-kernel` (and alias `async_kernel`) is provided as a system executable. + +**Options:** + +- [Start a kernel](#start-a-kernel) +- [Add kernel spec](#add-a-kernel-spec) +- [Remove](#remove-a-kernel-spec) + +## Add a kernel spec + +Use the argument `-a` followed by the kernel name to add a new kernel spec. +Include 'trio' in the kernel name to use a 'trio' backend. Any valid kernel name is +allowed do not include whitespace in the kernel name. Recommended kernel names are + +- 'async': Default kernel that is installed that provides a the default 'asyncio' backend. +- 'async-trio': A trio backend. Note: trio must be installed separately. + +Add a trio kernel spec. + +```console +async-kernel -a async-trio +``` + +!!! note + +``` +To modify how the kernel start see [Start a kernel](#start-a-kernel) for configuration options. +``` + +### Configuration + +Additional configuration of the kernel spec is supported by passing the each parameter +prefixed with '--' followed by the value. + +The parameters are first used with creating the kernel spec. + +## Remove a kernel spec + +You can remove any kernel spec that is listed. Call `async-kernel` with no arguments to see a list of the installed kernels. + +```shell +async-kernel +``` + +If you added the custom kernel spec above, you can remove it with: + +```shell +async-kernel -r async-trio-custom +``` + +## Start a kernel + +To start a kernel from the command prompt, use the argument `-f`. + +This will start the default kernel (async). + +```shell +async-kernel -f . +``` + +Additional settings can be passed as arguments. + +```shell +async-kernel -f . --kernel_name async-trio-custom --display_name 'My custom kernel' --quiet False +``` + +The call above will start a new kernel with a 'trio' backend. The quiet setting is +a parameter that gets set on kernel. Parameters of this type are converted using [eval] +prior to setting. + +For further detail, see the api for the command line handler [main]\[async_kernel.__main__.main\]. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 39a1d5924..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,314 +0,0 @@ -# IPython Kernel documentation build configuration file, created by -# sphinx-quickstart on Mon Oct 5 11:32:44 2015. -# -# 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 os -import shutil -from pathlib import Path -from typing import Any - -from intersphinx_registry import get_intersphinx_mapping - -# 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 = [ - "myst_parser", - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinxcontrib_github_alt", - "sphinx_autodoc_typehints", -] - -try: - import enchant # noqa: F401 - - extensions += ["sphinxcontrib.spelling"] -except ImportError: - pass - - -github_project_url = "https://github.com/ipython/ipykernel" - -# 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 = "IPython Kernel" -copyright = "2015, IPython Development Team" -author = "IPython Development Team" - -# 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. -# - -version_ns: dict[str, Any] = {} -here = Path(__file__).parent.resolve() -version_py = Path(here) / os.pardir / "src" / "async_kernel" / "_version.py" -with open(version_py) as f: - exec(compile(f.read(), version_py, "exec"), version_ns) - -# The short X.Y version. -version = "%i.%i" % version_ns["version_info"][:2] -# The full version, including alpha/beta/rc tags. -release = version_ns["__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 = "en" - -# 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. -exclude_patterns = ["_build"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -default_role = "literal" - -# 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 = "pydata_sphinx_theme" - -# 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 = {"navigation_with_keys": False} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# 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 (within the static path) to use as 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: list[str] = [] - -# 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 '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# 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' -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -# 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 = "ipykerneldoc" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements: dict[str, object] = {} - -# 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, - "ipykernel.tex", - "IPython Kernel Documentation", - "IPython Development Team", - "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, "ipykernel", "IPython Kernel 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, - "ipykernel", - "IPython Kernel Documentation", - author, - "ipykernel", - "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 = get_intersphinx_mapping(packages={"ipython", "python", "jupyter"}) - - -def setup(app): - shutil.copy(Path(here) / ".." / "CHANGELOG.md", "changelog.md") diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..ea38c9bff --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +--8<-- "CONTRIBUTING.md" diff --git a/examples/execute_mode.ipynb b/docs/execute_mode.ipynb similarity index 96% rename from examples/execute_mode.ipynb rename to docs/execute_mode.ipynb index 8cdfa578a..34df8dcc2 100644 --- a/examples/execute_mode.ipynb +++ b/docs/execute_mode.ipynb @@ -11,11 +11,10 @@ "\n", "``` python\n", "##thread\n", - "# This code gets run in the shell via Caller.to_thread\n", - "# Only valid for this code block\n", + "%callers\n", "```\n", "\n", - "Provided the frontend supports it (Jupyterlab does, VS code doesn't), multiple cells can be run at a time.\n", + "Provided the frontend supports it (Jupyterlab does, VS code doesn't), multiple cells can concurrently.\n", "\n", "## Example\n", "\n", diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..612c7a5e0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index aed0ac0e9..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. _index: - -IPython Kernel Docs -=================== - -This contains minimal version-sensitive documentation for the IPython kernel package. -Most IPython kernel documentation is in the `IPython documentation `_. - -Contents: - -.. toctree:: - :maxdepth: 1 - - changelog - API docs - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js new file mode 100644 index 000000000..e69de29bb diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 000000000..d96d2ab59 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,11 @@ +--- +title: License +hide: + - feedback +--- + +# License + +``` +--8<-- "LICENSE" +``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2b78b1464..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,263 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "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. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over 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 - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Kernel.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Kernel.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..9c0c9b880 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} {% block content %} {% if page.nb_url %} + + {% include ".icons/material/download.svg" %} + +{% endif %} {{ super() }} {% endblock content %} {% block outdated %} You're not +viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 000000000..fcb71554a --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,3 @@ +.md-grid { + max-width: 1200px; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..1686ff402 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,131 @@ +site_name: async-kernel +site_description: Async-kernel for Jupyter +site_author: "" +site_url: https://fleming79.github.io/async-kernel/ +repo_name: fleming79/async-kernel +repo_url: https://github.com/fleming79/async-kernel +copyright: "Copyright © 2025-present" +extra: + version: + provider: mike +extra_css: + - stylesheets/extra.css +theme: + name: material + custom_dir: docs/overrides + features: + - navigation.tabs + - navigation.tabs.sticky + - content.code.copy + - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#anchor-following + - toc.integrate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#navigation-integration + - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#back-to-top-button + - header.autohide # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/#automatic-hiding + - navigation.footer # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-footer/#navigation + - search.suggest # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-suggestions + - search.highlight # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-highlighting + - navigation.instant # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#instant-loading + font: + text: Noto Sans Cham + code: Noto Sans Mono + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: darkula + primary: blue + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: lime + toggle: + icon: material/weather-night + name: Switch to system preference +plugins: + - search + - autorefs + - mkdocs-jupyter: + include_source: True + - mkdocstrings: + enabled: true + # custom_templates: templates + default_handler: python + locale: en + handlers: + python: + paths: [src, docs/snippets] + inventories: + - https://docs.python.org/3/objects.inv + options: + docstring_options: + ignore_init_summary: true + docstring_section_style: list + extensions: [] + filters: public + heading_level: 2 + inherited_members: true + line_length: 120 + merge_init_into_class: true + parameter_headings: true + # preload_modules: [mkdocstrings] + relative_crossrefs: true + scoped_crossrefs: true + separate_signature: true + show_bases: false + show_inheritance_diagram: true + show_root_heading: true + show_root_full_path: true + show_signature_annotations: true + show_if_no_docstring: false + show_root_members_full_path: false + show_source: true + show_symbol_type_heading: false + show_symbol_type_toc: true + show_category_heading: false + signature_crossrefs: true + summary: true + unwrap_annotated: true + +markdown_extensions: + - attr_list + - md_in_html + - admonition + - pymdownx.details + - pymdownx.snippets + - pymdownx.superfences + - footnotes + - toc: + permalink: true + toc_depth: 4 + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower +nav: + - Home: index.md + - Usage: + - Execute mode: execute_mode.ipynb + - Caller notebook: caller.ipynb + - Command line: command_line.md + - API: api.md + - Info: + - Contributing: contributing.md + - Changelog: changelog.md + - License: license.md diff --git a/pyproject.toml b/pyproject.toml index ecabb7c1d..1d9107dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,23 +33,18 @@ dependencies = [ ] [project.urls] -# Homepage = "" -# Documentation = "" -# Source = "" -# Tracker = "" +Homepage = "https://fleming79.github.io/async-kernel" +Documentation = "https://fleming79.github.io/async-kernel" +Source = "https://github.com/fleming79/async-kernel" +Tracker = "https://github.com/fleming79/async-kernel/issues" [project.scripts] async-kernel = "async_kernel.__main__:main" [dependency-groups] -docs = [ - "sphinx", - "myst_parser", - "pydata_sphinx_theme", - "sphinxcontrib_github_alt", - "sphinxcontrib-spelling", - "sphinx-autodoc-typehints", - "intersphinx_registry", +docs = ["mkdocs-material", + "mkdocstrings[python]", + "mkdocs-jupyter" ] dev = [ "debugpy", @@ -163,7 +158,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"examples/*" = ["PLC0415"] +"*.ipynb" = ["PLC0415"] [tool.ruff.format] docstring-code-format = true diff --git a/src/async_kernel/__init__.py b/src/async_kernel/__init__.py index bb6f8b6f0..3f98c6b6d 100644 --- a/src/async_kernel/__init__.py +++ b/src/async_kernel/__init__.py @@ -1,8 +1,17 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from async_kernel import utils from async_kernel._version import __version__, kernel_protocol_version, kernel_protocol_version_info from async_kernel.caller import Caller, Future from async_kernel.kernel import Kernel -__all__ = ["Caller", "Future", "Kernel", "__version__", "kernel_protocol_version", "kernel_protocol_version_info"] +__all__ = [ + "Caller", + "Future", + "Kernel", + "__version__", + "kernel_protocol_version", + "kernel_protocol_version_info", + "utils", +] diff --git a/src/async_kernel/__main__.py b/src/async_kernel/__main__.py index f86068e0f..9b0359fcb 100644 --- a/src/async_kernel/__main__.py +++ b/src/async_kernel/__main__.py @@ -1,28 +1,32 @@ -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - """The cli entry point for async_kernel.""" +from __future__ import annotations + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import argparse import contextlib -import pathlib import shutil import sys import traceback from itertools import pairwise +from typing import TYPE_CHECKING import anyio import traitlets from async_kernel.kernel import Kernel -from async_kernel.kernelspec import Backend, KernelName, write_kernel_spec +from async_kernel.kernelspec import Backend, KernelName, get_kernel_dir, write_kernel_spec + +if TYPE_CHECKING: + from pathlib import Path -def main(wait_exit_context=anyio.sleep_forever): +def main(wait_exit_context=anyio.sleep_forever) -> None: "Main entry point to launch kernel or add/remove installed kernel specs." - kernel_dir = pathlib.Path(sys.prefix) / "share/jupyter/kernels" + kernel_dir: Path = get_kernel_dir() parser = argparse.ArgumentParser( - description="Kernel interface to start a kernel or add/remove a kernel spec." + description="Kernel interface to start a kernel or add/remove a kernel spec. " + f"The Jupyter Kernel directory is: f'{kernel_dir}'" ) parser.add_argument( @@ -51,10 +55,11 @@ def main(wait_exit_context=anyio.sleep_forever): if k.startswith("--"): setattr(args, k.removeprefix("--"), v) if args.add: - args.kernel_name = args.add + if not hasattr(args, "kernel_name"): + args.kernel_name = args.add for name in ["add", "remove"] + (["connection_file"] if args.connection_file is None else []): delattr(args, name) - path = write_kernel_spec(path=kernel_dir / args.kernel_name, **vars(args)) + path = write_kernel_spec(**vars(args)) print(f"Added kernel spec {path!s}") elif args.remove: for name in args.remove.split(","): @@ -68,10 +73,10 @@ def main(wait_exit_context=anyio.sleep_forever): elif not args.connection_file: parser.print_help() else: - klass = getattr(args, "klass", None) + kernel_factory = getattr(args, "kernel_factory", None) kernel_name: str = getattr(args, "kernel_name", None) or KernelName.asyncio - cls: type[Kernel] = traitlets.import_item(klass) if klass else Kernel - kernel = cls(kernel_name=kernel_name) + factory: type[Kernel] = traitlets.import_item(kernel_factory) if kernel_factory else Kernel + kernel = factory(kernel_name=kernel_name) for k, v in vars(args).items(): if hasattr(kernel, k): if k == "connection_file" and v == ".": diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index 754cb24a5..d8963f9bb 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -7,7 +7,7 @@ import json import pathlib import sys -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Literal import IPython.core.release from IPython.core.displayhook import DisplayHook @@ -269,11 +269,13 @@ def connect_info(self, _): @line_magic def callers(self, _): - print("Active") + print("Active", "Protected", "\t", "Name") + print("─" * 70) for caller in Caller.all_callers(active_only=False): symbol = " ✓" if caller.active else " ✗" - current_thread = "← calling thread" if caller is Caller() else "" - print(symbol, caller, current_thread, sep="\t") + current_thread: Literal["← current thread", ""] = "← current thread" if caller is Caller() else "" + protected = " 🔐" if caller.protected else "" + print(symbol, protected, "", caller.thread.name, current_thread, sep="\t") InteractiveShellABC.register(AsyncInteractiveShell) diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index e12961342..683acca3d 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -35,7 +35,7 @@ class CancelledError(anyio.ClosedResourceError): - "Used to indicate a future is cancelled." + "Used to indicate a Future is cancelled." class InvalidStateError(RuntimeError): @@ -44,30 +44,10 @@ class InvalidStateError(RuntimeError): class Future(Awaitable[T]): """ - A class representing a future result of an asynchronous operation. - - This class provides a way to wait for the result of a computation - that may be running in another thread. It supports setting a result - or an exception, adding callbacks to be executed when the future is - done, and canceling the future. - - The set_result/set_exception methods must be called from inside the thread - specified when the instance was created. - - Attributes: - thread (threading.Thread | None): The thread associated with the future. - - Methods: - result(): Wait for the result (thread-safe). - wait_sync(): Synchronously wait for the result. - set_result(value): Set the result of the future. - set_exception(exception): Set the exception for the future. - done(): Return True if the Future is done. - add_done_callback(fn): Add a callback to be called when the future is done (not thread-safe). - cancel(): Cancel the Future and schedule callbacks (thread-safe). - cancelled(): Return True if the future has been cancelled. - exception(): Return the exception that was set on this Future. - remove_done_callback(fn): Remove all instances of a callback from the callbacks list. + A class representing a future result modelled on the asyncio class [`Future`](https://docs.python.org/3/library/asyncio-future.html#futures) . + + This class provides an anyio compatible Future primitive. It is designed + to work with the Caller to enable thread-safe, event loop function calls. """ __slots__ = [ @@ -123,13 +103,13 @@ def wait_sync(self) -> T: raise self._exception return self._result - def set_result(self, value: T): + def set_result(self, value: T) -> None: self._set_value("result", value) - def set_exception(self, exception: BaseException): + def set_exception(self, exception: BaseException) -> None: self._set_value("exception", exception) - def _set_value(self, mode: Literal["result", "exception"], value): + def _set_value(self, mode: Literal["result", "exception"], value) -> None: if self._setting_value: raise InvalidStateError self._setting_value = True @@ -159,13 +139,13 @@ def set_value(): else: set_value() - def done(self): + def done(self) -> bool: """Return True if the Future is done. Done means either that a result / exception are available.""" return self._event_done.is_set() - def add_done_callback(self, fn: Callable[[Self], object]): + def add_done_callback(self, fn: Callable[[Self], object]) -> None: """Add a callback for when the callback is done (not thread-safe). The result of the future and done callbacks are always called for the futures thread. @@ -205,17 +185,15 @@ def remove_done_callback(self, fn: Callable[[Self], object], /) -> int: self._done_callbacks.remove(fn) return n - def set_cancel_scope(self, scope: anyio.CancelScope): - "Provide a cancel scope for cancellation" + def set_cancel_scope(self, scope: anyio.CancelScope) -> None: + "Provide a cancel scope for cancellation." if self._cancelled: scope.cancel() self._cancel_scope = scope class Caller: - """ - A class to manage calls to functions and coroutines in a separate thread, - utilizing AnyIO for asynchronous operations. + """A class to enable calling functions and coroutines between anyio event loops. The `Caller` class provides a mechanism to execute functions and coroutines in a dedicated thread, leveraging AnyIO for asynchronous task management. @@ -226,47 +204,6 @@ class Caller: The class maintains a registry of instances, associating each with a specific thread. It uses a task group to manage the execution of scheduled tasks and provides methods to start, stop, and query the status of the caller. - - Attributes: - thread (threading.Thread): The thread associated with this `Caller` instance. - backend (str): The AnyIO backend used by this `Caller` instance. - log (logging.LoggerAdapter): A logger adapter for logging messages. - active (bool): A flag indicating whether the `Caller` is active. - iopub_sockets (weakref.WeakKeyDictionary[threading.Thread, Socket]): A class-level - weak key dictionary mapping threads to ZeroMQ sockets for inter-process - communication. - iopub_url (str): The URL for the ZeroMQ IOPub socket. - - Methods: - __new__(cls, thread: threading.Thread | None = None, *, log: logging.LoggerAdapter | None = None, create=False, protected=False) -> Self: - Creates a new `Caller` instance or returns an existing one for the given thread. - taskgroup (property): - Returns the AnyIO task group associated with this `Caller` instance. - stopped (property): - Returns True if the `Caller` is stopped, False otherwise. - stop(self, *, force=False): - Stops the `Caller` instance, closing the event loop and releasing resources. - call_later(self, func: Callable[P, T | Awaitable[T]], delay=0.0, /, *args: P.args, **kwargs: P.kwargs) -> Future[T]: - Schedules a function or coroutine for execution after a delay. - call_soon(self, func: Callable[P, T | Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> Future[T]: - Schedules a function or coroutine for immediate execution. - call_no_context(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: - Schedules a function for execution without a context. - Classmethods: - stop_all(cls, **kwgs): - Stops all active `Caller` instances. - get_instance(cls, name: str | None = "MainThread", *, create=False) -> Self: - Gets an instance of `Caller` for the given thread name. - to_thread(cls, func: Callable[P, T | Awaitable[T]], /, *args: P.args, **kwargs: P.kwargs) -> Future[T]: - Calls a function in a separate thread using a thread pool. - to_thread_by_name(cls, name: str | None, func: Callable[P, T | Awaitable[T]], /, *args: P.args, **kwargs: P.kwargs) -> Future[T]: - Calls a function in a separate thread, creating a new thread if necessary. - start_new(cls, *, backend: Literal["asyncio", "trio"] | str = "", log: logging.LoggerAdapter | None = None, name: str | None = None, protected=False): - Starts a new `Caller` in a separate thread. - as_completed(cls, items: Iterable[Future[T]] | AsyncGenerator[Future[T]], *, max_concurrent: NoValue | int = NoValue): - An asynchronous iterator that yields futures as they complete. - list_active(cls) -> list[str]: - Lists the names of all active callers. """ _instances: ClassVar[dict[threading.Thread, Self]] = {} @@ -322,12 +259,12 @@ async def __aenter__(self) -> Self: self.__stack = stack.pop_all() return self - async def __aexit__(self, exc_type, exc_value, exc_tb): + async def __aexit__(self, exc_type, exc_value, exc_tb) -> None: if self.__stack is not None: self.stop() await self.__stack.__aexit__(exc_type, exc_value, exc_tb) - async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]): + async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> None: thread = threading.current_thread() socket = Context.instance().socket(SocketType.PUB) socket.linger = 500 @@ -408,11 +345,14 @@ def taskgroup(self) -> TaskGroup: raise RuntimeError(msg) @property - def stopped(self): + def stopped(self) -> bool: return self._stopped - def stop(self, *, force=False): - "Once closed it can not be reopened." + def stop(self, *, force=False) -> None: + """Stop the caller cancelling all pending tasks and close the thread. + + If the instance is protected, this is no-op unless force is used. + """ if self._protected and not force: return self._stopped = True @@ -422,14 +362,15 @@ def stop(self, *, force=False): self._to_thread_pool.remove(self) def call_later( - self, func: Callable[P, T | Awaitable[T]], delay=0.0, /, *args: P.args, **kwargs: P.kwargs + self, func: Callable[P, T | Awaitable[T]], delay: float = 0.0, /, *args: P.args, **kwargs: P.kwargs ) -> Future[T]: - """Schedules a function or coroutine for execution. + """Schedule func to be called in this instances event loop using the current contextvars context. - If the instance is not open in an async context, the function will be queued and - executed once the async context is open. - - The delay is calculated from the submission time. + Args: + func: The function (awaitables permitted, though discouraged). + delay: The minimum delay to add between submission and execution. + *args: Arguments to use with func. + **kwargs: Keyword arguments to use with func. """ if self._stopped: raise anyio.ClosedResourceError @@ -443,14 +384,31 @@ def call_later( return fut def call_soon(self, func: Callable[P, T | Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> Future[T]: - "Calls call_later with delay=0.0." + """Schedule func to be called in this instances event loop using the current contextvars context. + + Args: + func: The function (awaitables permitted, though discouraged). + *args: Arguments to use with func. + **kwargs: Keyword arguments to use with func. + """ return self.call_later(func, 0.0, *args, **kwargs) def call_no_context(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: - """Call func in the thread event loop.""" + """Call func in the thread event loop. + + Args: + func: The function (awaitables permitted, though discouraged). + *args: Arguments to use with func. + **kwargs: Keyword arguments to use with func. + """ self._jobs.append(functools.partial(func, *args, **kwargs)) self._jobs_added.set() + @property + def protected(self) -> bool: + "Returns `True` when the instance is protected from stopping." + return self._protected + @classmethod def stop_all(cls, **kwgs) -> None: "Stop all instances." @@ -459,13 +417,12 @@ def stop_all(cls, **kwgs) -> None: caller.stop(force=force) @classmethod - def get_instance(cls, name: str | None = "MainThread", *, create=False) -> Self: - """Gets an instance of Caller. - name: str | None - If the + def get_instance(cls, name: str | None = "MainThread", *, create: bool = False) -> Self: + """Gets an instance of the Caller by name. - create: bool - If the Caller instance does not exist a new thread is created. + Args: + name: Name of the caller instance thread. + create: If the Caller instance does not exist a new thread is created. """ for thread in cls._instances: if thread.name == name: @@ -489,13 +446,14 @@ def to_thread_by_name( ) -> Future[T]: """Call the function in the Caller's thread. - name: name of the caller's thread. passing an empty string will provide a caller from the pool. - - func: - The function (awaitables permitted, though discouraged). - *args, **kwargs: for func. + If a caller does not exist for `name` a new `Caller` is started. The one caveat being 'MainThread'. - If a caller thread is not found a new one is created with the specified name.""" + Args: + name: name of the caller's thread. passing an empty string will provide a caller from the pool. + func: The function (awaitables permitted, though discouraged). + *args: Arguments to use with func. + **kwargs: Keyword arguments to use with func. + """ caller = ( cls._to_thread_pool.popleft() if not name and cls._to_thread_pool @@ -514,7 +472,7 @@ def start_new( backend: Literal["asyncio", "trio"] | str = "", # noqa: PYI051 log: logging.LoggerAdapter | None = None, name: str | None = None, - protected=False, + protected: bool = False, ) -> Self: """Start a new thread with a new Caller open in the context of anyio event loop. @@ -552,7 +510,7 @@ async def as_completed( items: Iterable[Future[T]] | AsyncGenerator[Future[T]], *, max_concurrent: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] - ): + ) -> AsyncGenerator[Future[T], Any]: """An iterator to get Futures as they complete. Pass a generator should you wish to limit the number future jobs when calling to_thread/to_task etc. @@ -560,9 +518,7 @@ async def as_completed( Args: items: Either a container with existing futures or generator of Futures. - max_concurrent: The maximum number of concurrent futures to monitor at a time. - This is useful when `items` is a generator utilising Caller.to_thread. By default this will - limit to `Caller.MAX_IDLE_POOL_INSTANCES`. + max_concurrent: The maximum number of concurrent futures to monitor at a time. This is useful when `items` is a generator utilising Caller.to_thread. By default this will limit to `Caller.MAX_IDLE_POOL_INSTANCES`. """ event_future_ready = threading.Event() has_result: deque[Future[T]] = deque() diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index f3c4b3ca2..db6351cf3 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -8,6 +8,7 @@ import builtins import contextlib import contextvars +import errno import getpass import logging import os @@ -33,7 +34,7 @@ from jupyter_client.session import Session from jupyter_core.paths import jupyter_runtime_dir from traitlets import CaselessStrEnum, CBool, Container, Dict, Instance, Int, Set, Tuple, UseEnum, default -from zmq import Context, Flag, PollEvent, Socket, SocketOption, SocketType +from zmq import Context, Flag, PollEvent, Socket, SocketOption, SocketType, ZMQError from async_kernel import _version, utils from async_kernel.asyncshell import AsyncInteractiveShell @@ -55,6 +56,80 @@ __all__ = ["Kernel", "KernelInterruptError"] +def error_to_dict(error: BaseException): + """Convert the error to a dict. + + ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply + """ + return { + "status": "error", + "ename": type(error).__name__, + "evalue": str(error), + "traceback": traceback.format_exception(error), + } + + +def bind_socket( + socket: Socket[SocketType], + transport: Literal["tcp", "ipc"], + ip: str, + port: int = 0, + max_attempts: int | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] +) -> int: + """Bind the socket to a port using the settings. + + max_attempts: The maximum number of attempts to bind the socket. If un-specified, + defaults to 100 if port missing, else 2 attempts. + """ + + def _try_bind_socket(port: int): + if transport == "tcp": + if not port: + port = socket.bind_to_random_port(f"tcp://{ip}") + else: + socket.bind(f"tcp://{ip}:{port}") + elif transport == "ipc": + if not port: + port = 1 + while True: + port = port + 1 + path = f"{ip}-{port}" + if not Path(path).exists(): + break + else: + path = f"{ip}-{port}" + socket.bind(f"ipc://{path}") + return port + + if transport == "ipc": + ip = Path(ip).as_posix() + if socket.TYPE == SocketType.ROUTER: + # ref: https://github.com/ipython/ipykernel/issues/270 + socket.router_handover = 1 + try: + win_in_use = errno.WSAEADDRINUSE # type: ignore[attr-defined] + except AttributeError: + win_in_use = None + # Try up to 100 times to bind a port when in conflict to avoid + # infinite attempts in bad setups + if max_attempts is NoValue: + max_attempts = 2 if port else 100 + e = None + for _ in range(max_attempts): + try: + return _try_bind_socket(port) + except ZMQError as e_: + # Raise if we have any error not related to socket binding + # 135: Protocol not supported + if e_.errno in {errno.EADDRINUSE, win_in_use, 135}: + e = e_ + break + if port: + time.sleep(1) + msg = f"Failed to bind {socket} for {transport=}" + (f" to {port=}!" if port else "!") + raise RuntimeError(msg) from e + + class KernelInterruptError(InterruptedError): "Raised to interrupt the kernel." @@ -252,7 +327,9 @@ async def start_in_context(self): pathlib.Path(self.connection_file).parent.mkdir(parents=True, exist_ok=True) self.write_connection_file() atexit.register(self.cleanup_connection_file) - print(f"""Kernel started. To connect a client use: --existing "{self.connection_file}" """) + print( + f"""Kernel started with backend: {self.anyio_backend}. To connect a client use: --existing "{self.connection_file}" """ + ) await tg.start(self._start_iopub) yield self finally: @@ -428,7 +505,7 @@ async def _receive_msg_loop( @staticmethod def get_execute_mode(job: Job[ExecuteContent]) -> ExecuteMode: - """Get job["msg"]["content"]["execute_mode"] adding it if it has't been set.""" + """Extract `ExecuteMode` from the job.""" if m := job["msg"]["content"].get("execute_mode"): # Respect an existing mode return ExecuteMode(m) @@ -452,7 +529,7 @@ async def _run_handler(self, handler: Callable[[Job], CoroutineType] | None, job self._publish_status("busy", job) await handler(job) except Exception as e: - self._send_reply(job, utils.error_to_dict(e)) + self._send_reply(job, content=error_to_dict(e)) self.log.exception("Exception in message handler:", exc_info=e) finally: self._publish_status("idle", job) @@ -469,7 +546,7 @@ def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket): if socket_id is not SocketID.iopub: # ref: https://github.com/ipython/ipykernel/issues/270 socket.router_handover = 1 - port = utils.bind_socket(socket=socket, transport=self.transport, ip=self.ip, port=getattr(self, port_name)) # pyright: ignore[reportArgumentType] + port = bind_socket(socket=socket, transport=self.transport, ip=self.ip, port=getattr(self, port_name)) # pyright: ignore[reportArgumentType] setattr(self, port_name, port) self.log.debug("%s socket on port: %i", socket_id, port) self._sockets[socket_id] = socket @@ -596,7 +673,7 @@ async def execute_request(self, received_time: float, job: Job[ExecuteContent]): if (received_time < self._stop_on_error_time) and not job["msg"]["content"]["silent"]: self.log.info("Aborting execute_request: %s", job) self._publish_status("busy", job) - content = utils.error_to_dict(RuntimeError("Aborting due to prior exception")) + content: dict[str, str | list[str]] = error_to_dict(RuntimeError("Aborting due to prior exception")) content["execution_count"] = self.execution_count # pyright: ignore[reportArgumentType] self._send_reply(job, content) self._publish_status("idle", job) @@ -638,7 +715,7 @@ async def _execute_request_handler(self, job: Job[ExecuteContent]): "user_expressions": self.shell.user_expressions(content.get("user_expressions", {})), } if err: - reply_content |= utils.error_to_dict(error=err) + reply_content |= error_to_dict(error=err) if not silent and content.get("stop_on_error"): self._stop_on_error_time = time.monotonic() self.log.info("An error occurred in a non-silent execution request") diff --git a/src/async_kernel/kernelspec.py b/src/async_kernel/kernelspec.py index bc885f5bd..d91ca520f 100644 --- a/src/async_kernel/kernelspec.py +++ b/src/async_kernel/kernelspec.py @@ -1,4 +1,4 @@ -"""The IPython kernel spec for Jupyter""" +"""Add and remove kernel specifications for Jupyter.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. @@ -10,7 +10,6 @@ import json import shutil import sys -import tempfile from pathlib import Path from jupyter_client.kernelspec import KernelSpec, _is_valid_kernel_name # pyright: ignore[reportPrivateUsage] @@ -19,7 +18,7 @@ RESOURCES = Path(__file__).parent.joinpath("resources") -__all__ = ["Backend", "KernelName", "make_argv", "write_kernel_spec"] +__all__ = ["Backend", "KernelName", "get_kernel_dir", "make_argv", "write_kernel_spec"] class Backend(enum.StrEnum): @@ -37,19 +36,20 @@ def make_argv( *, connection_file="{connection_file}", kernel_name: KernelName | str = KernelName.asyncio, - klass="async_kernel.Kernel", + kernel_factory="async_kernel.Kernel", fullpath=True, **kwargs, -): - """ - Constructs the argument vector (argv) for launching a Python kernel module. - The backend is determined from the kernel_name. The default backend `asyncio` will - be used unless the kernel_name contains 'trio' (case-insensitive). where a trio backend. +) -> list[str]: + """Constructs the argument vector (argv) for launching a Python kernel module. + + The backend is determined from the kernel_name. If the kernel_name contains 'trio' + (case-insensitive)a trio backend will be used otherwise an 'asyncio' backend is used. Args: - connection_file (str): The path to the connection file. Defaults to "{connection_file}". - klass (str): The string import path to a Kernel. - kernel_name (KernelName or str): The name of the kernel to use. + connection_file: The path to the connection file. + kernel_factory: The string import path to a callable that creates the kernel. + kernel_name: The name of the kernel to use. + fullpath: If True the full path to the executable is used, otherwise 'python' is used. kwargs: Additional settings to use on the instance of the Kernel. kwargs are converted to key/value pairs, keys will be prefixed with '--'. @@ -63,7 +63,7 @@ def make_argv( """ python = sys.executable if fullpath else "python" argv = [python, "-m", "async_kernel", "-f", connection_file] - for k, v in ({"klass": klass, "kernel_name": kernel_name} | kwargs).items(): + for k, v in ({"kernel_factory": kernel_factory, "kernel_name": kernel_name} | kwargs).items(): argv.extend((f"--{k}", str(v))) return argv @@ -71,7 +71,7 @@ def make_argv( def write_kernel_spec( path: Path | str | None = None, *, - klass="async_kernel.Kernel", + kernel_factory="async_kernel.Kernel", connection_file="{connection_file}", kernel_name: KernelName | str = KernelName.asyncio, fullpath=False, @@ -79,22 +79,34 @@ def write_kernel_spec( **kwargs, ) -> Path: """ - Write a kernel spec directory to `path/kernel_name`. + Write a kernel spec directory to `path` for launching a kernel. - If `path` is not specified, a temporary directory is created. - The path to the kernelspec is always returned. + The kernel spec will always call async_kernel.__main__.main which is designed + to enable launching of customised kernels. + + Args: + connection_file: The path to the connection file. + kernel_factory: The string import path to a callable that creates the kernel. + kernel_name: The name of the kernel to use. + fullpath: If True the full path to the executable is used, otherwise 'python' is used. + display_name: The display name for Jupyter to use for the kernel. The default is `"Python ({kernel_name})"`. - **kwargs: - kwargs are passed to make_argv for additional runtime configuration of the kernel. + kwargs: + Additional settings to use on the instance of the Kernel. + kwargs are converted to key/value pairs, keys will be prefixed with '--'. + The kwargs should correspond to settings to set prior to starting. kwargs + are set on the + instance by eval on the value. + keys that correspond to an attribute on the kernel instance are not used. """ assert _is_valid_kernel_name(kernel_name) - path = Path(path) if path else Path(tempfile.mkdtemp(suffix="_kernels")) / kernel_name + path = Path(path) if path else get_kernel_dir() / kernel_name # stage resources path.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(RESOURCES, path, dirs_exist_ok=True) spec = KernelSpec() spec.argv = make_argv( - klass=klass, + kernel_factory=kernel_factory, connection_file=connection_file, kernel_name=kernel_name, fullpath=fullpath, @@ -110,3 +122,8 @@ def write_kernel_spec( with path.joinpath("kernel.json").open("w") as f: json.dump(spec.to_dict(), f, indent=1) return path + + +def get_kernel_dir() -> Path: + "The path to where kernel specs are stored for Jupyter." + return Path(sys.prefix) / "share/jupyter/kernels" diff --git a/src/async_kernel/utils.py b/src/async_kernel/utils.py index 1f56f21fb..3de8c508b 100644 --- a/src/async_kernel/utils.py +++ b/src/async_kernel/utils.py @@ -1,89 +1,21 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +"""Utility functions.""" from __future__ import annotations import contextlib -import errno import sys import threading -import time -import traceback -from pathlib import Path -from typing import Literal import anyio import anyio.to_thread -from zmq import Socket, SocketType, ZMQError -from async_kernel.typing import NoValue - -__all__ = ["bind_socket", "do_not_debug_this_thread", "mark_thread_pydev_do_not_trace", "wait_thread_event"] +__all__ = ["do_not_debug_this_thread", "mark_thread_pydev_do_not_trace", "wait_thread_event"] LAUNCHED_BY_DEBUGPY = "debugpy" in sys.modules -def bind_socket( - socket: Socket[SocketType], - transport: Literal["tcp", "ipc"], - ip: str, - port: int = 0, - max_attempts: int | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] -) -> int: - """Bind the socket to a port using the settings. - - max_attempts: The maximum number of attempts to bind the socket. If un-specified, - defaults to 100 if port missing, else 2 attempts. - """ - - def _try_bind_socket(port: int): - if transport == "tcp": - if not port: - port = socket.bind_to_random_port(f"tcp://{ip}") - else: - socket.bind(f"tcp://{ip}:{port}") - elif transport == "ipc": - if not port: - port = 1 - while True: - port = port + 1 - path = f"{ip}-{port}" - if not Path(path).exists(): - break - else: - path = f"{ip}-{port}" - socket.bind(f"ipc://{path}") - return port - - if transport == "ipc": - ip = Path(ip).as_posix() - if socket.TYPE == SocketType.ROUTER: - # ref: https://github.com/ipython/ipykernel/issues/270 - socket.router_handover = 1 - try: - win_in_use = errno.WSAEADDRINUSE # type: ignore[attr-defined] - except AttributeError: - win_in_use = None - # Try up to 100 times to bind a port when in conflict to avoid - # infinite attempts in bad setups - if max_attempts is NoValue: - max_attempts = 2 if port else 100 - e = None - for _ in range(max_attempts): - try: - return _try_bind_socket(port) - except ZMQError as e_: - # Raise if we have any error not related to socket binding - # 135: Protocol not supported - if e_.errno in {errno.EADDRINUSE, win_in_use, 135}: - e = e_ - break - if port: - time.sleep(1) - msg = f"Failed to bind {socket} for {transport=}" + (f" to {port=}!" if port else "!") - raise RuntimeError(msg) from e - - def mark_thread_pydev_do_not_trace(thread: threading.Thread, name="", *, remove=False): """Modifies the given thread's attributes to hide or unhide it from the debugger (e.g., debugpy).""" thread.pydev_do_not_trace = not remove # pyright: ignore[reportAttributeAccessIssue] @@ -117,16 +49,3 @@ def _in_thread_call(): await anyio.to_thread.run_sync(_in_thread_call) finally: event.set() - - -def error_to_dict(error: BaseException): - """Convert the error to a dict. - - ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply - """ - return { - "status": "error", - "ename": type(error).__name__, - "evalue": str(error), - "traceback": traceback.format_exception(error), - } diff --git a/tests/test_utils.py b/tests/test_bind_socket.py similarity index 79% rename from tests/test_utils.py rename to tests/test_bind_socket.py index 5d3be46fc..20087fc3e 100644 --- a/tests/test_utils.py +++ b/tests/test_bind_socket.py @@ -7,7 +7,7 @@ import pytest import zmq -from async_kernel.utils import bind_socket +from async_kernel.kernel import bind_socket @pytest.fixture(scope="module", params=["tcp", "ipc"]) @@ -24,9 +24,9 @@ def test_bind_socket(transport: Literal["tcp", "ipc"], tmp_path): ip = tmp_path / "mypath" if transport == "ipc" else "0.0.0.0" with ctx: with ctx.socket(zmq.SocketType.ROUTER) as socket: - port = bind_socket(socket, transport, ip) + port = bind_socket(socket, transport, ip) # pyright: ignore[reportArgumentType] with ctx.socket(zmq.SocketType.ROUTER) as socket: - assert bind_socket(socket, transport, ip, port) == port + assert bind_socket(socket, transport, ip, port) == port # pyright: ignore[reportArgumentType] if transport == "tcp": with pytest.raises(RuntimeError): - bind_socket(socket, transport, ip, "invalid port") # type: ignore[call-arg] + bind_socket(socket, transport, ip, "invalid port") # pyright: ignore[reportArgumentType] diff --git a/tests/test_main.py b/tests/test_main.py index 1fbcd9f36..f912f8012 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,7 +38,7 @@ def test_prints_help_when_no_args(monkeypatch, capsys): def test_add_kernel(monkeypatch, fake_kernel_dir: pathlib.Path, capsys): monkeypatch.setattr( - sys, "argv", ["prog", "-a", "async-trio", "--display_name", "my kernel", "--klass", "my.custom.class"] + sys, "argv", ["prog", "-a", "async-trio", "--display_name", "my kernel", "--kernel_factory", "my.custom.class"] ) main.main() out = capsys.readouterr().out @@ -54,7 +54,7 @@ def test_add_kernel(monkeypatch, fake_kernel_dir: pathlib.Path, capsys): "async_kernel", "-f", "{connection_file}", - "--klass", + "--kernel_factory", "my.custom.class", "--kernel_name", "async-trio", diff --git a/tests/test_message_spec.py b/tests/test_message_spec.py index 3bb113578..2fad622cf 100644 --- a/tests/test_message_spec.py +++ b/tests/test_message_spec.py @@ -18,6 +18,7 @@ async def test_execute(client, kernel): async def test_execute_control(client, kernel): + await utils.clear_iopub(client) await utils.send_control_message(client, "execute_request", {"code": "y=10", "silent": True}, clear_pub=False) assert kernel.shell.user_ns["y"] == 10 await utils.check_pub_message(client, execution_state="busy") @@ -25,6 +26,7 @@ async def test_execute_control(client, kernel): async def test_execute_silent(client): + await utils.clear_iopub(client) msg_id, reply = await utils.execute(client, code="x=1", silent=True, clear_pub=False) count = reply["execution_count"] await utils.check_pub_message(client, msg_id, execution_state="busy") @@ -44,6 +46,7 @@ async def test_execute_silent(client): async def test_execute_error(client): + await utils.clear_iopub(client) msg_id, reply = await utils.execute(client, code="1/0", clear_pub=False) assert reply["status"] == "error" assert reply["ename"] == "ZeroDivisionError" diff --git a/uv.lock b/uv.lock index 38f008f7f..a227edbe2 100644 --- a/uv.lock +++ b/uv.lock @@ -2,27 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.11" -[[package]] -name = "accessible-pygments" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, -] - -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "anyio" version = "4.10.0" @@ -37,6 +16,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + [[package]] name = "asttokens" version = "3.0.0" @@ -84,13 +72,9 @@ dev = [ { name = "trio" }, ] docs = [ - { name = "intersphinx-registry" }, - { name = "myst-parser" }, - { name = "pydata-sphinx-theme" }, - { name = "sphinx" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinxcontrib-github-alt" }, - { name = "sphinxcontrib-spelling" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, ] [package.metadata] @@ -129,13 +113,9 @@ dev = [ { name = "trio" }, ] docs = [ - { name = "intersphinx-registry" }, - { name = "myst-parser" }, - { name = "pydata-sphinx-theme" }, - { name = "sphinx" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinxcontrib-github-alt" }, - { name = "sphinxcontrib-spelling" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, ] [[package]] @@ -165,6 +145,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] +[[package]] +name = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + [[package]] name = "basedpyright" version = "1.31.1" @@ -190,6 +184,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -420,77 +431,77 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" }, - { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" }, - { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" }, - { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" }, - { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" }, - { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" }, - { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" }, - { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" }, - { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, - { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, - { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, - { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, - { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, - { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, - { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, - { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, - { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, - { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, - { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, - { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, - { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, - { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, - { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, - { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, - { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, - { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, - { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, - { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, +version = "7.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, + { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" }, + { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" }, + { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, + { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, + { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" }, + { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, + { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" }, + { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" }, + { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload-time = "2025-08-10T21:26:44.132Z" }, + { url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload-time = "2025-08-10T21:26:45.598Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload-time = "2025-08-10T21:26:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload-time = "2025-08-10T21:26:48.65Z" }, + { url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload-time = "2025-08-10T21:26:50.368Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload-time = "2025-08-10T21:26:52.045Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload-time = "2025-08-10T21:26:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload-time = "2025-08-10T21:26:55.11Z" }, + { url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload-time = "2025-08-10T21:26:56.637Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload-time = "2025-08-10T21:26:58.207Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload-time = "2025-08-10T21:26:59.707Z" }, + { url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload-time = "2025-08-10T21:27:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload-time = "2025-08-10T21:27:03.442Z" }, + { url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload-time = "2025-08-10T21:27:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload-time = "2025-08-10T21:27:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload-time = "2025-08-10T21:27:08.398Z" }, + { url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload-time = "2025-08-10T21:27:10.237Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload-time = "2025-08-10T21:27:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload-time = "2025-08-10T21:27:13.254Z" }, + { url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload-time = "2025-08-10T21:27:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload-time = "2025-08-10T21:27:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload-time = "2025-08-10T21:27:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, ] [package.optional-dependencies] @@ -571,21 +582,21 @@ wheels = [ ] [[package]] -name = "distlib" -version = "0.4.0" +name = "defusedxml" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] -name = "docutils" -version = "0.21.2" +name = "distlib" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] @@ -597,6 +608,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, ] +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload-time = "2024-12-02T10:55:15.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -639,6 +659,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] +[[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 = "griffe" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/0f/9cbd56eb047de77a4b93d8d4674e70cd19a1ff64d7410651b514a1ed93d5/griffe-1.11.1.tar.gz", hash = "sha256:d54ffad1ec4da9658901eb5521e9cddcdb7a496604f67d8ae71077f03f549b7e", size = 410996, upload-time = "2025-08-11T11:38:35.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/a3/451ffd422ce143758a39c0290aaa7c9727ecc2bcc19debd7a8f3c6075ce9/griffe-1.11.1-py3-none-any.whl", hash = "sha256:5799cf7c513e4b928cfc6107ee6c4bc4a92e001f07022d97fd8dee2f612b6064", size = 138745, upload-time = "2025-08-11T11:38:33.964Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -748,15 +792,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] -[[package]] -name = "imagesize" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, -] - [[package]] name = "importlib-metadata" version = "8.7.0" @@ -779,12 +814,27 @@ wheels = [ ] [[package]] -name = "intersphinx-registry" -version = "0.2501.23" +name = "ipykernel" +version = "6.30.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/a0/8e9c1dcc824a73940eeadcd35a50261431dafdcae79961ca7fa9ed6a3635/intersphinx_registry-0.2501.23.tar.gz", hash = "sha256:0b3ef07afe630f3e6e3830fce7224814540d83a5e43601d31d188338c840acf6", size = 8068, upload-time = "2025-01-23T08:43:30.697Z" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/76/11082e338e0daadc89c8ff866185de11daf67d181901038f9e139d109761/ipykernel-6.30.1.tar.gz", hash = "sha256:6abb270161896402e76b91394fcdce5d1be5d45f456671e5080572f8505be39b", size = 166260, upload-time = "2025-08-04T15:47:35.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/35/226860923a154c2587ff2114c17bc610910824f990bc2c5fd009df40ba6b/intersphinx_registry-0.2501.23-py2.py3-none-any.whl", hash = "sha256:b9036ae7ba6b70826526a7ede7e437ca188bc12c6dbbddb65da690f2f6a6a2c5", size = 8822, upload-time = "2025-01-23T08:43:28.961Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/b445faca8deb954fe536abebff4ece5b097b923de482b26e78448c89d1dd/ipykernel-6.30.1-py3-none-any.whl", hash = "sha256:aa6b9fb93dca949069d8b85b6c79b2518e32ac583ae9c7d37c51d119e18b3fb4", size = 117484, upload-time = "2025-08-04T15:47:32.622Z" }, ] [[package]] @@ -890,6 +940,33 @@ 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 = "jsonschema" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + [[package]] name = "jupyter-client" version = "8.6.3" @@ -920,6 +997,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, ] +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupytext" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/0bd5290ca4978777154e2683413dca761781aacf57f7dc0146f5210df8b1/jupytext-1.17.2.tar.gz", hash = "sha256:772d92898ac1f2ded69106f897b34af48ce4a85c985fa043a378ff5a65455f02", size = 3748577, upload-time = "2025-06-01T21:31:48.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/f1/82ea8e783433707cafd9790099a2d19f113c22f32a31c8bb5abdc7a61dbb/jupytext-1.17.2-py3-none-any.whl", hash = "sha256:4f85dc43bb6a24b75491c5c434001ad5ef563932f68f15dd3e1c8ce12a4a426b", size = 164401, upload-time = "2025-06-01T21:31:46.319Z" }, +] + [[package]] name = "keyring" version = "25.6.0" @@ -940,80 +1042,113 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -1142,14 +1277,14 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.4.2" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[package]] @@ -1161,6 +1296,160 @@ 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 = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload-time = "2025-03-19T14:27:24.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, +] + +[[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.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-jupyter" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupytext" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "nbconvert" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/23/6ffb8d2fd2117aa860a04c6fe2510b21bc3c3c085907ffdd851caba53152/mkdocs_jupyter-0.25.1.tar.gz", hash = "sha256:0e9272ff4947e0ec683c92423a4bfb42a26477c103ab1a6ab8277e2dcc8f7afe", size = 1626747, upload-time = "2024-10-15T14:56:32.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/37/5f1fd5c3f6954b3256f8126275e62af493b96fb6aef6c0dbc4ee326032ad/mkdocs_jupyter-0.25.1-py3-none-any.whl", hash = "sha256:3f679a857609885d322880e72533ef5255561bbfdb13cfee2a1e92ef4d4ad8d8", size = 1456197, upload-time = "2024-10-15T14:56:29.854Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +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/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, +] + [[package]] name = "more-itertools" version = "10.7.0" @@ -1171,20 +1460,67 @@ wheels = [ ] [[package]] -name = "myst-parser" -version = "4.0.1" +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] @@ -1312,6 +1648,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + [[package]] name = "parso" version = "0.8.4" @@ -1472,6 +1826,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -1500,41 +1869,25 @@ wheels = [ ] [[package]] -name = "pydata-sphinx-theme" -version = "0.16.1" +name = "pygments" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] -name = "pyenchant" -version = "3.2.2" +name = "pymdown-extensions" +version = "10.16.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/a3/86763b6350727ca81c8fcc5bb5bccee416e902e0085dc7a902c81233717e/pyenchant-3.2.2.tar.gz", hash = "sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637", size = 49580, upload-time = "2021-10-05T17:25:25.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/4c/a741dddab6ad96f257d90cb4d23067ffadac526c9cab3a99ca6ce3c05477/pyenchant-3.2.2-py3-none-any.whl", hash = "sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6", size = 55660, upload-time = "2021-10-05T17:25:19.548Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/1e9a273d230abf5c961750a75e42b449adfb61eb446f80b6523955d2a4a2/pyenchant-3.2.2-py3-none-win32.whl", hash = "sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce", size = 11884084, upload-time = "2021-10-05T17:25:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/49/96/2087455de16b08e86fa7ce70b53ddac5fcc040c899d9ebad507a0efec52d/pyenchant-3.2.2-py3-none-win_amd64.whl", hash = "sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1", size = 11890882, upload-time = "2021-10-05T17:25:17.013Z" }, +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, ] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] [[package]] @@ -1687,6 +2040,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "pyzmq" version = "27.0.1" @@ -1745,6 +2110,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/b7/769598c5ae336fdb657946950465569cf18803140fe89ce466d7f0a57c11/pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", size = 544566, upload-time = "2025-08-03T05:05:20.798Z" }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + [[package]] name = "requests" version = "2.32.4" @@ -1774,12 +2153,111 @@ wheels = [ ] [[package]] -name = "roman-numerals-py" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, +name = "rpds-py" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, + { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, + { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, + { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, + { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, + { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, + { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, + { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, + { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, ] [[package]] @@ -1847,15 +2325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1874,127 +2343,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] -[[package]] -name = "sphinx" -version = "8.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals-py" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-github-alt" -version = "1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/d5/2880f4f441f3b46f264cb031d9e7135714b5060c895c8a6458051002c41a/sphinxcontrib_github_alt-1.2.tar.gz", hash = "sha256:cfd7584d559bb89a1dde3b418fea3b5b8e601e1f50459873635256b5d1712af5", size = 4015, upload-time = "2020-04-27T09:12:43.982Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/12/d9b6bf8093906108017f3cdbecae3e2b3b4963c5112b28f0cd482b433182/sphinxcontrib_github_alt-1.2-py2.py3-none-any.whl", hash = "sha256:cdd1f61090e9ca1f317283dc85b311d788864d7e41baa479882c9fc914b43641", size = 4269, upload-time = "2020-04-27T09:12:42.296Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - -[[package]] -name = "sphinxcontrib-spelling" -version = "8.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyenchant" }, - { name = "requests" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/04/099b55abd934cacccaedab9680c8238042eb3c722bdd420bc752d0eddb78/sphinxcontrib_spelling-8.0.1.tar.gz", hash = "sha256:f0447b6413c78b613b916c7891e36be85a105d1919c99784c53dfea2d8f8040f", size = 36005, upload-time = "2024-12-19T17:07:54.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/30/05efe7261eac789cf3ba28ef5dfb76d719df30baae6881cb54a6801c0e8f/sphinxcontrib_spelling-8.0.1-py3-none-any.whl", hash = "sha256:21704857c1b5e26e06bb07d15927df41c9d7ecfc1843169ecd22cb59f24069ac", size = 14617, upload-time = "2024-12-19T17:07:52.799Z" }, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -2009,6 +2357,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -2152,28 +2512,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/d0/4cd8ac2c7938da78c8e9ca791205f80e74b0f5a680f2a2d50323d54961d0/uv-0.8.8.tar.gz", hash = "sha256:6880e96cd994e53445d364206ddb4b2fff89fd2fbc74a74bef4a6f86384b07d9", size = 3477036, upload-time = "2025-08-09T00:26:00.883Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/d5/49e188db80f3d8b1969bdbcb8a5468a3796827f15d773241204f206a9ff6/uv-0.8.8-py3-none-linux_armv6l.whl", hash = "sha256:fcdbee030de120478db1a4bb3e3bbf04eec572527ea9107ecf064a808259b6c9", size = 18470316, upload-time = "2025-08-09T00:25:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/01/50/add1afadccd141d0d72b54e5146f8181fcc6efd1567a17c5b1edec444010/uv-0.8.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:461e8fb83931755cf0596bf1b8ccbfe02765e81a0d392c495c07685d6b6591f9", size = 18468770, upload-time = "2025-08-09T00:25:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ac/3c6dc8781d37ef9854f412322caffac2978dd3fa1bf806f7daebcfebf2be/uv-0.8.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58056e5ccebb0a1aad27bd89d0ccc5b65c086d5a7f6b0ac16a9dde030b63cf14", size = 17200419, upload-time = "2025-08-09T00:25:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9e/c30ea1f634673d234999985984afbe96c3d2a4381986e36df0bb46c0f21b/uv-0.8.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5b4c56a620137f562e1d7b09eac6c9d4adeb876aefc51be27973257fcb426c9d", size = 17779351, upload-time = "2025-08-09T00:25:20.891Z" }, - { url = "https://files.pythonhosted.org/packages/2f/89/f2885c6e97a265b4b18050df6285f56c81b603a867a63fcd8f2caa04d95c/uv-0.8.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fc33adb91c4e3db550648aa30c2b97e8e4d8b8842ead7784a9e76dae3cb14dc", size = 18139292, upload-time = "2025-08-09T00:25:23.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/5f/98dad16987919e7dc02f2566026a263ea6307bf57e8de0008dde4717d9cf/uv-0.8.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19a82d6738d3aa58e6646b9d6c343d103abf0c4caf97a68d16a8cab55282e4be", size = 18932468, upload-time = "2025-08-09T00:25:25.691Z" }, - { url = "https://files.pythonhosted.org/packages/56/99/52d0d9f53cc5df11b1a459e743bd7b2f4660d49f125a63640eb85ce993e0/uv-0.8.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9dce4de70098cb5b98feea9ef0b8f7db5d6b9deea003a926bc044a793872d719", size = 20251614, upload-time = "2025-08-09T00:25:28.122Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/0698099a905b4a07b8fa9d6838e0680de707216ccf003433ca1b4afff224/uv-0.8.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1038324c178d2d7407a4005c4c3294cbad6a02368ba5a85242308de62a6f4e12", size = 19916222, upload-time = "2025-08-09T00:25:30.732Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/8384e0f3f3536ef376d94b7ab177753179906a6c2f5bab893e3fb9525b45/uv-0.8.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bd016beea3935f9148b3d2482e3d60dee36f0260f9e99d4f57acfd978c1142a", size = 19238516, upload-time = "2025-08-09T00:25:33.637Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f1/6c107deccd6e66eb1c46776d8cef4ca9274aac73cec1b14453fe85e18a54/uv-0.8.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a2b5ebc96aba2b0bf54283d2906b40f32949298cbc6ec48648097ddeac5c5d", size = 19232295, upload-time = "2025-08-09T00:25:37.154Z" }, - { url = "https://files.pythonhosted.org/packages/c5/96/9f5e935cd970102c67ce2a753ac721665fb4477c262e86afa0ab385cefff/uv-0.8.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e529dc0a1be5e896d299e4eae4599fa68909f8cb3e6c5ee1a46f66c9048e3334", size = 18046917, upload-time = "2025-08-09T00:25:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/97f371add0a02e5e37156ac0fea908ab4a1160fdf716d0e6c257b6767122/uv-0.8.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5d58d986c3b6a9ce0fb48cd48b3aee6cb1b1057f928d598432e75a4fcaa370f4", size = 18949133, upload-time = "2025-08-09T00:25:42.139Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/ea988ae9d8c5531454ea6904290e229624c9ea830a5c37b91ec74ebde9a4/uv-0.8.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e117e1230559058fd286292dd5839e8e82d1aaf05763bf4a496e91fe07b69fa1", size = 18080018, upload-time = "2025-08-09T00:25:44.645Z" }, - { url = "https://files.pythonhosted.org/packages/ff/14/3b16af331b79ae826d00a73e98f26f7f660dabedc0f82acb99069601b355/uv-0.8.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:372934fd94193c98dec59bd379cf39e73f906ae6162cbfb66686f32afd75fa0f", size = 18437896, upload-time = "2025-08-09T00:25:49.162Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b6/c866684da5571dbf42e9a60b6587a62adc8a2eb592f07411d3b29cb09871/uv-0.8.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9330c924faa9df00a5e78b54561ecf4e5eac1211066f027620dbe85bd6f479ce", size = 19341221, upload-time = "2025-08-09T00:25:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/49/ea/55a0eff462b2ec5a6327dd87c401c53306406c830fa8f2cabd2af79dd97f/uv-0.8.8-py3-none-win32.whl", hash = "sha256:65113735aa3427d3897e2f537da1331d1391735c6eecb9b820da6a15fd2f6738", size = 18244601, upload-time = "2025-08-09T00:25:53.696Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c0/f56ddb1b2276405618e3d2522018c962c010fc71f97f385d01b7e1dcd8df/uv-0.8.8-py3-none-win_amd64.whl", hash = "sha256:66189ca0b4051396aa19a6f036351477656073d0fd01618051faca699e1b3cdc", size = 20233481, upload-time = "2025-08-09T00:25:56.247Z" }, - { url = "https://files.pythonhosted.org/packages/ac/1a/70dc4c730c19f3af40be9450b98b801e03cd6d16609743013f7258f69a29/uv-0.8.8-py3-none-win_arm64.whl", hash = "sha256:1d829486e88ebbf7895306ff09a8b6014d3af7a18e27d751979ee37bf3a27832", size = 18786215, upload-time = "2025-08-09T00:25:58.941Z" }, +version = "0.8.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a1/4dea87c10875b441d906f82df42d725a4a04c2e8ae720d9fa01e1f75e3dc/uv-0.8.9.tar.gz", hash = "sha256:54d76faf5338d1e5643a32b048c600de0cdaa7084e5909106103df04f3306615", size = 3478291, upload-time = "2025-08-12T02:32:37.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/d8/a2a24d30660b5f05f86699f86b642b1193bea1017e77e5e5d3e1c64f7bcc/uv-0.8.9-py3-none-linux_armv6l.whl", hash = "sha256:4633c693c79c57a77c52608cbca8a6bb17801bfa223326fbc5c5142654c23cc3", size = 18477020, upload-time = "2025-08-12T02:31:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/937e590fb08ce4c82503fddb08b54613c0d42dd06c660460f8f0552dd3a7/uv-0.8.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cdc11cbc81824e51ebb1bac35745a79048557e869ef9da458e99f1c3a96c7f9", size = 18486975, upload-time = "2025-08-12T02:31:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/a8/e6fc3e204731aa26b09934bbdecc8d6baa58a2d9e55b59b13130bacf8e52/uv-0.8.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b20ee83e3bf294e0b1347d0b27c56ea1a4fa7eeff4361fbf1f39587d4273059", size = 17178749, upload-time = "2025-08-12T02:31:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3e/3104a054bb6e866503a13114ee969d4b66227ebab19a38e3468f36c03a87/uv-0.8.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3418315e624f60a1c4ed37987b35d5ff0d03961d380e7e7946a3378499d5d779", size = 17790897, upload-time = "2025-08-12T02:31:59.451Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/ab64cca644f40bf85fb9b3a9050aad25af7882a1d774a384fc473ef9c697/uv-0.8.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7efe01b3ed9816e07e6cd4e088472a558a1d2946177f31002b4c42cd55cb4604", size = 18124831, upload-time = "2025-08-12T02:32:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/08/d1/68a001e3ad5d0601ea9ff348b54a78c8ba87fd2a6b6b5e27b379f6f3dff0/uv-0.8.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e571132495d7ab24d2f0270c559d6facd4224745d9db7dff8c20ec0c71ae105a", size = 18924774, upload-time = "2025-08-12T02:32:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/1b252e523eb875aa4ac8d06d5f8df175fa2d29e13da347d5d4823bce6c47/uv-0.8.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:67507c66837d8465daaad9f2ccd7da7af981d8c94eb8e32798f62a98c28de82d", size = 20256335, upload-time = "2025-08-12T02:32:07.12Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/062a25088b30a0fd27e4cc46baa272dd816acdec252b120d05a16d63170a/uv-0.8.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3162f495805a26fba5aacbee49c8650e1e74313c7a2e6df6aec5de9d1299087", size = 19920018, upload-time = "2025-08-12T02:32:10.041Z" }, + { url = "https://files.pythonhosted.org/packages/d8/55/90a0dc35938e68509ff8e8a49ff45b0fd13f3a44752e37d8967cd9d19316/uv-0.8.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60eb70afeb1c66180e12a15afd706bcc0968dbefccf7ef6e5d27a1aaa765419b", size = 19235553, upload-time = "2025-08-12T02:32:12.361Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a4/2db5939a3a993a06bca0a42e2120b4385bf1a4ff54242780701759252052/uv-0.8.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011d2b2d4781555f7f7d29d2f0d6b2638fc60eeff479406ed570052664589e6a", size = 19259174, upload-time = "2025-08-12T02:32:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/c52249b5f40f8eb2157587ae4b997942335e4df312dfb83b16b5ebdecc61/uv-0.8.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:97621843e087a68c0b4969676367d757e1de43c00a9f554eb7da35641bdff8a2", size = 18048069, upload-time = "2025-08-12T02:32:16.955Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ca/524137719fb09477e57c5983fa8864f824f5858b29fc679c0416634b79f0/uv-0.8.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b1be6a7b49d23b75d598691cc5c065a9e3cdf5e6e75d7b7f42f24d758ceef3c4", size = 18943440, upload-time = "2025-08-12T02:32:19.212Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/877bf9a52207023a8bf9b762bed3853697ed71c5c9911a4e31231de49a23/uv-0.8.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:91598361309c3601382c552dc22256f70b2491ad03357b66caa4be6fdf1111dd", size = 18075581, upload-time = "2025-08-12T02:32:21.732Z" }, + { url = "https://files.pythonhosted.org/packages/96/de/272d4111ff71765bcbfd3ecb4d4fff4073f08cc38b3ecdb7272518c3fe93/uv-0.8.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc81df9dd7571756e34255592caab92821652face35c3f52ad05efaa4bcc39d3", size = 18420275, upload-time = "2025-08-12T02:32:24.488Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/fecfc6665d1bfc5c7dbd32ff1d63413ac43d7f6d16d76fdc4d2513cbe807/uv-0.8.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9ef728e0a5caa2bb129c009a68b30819552e7addf934916a466116e302748bed", size = 19354288, upload-time = "2025-08-12T02:32:27.714Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/9fef88ac0cc3ca71ff718fa7d7e90c1b3a8639b041c674825aae00d24bf5/uv-0.8.9-py3-none-win32.whl", hash = "sha256:a347c2f2630a45a3b7ceae28a78a528137edfec4847bb29da1561bd8d1f7d254", size = 18197270, upload-time = "2025-08-12T02:32:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/dacd483c9726d2b74e42ee1f186aabab508222114f3099a7610ad0f78004/uv-0.8.9-py3-none-win_amd64.whl", hash = "sha256:dc12048cdb53210d0c7218bb403ad30118b1fe8eeff3fbcc184c13c26fcc47d4", size = 20221458, upload-time = "2025-08-12T02:32:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/f2b35278304673dcf9e8fe84b6d15531d91c59530dcf7919111f39a8d28f/uv-0.8.9-py3-none-win_arm64.whl", hash = "sha256:53332de28e9ee00effb695a15cdc70b2455d6b5f6b596d556076b5dd1fd3aa26", size = 18805689, upload-time = "2025-08-12T02:32:35.036Z" }, ] [[package]] @@ -2190,6 +2550,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, ] +[[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/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/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 = "wcwidth" version = "0.2.13" @@ -2199,6 +2586,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From f59885496c65918034c68a3873573f357784cce0 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Fri, 15 Aug 2025 14:50:55 +1000 Subject: [PATCH 02/18] Added badge for Material for MkDocs --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9096787f1..d743637d2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Async kernel + + [![image](https://img.shields.io/pypi/pyversions/async-kernel.svg)](https://pypi.python.org/pypi/async-kernel) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) [![basedpyright - checked](https://img.shields.io/badge/basedpyright-checked-42b983)](https://docs.basedpyright.com) +[![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=plastic&logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/) Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages) that runs in an [anyio](https://pypi.org/project/anyio/) event loop. From 574f56a6ab9f40418b268997176e69406853940f Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Fri, 15 Aug 2025 20:40:25 +1000 Subject: [PATCH 03/18] Run notebooks for documentation - features added to kernel for timeouts, metadata and tags. Renamed CancelledError to FutureCancelledError --- .vscode/spellright.dict | 8 + CONTRIBUTING.md | 42 +++- README.md | 31 +-- docs/api.md | 8 +- docs/caller.ipynb | 63 ++++- docs/execute_mode.ipynb | 101 ++++++-- docs/overrides/main.html | 1 + docs/simple_example.ipynb | 84 +++++++ mkdocs.yml | 27 +- pyproject.toml | 6 +- src/async_kernel/__main__.py | 9 +- src/async_kernel/asyncshell.py | 41 ++- src/async_kernel/caller.py | 51 ++-- src/async_kernel/kernel.py | 38 +-- src/async_kernel/typing.py | 34 ++- src/async_kernel/utils.py | 25 +- tests/test_caller.py | 10 +- tests/test_kernel.py | 38 ++- tests/test_main.py | 2 + tests/test_start_in_context.py | 3 +- tests/utils.py | 3 +- uv.lock | 444 ++++++++++++++++++++++++++++++++- 22 files changed, 932 insertions(+), 137 deletions(-) create mode 100644 .vscode/spellright.dict create mode 100644 docs/simple_example.ipynb diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict new file mode 100644 index 000000000..4664dd49e --- /dev/null +++ b/.vscode/spellright.dict @@ -0,0 +1,8 @@ +basedpyright +uv +Jupyter +anyio +asyncio +zmq +IPyKernel +anyio's diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59a2d60a4..d101d552e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -This project is in development. Create an issue to provide feedback. +This project is in development. Feel free to create an issue to provide feedback. ## Development @@ -22,8 +22,7 @@ pytest ## Running tests with coverage -We are aiming for 100% code coverage. Any new code should have meaningful tests -added to ensure reliability. +We are aiming for 100% code coverage on CI (Linux). Any new code should also update tests to maintain coverage. ```shell pytest -vv --cov @@ -48,9 +47,7 @@ pre-commit run ## Type checking -Type checking is performed using [basedpyright](https://docs.basedpyright.com/). It is installed automatically. - -To run use +Type checking is performed using [basedpyright](https://docs.basedpyright.com/). ```shell basedpyright @@ -58,20 +55,43 @@ basedpyright ## Documentation -Documentation is provided my [Material for MkDocs ](https://squidfunk.github.io/mkdocs-material/). +Documentation is provided my [Material for MkDocs ](https://squidfunk.github.io/mkdocs-material/). To start up a server for editing locally: -To install dependencies: +### Install ```shell -uv sync --no-dev --frozen --group docs +uv sync --group docs +uv run async-kernel -a async-docs --cell_execute_timeout 0.1 ``` -To start the server locally: +### Serve locally ```shell -mkdocs serve +mkdocs serve ``` +### API / Docstrings + +API documentation is included using [mkdocstrings](https://mkdocstrings.github.io/). + +Docstrings are written in [google format without types](https://mkdocstrings.github.io/griffe/reference/docstrings/?h=google#google-style). +Typing information is included automatically by [griff](https://mkdocstrings.github.io/griffe). + +#### See also + +- [cross-referencing](https://mkdocstrings.github.io/usage/#cross-references) + +### Notebooks + +Notebooks are included in the documentation with the plugin [mkdocs-jupyter](https://github.com/danielfrg/mkdocs-jupyter). + +#### Useful links + +These links are not relevant for docstrings. + +- [footnotes](https://squidfunk.github.io/mkdocs-material/reference/footnotes/#usage) +- [tooltips](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#usage) + ## Releasing Async kernel TODO diff --git a/README.md b/README.md index d743637d2..b96e7ebd9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Async kernel - - [![image](https://img.shields.io/pypi/pyversions/async-kernel.svg)](https://pypi.python.org/pypi/async-kernel) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) @@ -10,42 +8,45 @@ Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages) that runs in an [anyio](https://pypi.org/project/anyio/) event loop. -Async kernel is designed to run execute requests in tasks separate to the shell message loop. This means the kernel won't dead locks awaiting a response that is delivered via the shell. - -Execute requests are queued for execution by default, but can also be run concurrently by including either `##task` or `##thread` at the top of the code. `##thread` will run the code in a separate `Caller` thread, that provides its own event loop. +Async kernel is designed to run [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) outside the shell message loop to prevent dead locks when waiting for a response via the shell message loop. ## Highlights -- Concurrent cell execution in tasks or cells supported [^run-concurrent] - Comms is not blocked during cell execution[^non-blocking-execution] -- Debugger included +- Concurrent cell execution in tasks or cells [^run-concurrent] +- [Debugger client](https://jupyterlab.readthedocs.io/en/latest/user/debugger.html#debugger) - Configurable backend - "asyncio" (default) or "trio backend" [^config-backend] -- [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, etc. +- [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, etc - No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets +![Simple demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811) + ## Installation ```shell pip install async-kernel ``` -To add a kernel spec for `trio`. +### Trio + +To add a kernel spec for `trio`[^config-backend]. ```shell pip install trio async-kernel add async-trio ``` -## Origin - -Async-kernel started as fork of [IPyKernel](https://pypi.org/project/ipykernel/) commit [#8322a7684b004ee95f07b2f86f61e28146a5996d](https://github.com/ipython/ipykernel/commit/8322a7684b004ee95f07b2f86f61e28146a5996d). - ```shell async-kernel -a async-trio ``` -[^run-concurrent]: Execute requests (code cells) are run passed to a queue for execution that is run in a different task to shell message handling. This means shell messages can be processed whilst execute requests are being performed. Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. - [^non-blocking-execution]: Shell messaging runs in a task separate to execute requests in the main thread. This means shell messages (including comms) can pass freely whilst an execute request is busy awaiting a result. +[^run-concurrent]: Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. + + Works with concurrent cell execution + + - [x] Jupyterlab + - [ ] VS code - runs one cell at a time + [^config-backend]: The default backend is 'asyncio'. To add a 'trio' backend, define a KernelSpec with a kernel name that includes trio in it. diff --git a/docs/api.md b/docs/api.md index 013cd77a8..1bc9b8977 100644 --- a/docs/api.md +++ b/docs/api.md @@ -16,10 +16,16 @@ show_submodules: true ## Utils +Utility functions that are widely useful. + ::: async_kernel.utils options: show_submodules: true ## main (command line handler) -:::async_kernel.__main__.main +::: async_kernel.__main__.main + +## types + +::: async_kernel.typing diff --git a/docs/caller.ipynb b/docs/caller.ipynb index 407673779..b05dc3ba0 100644 --- a/docs/caller.ipynb +++ b/docs/caller.ipynb @@ -3,11 +3,19 @@ { "cell_type": "markdown", "id": "0", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Caller\n", "\n", - "`Caller` is a class that makes it easy to call code in different threads/tasks. One caller instance is created per thread, and each of those instances can be retrieved by name using the `Caller.get_instance` class method or in the thread in which it is running simply by `Caller()`. \n", + "`Caller` is a class that makes it easy to call code in different threads/tasks. \n", + "\n", + "One caller instance is created per thread, and each of those instances can be retrieved by name using the `Caller.get_instance` class method or in the thread in which it is running simply by `Caller()`. \n", "\n", "`Caller` is used by the [kernel](#usage-by-the-kernel) internally for running code, but can also be used directly by the user. Each caller starts its own iopub zmq socket.\n", "\n", @@ -31,7 +39,13 @@ { "cell_type": "markdown", "id": "1", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "To get the caller for the main thread.\n", "\n", @@ -42,19 +56,28 @@ "cell_type": "code", "execution_count": null, "id": "2", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ - "from async_kernel import Caller\n", - "\n", - "main_caller = Caller.get_instance()\n", - "main_caller" + "%callers" ] }, { "cell_type": "markdown", "id": "3", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "## Example\n", "**This example requires ipywidgets!**" @@ -64,7 +87,15 @@ "cell_type": "code", "execution_count": null, "id": "4", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, "outputs": [], "source": [ "import random\n", @@ -105,9 +136,17 @@ "cell_type": "code", "execution_count": null, "id": "5", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], - "source": [] + "source": [ + "%callers" + ] } ], "metadata": { diff --git a/docs/execute_mode.ipynb b/docs/execute_mode.ipynb index 34df8dcc2..7511d8685 100644 --- a/docs/execute_mode.ipynb +++ b/docs/execute_mode.ipynb @@ -3,18 +3,19 @@ { "cell_type": "markdown", "id": "0", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Execute mode\n", "\n", - "The user can tell the kernel that they want to execute the code concrrently in a `thread` or `task` by adding the comment `##thread` or `##task` respectively as the top line in the code cell.\n", - "\n", - "``` python\n", - "##thread\n", - "%callers\n", - "```\n", + "You can execute code concurrently by adding either `##thread` or `##task` at top of a code cell.\n", "\n", - "Provided the frontend supports it (Jupyterlab does, VS code doesn't), multiple cells can concurrently.\n", + "Executing multiple cells concurrently is possible if frontend supports it. Jupyterlab does and VScode does not.\n", "\n", "## Example\n", "\n", @@ -28,7 +29,13 @@ "cell_type": "code", "execution_count": null, "id": "1", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "async def demo():\n", @@ -64,11 +71,17 @@ "cell_type": "code", "execution_count": null, "id": "3", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, "outputs": [], "source": [ - "##queue ('queue' is the default run mode)\n", - "\n", "await demo()" ] }, @@ -76,7 +89,15 @@ "cell_type": "code", "execution_count": null, "id": "4", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, "outputs": [], "source": [ "# Tip: try running this cell while the previous cell is still busy.\n", @@ -86,7 +107,13 @@ { "cell_type": "markdown", "id": "5", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "### Execute mode: task\n", "``` python\n", @@ -103,7 +130,15 @@ "cell_type": "code", "execution_count": null, "id": "6", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, "outputs": [], "source": [ "##task\n", @@ -113,7 +148,13 @@ { "cell_type": "markdown", "id": "7", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "### Execute mode: thread\n", "``` python\n", @@ -126,7 +167,15 @@ "cell_type": "code", "execution_count": null, "id": "8", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, "outputs": [], "source": [ "##thread\n", @@ -137,20 +186,18 @@ "cell_type": "code", "execution_count": null, "id": "9", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "##thread\n", "%callers # magic provided by async kernel" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -169,7 +216,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.8.final.0" + "version": "3.11.13.final.0" } }, "nbformat": 4, diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 9c0c9b880..d8b9260c7 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -3,6 +3,7 @@ href="{{ page.nb_url }}" title="Download Notebook" class="md-content__button md-icon" + download="" > {% include ".icons/material/download.svg" %} diff --git a/docs/simple_example.ipynb b/docs/simple_example.ipynb new file mode 100644 index 000000000..474d09d51 --- /dev/null +++ b/docs/simple_example.ipynb @@ -0,0 +1,84 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "async def demo():\n", + " %callers\n", + " import anyio\n", + " import ipywidgets as ipw\n", + "\n", + " from async_kernel import Caller\n", + "\n", + " caller = Caller() # Use caller set the event in the waiting thread\n", + " b = ipw.Button(description=\"Continue\")\n", + " display(b)\n", + " for i in range(1, 4):\n", + " b.description = f\"Continue {i}\"\n", + " event = anyio.Event()\n", + " b.on_click(lambda _: caller.call_soon(event.set)) # noqa: B023\n", + " print(f\"Waiting {i}\", end=\"\\r\")\n", + " await event.wait()\n", + " b.close()\n", + " print(\"\\nDone!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "await demo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "##task\n", + "await demo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "##thread\n", + "await demo()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (async)", + "language": "python", + "name": "async" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13.final.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/mkdocs.yml b/mkdocs.yml index 1686ff402..60c79c3b9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,14 @@ copyright: "Copyright © 2025-present" extra: version: provider: mike + consent: + title: Cookie consent + description: >- + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. + extra_css: - stylesheets/extra.css theme: @@ -17,6 +25,8 @@ theme: - navigation.tabs - navigation.tabs.sticky - content.code.copy + - content.action.edit + - content.action.view - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#anchor-following - toc.integrate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#navigation-integration - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#back-to-top-button @@ -25,6 +35,8 @@ theme: - search.suggest # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-suggestions - search.highlight # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-highlighting - navigation.instant # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#instant-loading + icon: + annotation: material/information-outline font: text: Noto Sans Cham code: Noto Sans Mono @@ -49,9 +61,21 @@ theme: name: Switch to system preference plugins: - search + - open-in-new-tab - autorefs - mkdocs-jupyter: include_source: True + kernel_name: async-docs + execute: true + # include_requirejs: true + highlight_extra_classes: custom-css-classes + include: ["*.ipynb"] + # - privacy: + # enabled: !ENV + # links_attr_map: + # target: _blank # sponsors only - https://squidfunk.github.io/mkdocs-material/plugins/privacy/?h=external+lin#external-links + # - open-in-new-tab # https://github.com/JakubAndrysek/mkdocs-open-in-new-tab + # add_icon: true - mkdocstrings: enabled: true # custom_templates: templates @@ -121,11 +145,12 @@ markdown_extensions: nav: - Home: index.md - Usage: + - Simple example: simple_example.ipynb - Execute mode: execute_mode.ipynb - Caller notebook: caller.ipynb - Command line: command_line.md - API: api.md - - Info: + - About: - Contributing: contributing.md - Changelog: changelog.md - License: license.md diff --git a/pyproject.toml b/pyproject.toml index 1d9107dac..a45f503b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "comm>=0.2.2", "traitlets>=5.14.3", "jupyter_client>=8.6.3", - "jupyter_core>=5.8.1", "pyzmq>=26.0", "anyio>=4.8.0,<5.0.0", "typing_extensions>=4.14.1", @@ -44,7 +43,10 @@ async-kernel = "async_kernel.__main__:main" [dependency-groups] docs = ["mkdocs-material", "mkdocstrings[python]", - "mkdocs-jupyter" + "mkdocs-jupyter", + "mkdocs-open-in-new-tab", + "jupyterlab", + "ipywidgets" ] dev = [ "debugpy", diff --git a/src/async_kernel/__main__.py b/src/async_kernel/__main__.py index 9b0359fcb..1a864c61a 100644 --- a/src/async_kernel/__main__.py +++ b/src/async_kernel/__main__.py @@ -1,9 +1,10 @@ """The cli entry point for async_kernel.""" -from __future__ import annotations - # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + +from __future__ import annotations + import argparse import contextlib import shutil @@ -97,7 +98,7 @@ async def _start() -> None: backend = Backend.trio if "trio" in kernel_name.lower() else Backend.asyncio anyio.run(_start, backend=backend) except KeyboardInterrupt: - print("\nKernel stopped") + pass except BaseException as e: traceback.print_exception(e, file=sys.stderr) if sys.__stderr__ is not sys.stderr: @@ -105,6 +106,8 @@ async def _start() -> None: sys.exit(1) else: sys.exit(0) + finally: + print("\nKernel stopped: ", kernel.connection_file) if __name__ == "__main__": diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index d8963f9bb..7a48ae0f6 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -7,8 +7,10 @@ import json import pathlib import sys +from contextvars import ContextVar from typing import TYPE_CHECKING, Any, ClassVar, Literal +import anyio import IPython.core.release from IPython.core.displayhook import DisplayHook from IPython.core.displaypub import DisplayPublisher @@ -22,6 +24,7 @@ import async_kernel from async_kernel.caller import Caller from async_kernel.compiler import XCachingCompiler +from async_kernel.typing import Tags if TYPE_CHECKING: from async_kernel.kernel import Kernel @@ -124,6 +127,7 @@ class AsyncInteractiveShell(InteractiveShell): compile: Instance[XCachingCompiler] user_ns_hidden = Dict() _main_mod_cache = Dict() + _execute_request_timeout: ContextVar[float | None] = ContextVar("execute_request_timeout", default=None) @default("banner1") def _default_banner1(self): @@ -141,6 +145,14 @@ def _default_banner1(self): # will print a warning in the absence of readline. autoindent = CBool(False) + @property + def execute_request_timeout(self): + return self._execute_request_timeout.get() + + @execute_request_timeout.setter + def execute_request_timeout(self, value: float | None): + self._execute_request_timeout.set(value) + @observe("exit_now") def _update_exit_now(self, _): """stop eventloop when exit_now fires""" @@ -208,15 +220,16 @@ async def run_cell_async( preprocessing_exc_tuple: tuple | None = None, cell_id: str | None = None, ) -> ExecutionResult: - result = await super().run_cell_async( - raw_cell=raw_cell, - store_history=store_history, - silent=silent, - shell_futures=shell_futures, - transformed_cell=transformed_cell, - preprocessing_exc_tuple=preprocessing_exc_tuple, - cell_id=cell_id, - ) + with anyio.fail_after(delay=self.execute_request_timeout): + result: ExecutionResult = await super().run_cell_async( + raw_cell=raw_cell, + store_history=store_history, + silent=silent, + shell_futures=shell_futures, + transformed_cell=transformed_cell, + preprocessing_exc_tuple=preprocessing_exc_tuple, + cell_id=cell_id, + ) self.events.trigger("post_execute") if not silent: self.events.trigger("post_run_cell", result) @@ -224,6 +237,10 @@ async def run_cell_async( @override def _showtraceback(self, etype, evalue, stb): + if Tags.do_not_publish_error in async_kernel.utils.get_tags(): + return + if self.execute_request_timeout is not None and etype is self.kernel.CancelledError: + etype, evalue, stb = TimeoutError, "Cell execute timeout", [] self.kernel.iopub_send( msg_or_type="error", content={"traceback": stb, "ename": str(etype.__name__), "evalue": str(evalue)}, @@ -269,13 +286,13 @@ def connect_info(self, _): @line_magic def callers(self, _): - print("Active", "Protected", "\t", "Name") - print("─" * 70) + lines = ["\t".join(["Active", "Protected", "\t", "Name"]), "─" * 70] for caller in Caller.all_callers(active_only=False): symbol = " ✓" if caller.active else " ✗" current_thread: Literal["← current thread", ""] = "← current thread" if caller is Caller() else "" protected = " 🔐" if caller.protected else "" - print(symbol, protected, "", caller.thread.name, current_thread, sep="\t") + lines.append("\t".join([symbol, protected, "", caller.thread.name, current_thread])) + print(*lines, sep="\n") InteractiveShellABC.register(AsyncInteractiveShell) diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index 683acca3d..dff8576b3 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -31,10 +31,10 @@ from async_kernel.typing import P -__all__ = ["Caller", "CancelledError", "Future"] +__all__ = ["Caller", "Future", "FutureCancelledError"] -class CancelledError(anyio.ClosedResourceError): +class FutureCancelledError(anyio.ClosedResourceError): "Used to indicate a Future is cancelled." @@ -289,7 +289,7 @@ async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> No self.active = False for job in self._jobs: if not callable(job): - job[1][0].set_exception(CancelledError()) + job[1][0].set_exception(FutureCancelledError()) socket.close() self.iopub_sockets.pop(thread, None) self.taskgroup.cancel_scope.cancel() @@ -310,7 +310,7 @@ async def _wrap_call( if (delay_ := delay - time.monotonic() + starttime) > 0: await anyio.sleep(float(delay_)) result = func(*args, **kwargs) if callable(func) else func # pyright: ignore[reportAssignmentType] - while inspect.isawaitable(result): + if inspect.isawaitable(result): result: T = await result if fut.cancelled() and not scope.cancel_called: scope.cancel() @@ -323,7 +323,7 @@ async def _wrap_call( self._outstanding -= 1 # # update first for _to_thread_on_done if not fut.done(): if isinstance(e, self._cancelled_exception_class): - e = CancelledError() + e = FutureCancelledError() else: self.log.exception("Exception occurred while running %s", func, exc_info=e) fut.set_exception(e) @@ -410,19 +410,18 @@ def protected(self) -> bool: return self._protected @classmethod - def stop_all(cls, **kwgs) -> None: - "Stop all instances." - force = kwgs.get("_stop_protected", False) + def stop_all(cls, *, _stop_protected=False) -> None: + "A classmethod to stop all un-protected instances." for caller in tuple(reversed(cls._instances.values())): - caller.stop(force=force) + caller.stop(force=_stop_protected) @classmethod def get_instance(cls, name: str | None = "MainThread", *, create: bool = False) -> Self: - """Gets an instance of the Caller by name. + """A classmethod that gets an instance by name, possibly starting a new instance. Args: - name: Name of the caller instance thread. - create: If the Caller instance does not exist a new thread is created. + name: The name to identify the caller. + create: Create a new instance if one with the corresponding name does not already exist. """ for thread in cls._instances: if thread.name == name: @@ -434,25 +433,33 @@ def get_instance(cls, name: str | None = "MainThread", *, create: bool = False) @classmethod def to_thread(cls, func: Callable[P, T | Awaitable[T]], /, *args: P.args, **kwargs: P.kwargs) -> Future[T]: - """Call func in a separate thread. - - A pool of 'workers' is are used to provide an event loop - """ + """A classmethod to call func in a separate thread see also [to_thread_by_name][async_kernel.Caller.to_thread_by_name].""" return cls.to_thread_by_name(None, func, *args, **kwargs) @classmethod def to_thread_by_name( cls, name: str | None, func: Callable[P, T | Awaitable[T]], /, *args: P.args, **kwargs: P.kwargs ) -> Future[T]: - """Call the function in the Caller's thread. - - If a caller does not exist for `name` a new `Caller` is started. The one caveat being 'MainThread'. + """A classmethod to call func in the thread specified by name. Args: - name: name of the caller's thread. passing an empty string will provide a caller from the pool. - func: The function (awaitables permitted, though discouraged). + name: The name of the `Caller`. A new `Caller` is created if an instance corresponding to name [^notes]. + + [^notes]: 'MainThread' is special name that applies to the main thread and + will raise a runtime error if a Caller does not exist for the main thread. + + func: The function to call. If it returns an awaitable, the awaitable will be awaited. + Passing a coroutine as `func` discourage, but will be awaited. + *args: Arguments to use with func. **kwargs: Keyword arguments to use with func. + + Returns: + A future that can be awaited for the result of func. + + + + """ caller = ( cls._to_thread_pool.popleft() @@ -576,6 +583,6 @@ async def iter_items(task_status: TaskStatus[None]): fut.cancel() @classmethod - def all_callers(cls, active_only=True): + def all_callers(cls, active_only=True) -> list[Caller]: "Get a list of the callers." return [caller for caller in Caller._instances.values() if caller.active or not active_only] diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index db6351cf3..08297e1d1 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -33,22 +33,22 @@ from jupyter_client.connect import ConnectionFileMixin from jupyter_client.session import Session from jupyter_core.paths import jupyter_runtime_dir -from traitlets import CaselessStrEnum, CBool, Container, Dict, Instance, Int, Set, Tuple, UseEnum, default +from traitlets import CaselessStrEnum, CBool, Container, Dict, Float, Instance, Int, Set, Tuple, UseEnum, default from zmq import Context, Flag, PollEvent, Socket, SocketOption, SocketType, ZMQError -from async_kernel import _version, utils +from async_kernel import Caller, _version, utils from async_kernel.asyncshell import AsyncInteractiveShell -from async_kernel.caller import Caller, CancelledError from async_kernel.debugger import Debugger from async_kernel.iostream import OutStream from async_kernel.kernelspec import Backend, KernelName -from async_kernel.typing import ExecuteContent, ExecuteMode, Job, MsgType, NoValue, SocketID +from async_kernel.typing import ExecuteContent, ExecuteMode, Job, MetadataKeys, MsgType, NoValue, SocketID, Tags if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import AsyncGenerator, Callable from types import CoroutineType, FrameType from anyio.abc import TaskStatus + from IPython.core.interactiveshell import ExecutionResult from async_kernel.comm import CommManager @@ -181,6 +181,7 @@ class Kernel(ConnectionFileMixin): transport: CaselessStrEnum[str] = CaselessStrEnum( ["tcp", "ipc"] if sys.platform == "linux" else ["tcp"], default_value="tcp", config=True ) + cell_execute_timeout = Float(None, allow_none=True) def __new__(cls, **kwargs) -> Self: # noqa: ARG004 # There is only one instance. @@ -228,7 +229,7 @@ def execution_count(self): return self._execution_count_var.get(self._execution_count) @property - def kernel_info(self): + def kernel_info(self) -> dict[str, str | dict[str, str | dict[str, str | int]] | Any | tuple[Any, ...] | bool]: return { "protocol_version": _version.kernel_protocol_version, "implementation": "async_kernel", @@ -241,7 +242,7 @@ def kernel_info(self): } @default("help_links") - def _default_help_links(self): + def _default_help_links(self) -> tuple[dict[str, str], ...]: return ( { "text": "Async Kernel Reference ", @@ -294,14 +295,14 @@ def _default_shell(self): return AsyncInteractiveShell.instance(parent=self, kernel=self) @classmethod - def stop(cls): + def stop(cls) -> None: """Stop the kernel.""" if instance := cls._instance: cls._instance = None instance._stop_event.set() @asynccontextmanager - async def start_in_context(self): + async def start_in_context(self) -> AsyncGenerator[Self, Any]: """Start the Kernel in an already running anyio event loop.""" if self._sockets: msg = "Already started" @@ -683,9 +684,11 @@ async def execute_request(self, received_time: float, job: Job[ExecuteContent]): async def _execute_request_handler(self, job: Job[ExecuteContent]): """Perform the actual execute_request.""" content = job["msg"]["content"] + metadata = job["msg"].get("metadata") or {} if not (silent := content["silent"]): self._execution_count += 1 self._execution_count_var.set(self._execution_count) + self.shell.execute_request_timeout = metadata.get(MetadataKeys.timeout) or self.cell_execute_timeout self.iopub_send( msg_or_type="execute_input", content={"code": content["code"], "execution_count": self.execution_count}, @@ -699,16 +702,21 @@ async def _execute_request_handler(self, job: Job[ExecuteContent]): silent=silent, transformed_cell=self.shell.transform_cell(content["code"]), shell_futures=True, - cell_id=None if silent else job["msg"].get("metadata", {}).get("cellId"), + cell_id=metadata.get("cellId"), ) if not silent: self._interrupts.add(fut.cancel) fut.add_done_callback(lambda fut: self._interrupts.discard(fut.cancel)) - try: - result = await fut - except CancelledError: - result = None + result: ExecutionResult = await fut err = result.error_before_exec or result.error_in_exec if result else KernelInterruptError() + if (err) and ( + (Tags.suppress_error in metadata.get("tags", ())) # 1. + or (isinstance(err, self.CancelledError) and (self.shell.execute_request_timeout is not None)) # 2. + ): + # Suppress the error due to either: + # 1. tag + # 2. timeout + err = None reply_content = { "status": "error" if err else "ok", "execution_count": self.execution_count, @@ -719,7 +727,7 @@ async def _execute_request_handler(self, job: Job[ExecuteContent]): if not silent and content.get("stop_on_error"): self._stop_on_error_time = time.monotonic() self.log.info("An error occurred in a non-silent execution request") - self._send_reply(job, reply_content) + self._send_reply(job, content=reply_content) async def interrupt_request(self, job: Job): """Handle an interrupt request.""" diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 282355dfa..0ad561945 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -9,6 +9,8 @@ from typing_extensions import Sentinel if TYPE_CHECKING: + from collections.abc import Mapping + import zmq __all__ = ["DebugMessage", "Job", "Message", "MsgHeader", "SocketID"] @@ -56,6 +58,36 @@ class MsgType(enum.StrEnum): debug_request = "debug_request" +class MetadataKeys(enum.StrEnum): + """This is an enum of keys for [metadata in kernel messages](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata) + that are used in async_kernel. + + !!! Note + Metadata can be edited in Jupyter lab "Advanced tools" and Tags can be added using "common tools" in the [right side bar](https://jupyterlab.readthedocs.io/en/stable/user/interface.html#left-and-right-sidebar). + """ + + tags = "tags" + """The `tags` metadata key corresponds to is a list of strings. + + The list can be edited by the user in a notebook. + see also: [Tags][async_kernel.typing.Tags]. + """ + timeout = "timeout" + """The `timeout` metadata key is used to specify a timeout for execution of the code. + + The value should be a floating point value of the timeout in seconds. + """ + + +class Tags(enum.StrEnum): + """Tags recognised by the kernel""" + + suppress_error = "suppress-error" + """Ignore`stop_on_error` in context of the `execute request`.""" + do_not_publish_error = "do-not-publish-error" + """Prevent the shell from publishing error messages in context of the `execute request`.""" + + class MsgHeader(TypedDict): # https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header msg_id: str @@ -69,7 +101,7 @@ class MsgHeader(TypedDict): class Message(TypedDict, Generic[T]): header: MsgHeader parent_header: MsgHeader - metadata: dict[str, Any] + metadata: Mapping[MetadataKeys | str, Any] content: T buffers: list[bytearray | bytes] diff --git a/src/async_kernel/utils.py b/src/async_kernel/utils.py index 3de8c508b..c9023ce21 100644 --- a/src/async_kernel/utils.py +++ b/src/async_kernel/utils.py @@ -1,17 +1,28 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -"""Utility functions.""" from __future__ import annotations import contextlib import sys import threading +from typing import TYPE_CHECKING, Any import anyio import anyio.to_thread -__all__ = ["do_not_debug_this_thread", "mark_thread_pydev_do_not_trace", "wait_thread_event"] +import async_kernel + +if TYPE_CHECKING: + from collections.abc import Mapping + +__all__ = [ + "do_not_debug_this_thread", + "get_metadata", + "get_tags", + "mark_thread_pydev_do_not_trace", + "wait_thread_event", +] LAUNCHED_BY_DEBUGPY = "debugpy" in sys.modules @@ -49,3 +60,13 @@ def _in_thread_call(): await anyio.to_thread.run_sync(_in_thread_call) finally: event.set() + + +def get_metadata() -> Mapping[str, Any]: + "Gets metadata from current [`Job`][async_kernel.typing.Job] context if there is one." + return (async_kernel.Kernel().job.get("msg") or {}).get("metadata") or {} + + +def get_tags() -> list[str]: + "Gets the list of tags from current [`Job`][async_kernel.typing.Job] context if there is one." + return get_metadata().get("tags") or [] diff --git a/tests/test_caller.py b/tests/test_caller.py index dc0bf79bc..ef62f8da3 100644 --- a/tests/test_caller.py +++ b/tests/test_caller.py @@ -18,7 +18,7 @@ import zmq from anyio.abc import TaskStatus -from async_kernel.caller import Caller, CancelledError, Future +from async_kernel.caller import Caller, Future, FutureCancelledError @pytest.fixture(scope="module", params=["asyncio", "trio"]) @@ -318,7 +318,7 @@ async def cancelled(task_status: TaskStatus[None]): await tg.start(cancelled) tg.cancel_scope.cancel() for item in items: - with pytest.raises(CancelledError): + with pytest.raises(FutureCancelledError): await item async def test_call_early(self, anyio_backend): @@ -368,13 +368,13 @@ async def close_tsc(): ready.wait() never_called_future = caller.call_later(str, 10) proceed.set() - with pytest.raises(CancelledError): + with pytest.raises(FutureCancelledError): await fut assert fut.done() assert caller.stopped with pytest.raises(anyio.ClosedResourceError): caller.call_soon(time.sleep, 0) - with pytest.raises(CancelledError): + with pytest.raises(FutureCancelledError): await never_called_future @pytest.mark.parametrize("mode", ["async", "blocking"]) @@ -421,4 +421,4 @@ async def async_func(): await anyio.sleep(0) tg.cancel_scope.cancel() await anyio.sleep(0) - assert isinstance(fut.exception(), CancelledError) # pyright: ignore[reportPossiblyUnboundVariable] + assert isinstance(fut.exception(), FutureCancelledError) # pyright: ignore[reportPossiblyUnboundVariable] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index cc06eedef..e9836405b 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -9,7 +9,7 @@ import pathlib import threading import time -from typing import Literal, cast +from typing import TYPE_CHECKING, Literal, cast import anyio import pytest @@ -18,9 +18,12 @@ import async_kernel.utils from async_kernel.caller import Caller from async_kernel.comm import Comm -from async_kernel.typing import EXECUTE_MODE_PREFIX, ExecuteContent, ExecuteMode, Job, SocketID +from async_kernel.typing import EXECUTE_MODE_PREFIX, ExecuteContent, ExecuteMode, Job, SocketID, Tags from tests import utils +if TYPE_CHECKING: + from async_kernel.kernel import Kernel + @pytest.mark.parametrize("mode", ["direct", "proxy"]) async def test_iopub(kernel, mode: Literal["direct", "proxy"]): @@ -74,6 +77,28 @@ async def test_simple_print(kernel, client, quiet: bool): await utils.clear_iopub(client) +@pytest.mark.parametrize("mode", ["kernel_timeout", "metadata", "metadata-do-not-publish-error"]) +async def test_execute_kernel_timeout(client, kernel: Kernel, mode: str): + kernel.cell_execute_timeout = 0.1 if "kernel" in mode else None + last_stop_time = kernel._stop_on_error_time # pyright: ignore[reportPrivateUsage] + metadata: dict[str, float | list] = {"timeout": 0.1} + if "do-not-publish-error" in mode: + metadata["tags"] = ["do-not-publish-error"] + try: + code = "\n".join(["import anyio", "await anyio.sleep_forever()"]) + msg_id, content = await utils.execute(client, code=code, metadata=metadata, clear_pub=False) + assert last_stop_time == kernel._stop_on_error_time, "Should not cause cancellation" # pyright: ignore[reportPrivateUsage] + assert content["status"] == "ok" + await utils.check_pub_message(client, msg_id, execution_state="busy") + await utils.check_pub_message(client, msg_id, msg_type="execute_input") + if "do-not-publish-error" not in mode: + expected = {"traceback": [], "ename": "TimeoutError", "evalue": "Cell execute timeout"} + await utils.check_pub_message(client, msg_id, msg_type="error", **expected) + await utils.check_pub_message(client, msg_id, execution_state="idle") + finally: + kernel.cell_execute_timeout = None + + async def test_bad_message(client): client.shell_channel.socket.send(b"") client.control_channel.socket.send(b"") @@ -195,6 +220,13 @@ async def test_execute_request_success(client): await utils.clear_iopub(client) +async def test_execute_request_error_tag_ignore_error(client): + metadata = {"tags": [Tags.suppress_error]} + _, content = await utils.execute(client, "ignore error", metadata=metadata) + assert content["status"] == "ok" + await utils.clear_iopub(client) + + async def test_execute_request_error(client): reply = await utils.send_shell_message(client, "execute_request", {"code": "some invalid code", "silent": False}) assert reply["header"]["msg_type"] == "execute_reply" @@ -302,7 +334,7 @@ async def test_interrupt_request_blocking_exec_request(subprocess_kernels_client reply = await utils.send_control_message(client, "interrupt_request") reply = await utils.get_reply(client, msg_id) assert reply["content"]["status"] == "error" - assert reply["content"]["ename"] == "KernelInterruptError" + assert reply["content"]["ename"] == "FutureCancelledError" async def test_interrupt_request_blocking_task(subprocess_kernels_client): diff --git a/tests/test_main.py b/tests/test_main.py index f912f8012..6f293793d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + from __future__ import annotations import json @@ -103,6 +104,7 @@ async def wait_exit(): assert started out = capsys.readouterr().out assert "Starting kernel" in out + assert "Kernel stopped" in out utils.clear_kernel() diff --git a/tests/test_start_in_context.py b/tests/test_start_in_context.py index b1ac5ad09..1a458abff 100644 --- a/tests/test_start_in_context.py +++ b/tests/test_start_in_context.py @@ -1,5 +1,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + from __future__ import annotations from typing import TYPE_CHECKING, cast @@ -7,8 +8,8 @@ import pytest from async_kernel import Kernel -from async_kernel.kernel import SocketID from async_kernel.kernelspec import KernelName +from async_kernel.typing import SocketID from tests import utils if TYPE_CHECKING: diff --git a/tests/utils.py b/tests/utils.py index 308dee83b..268388cde 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -86,7 +86,7 @@ def validate_message(msg: Mapping[str, Any], msg_type="", parent=None): raise -async def execute(client: AsyncKernelClient, /, code="", clear_pub=True, **kwargs): +async def execute(client: AsyncKernelClient, /, code="", clear_pub=True, metadata: dict | None = None, **kwargs): """Send an execute_request to the kernel and return the msg_id and content of the reply from the kernel.""" assert isinstance(client, AsyncKernelClient) @@ -94,6 +94,7 @@ async def execute(client: AsyncKernelClient, /, code="", clear_pub=True, **kwarg msg = client.session.msg( "execute_request", header=header, + metadata=metadata, content=ExecuteContent( code=code, store_history=True, diff --git a/uv.lock b/uv.lock index a227edbe2..7721991c7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "anyio" @@ -25,6 +29,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + [[package]] name = "asttokens" version = "3.0.0" @@ -42,7 +92,6 @@ dependencies = [ { name = "comm" }, { name = "ipython" }, { name = "jupyter-client" }, - { name = "jupyter-core" }, { name = "matplotlib-inline" }, { name = "pyzmq" }, { name = "sniffio" }, @@ -72,8 +121,11 @@ dev = [ { name = "trio" }, ] docs = [ + { name = "ipywidgets" }, + { name = "jupyterlab" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material" }, + { name = "mkdocs-open-in-new-tab" }, { name = "mkdocstrings", extra = ["python"] }, ] @@ -83,7 +135,6 @@ requires-dist = [ { name = "comm", specifier = ">=0.2.2" }, { name = "ipython", specifier = ">=9.4" }, { name = "jupyter-client", specifier = ">=8.6.3" }, - { name = "jupyter-core", specifier = ">=5.8.1" }, { name = "matplotlib-inline", specifier = ">=0.1" }, { name = "pyzmq", specifier = ">=26.0" }, { name = "sniffio" }, @@ -113,11 +164,23 @@ dev = [ { name = "trio" }, ] docs = [ + { name = "ipywidgets" }, + { name = "jupyterlab" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material" }, + { name = "mkdocs-open-in-new-tab" }, { name = "mkdocstrings", extras = ["python"] }, ] +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -659,6 +722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -797,7 +869,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -871,6 +943,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] +[[package]] +name = "ipywidgets" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/48/d3dbac45c2814cb73812f98dd6b38bbcc957a4e7bb31d6ea9c03bf94ed87/ipywidgets-8.1.7.tar.gz", hash = "sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376", size = 116721, upload-time = "2025-05-05T12:42:03.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6a/9166369a2f092bd286d24e6307de555d63616e8ddb373ebad2b5635ca4cd/ipywidgets-8.1.7-py3-none-any.whl", hash = "sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb", size = 139806, upload-time = "2025-05-05T12:41:56.833Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -940,6 +1040,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "json5" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + [[package]] name = "jsonschema" version = "4.25.0" @@ -955,6 +1073,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.4.1" @@ -997,6 +1128,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, ] +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/3d/40bdb41b665d3302390ed1356cebd5917c10769d1f190ee4ca595900840e/jupyter_lsp-2.2.6.tar.gz", hash = "sha256:0566bd9bb04fd9e6774a937ed01522b555ba78be37bebef787c8ab22de4c0361", size = 48948, upload-time = "2025-07-18T21:35:19.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/7c/12f68daf85b469b4896d5e4a629baa33c806d61de75ac5b39d8ef27ec4a2/jupyter_lsp-2.2.6-py3-none-any.whl", hash = "sha256:283783752bf0b459ee7fa88effa72104d87dd343b82d5c06cf113ef755b15b6d", size = 69371, upload-time = "2025-07-18T21:35:16.585Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c8/ba2bbcd758c47f1124c4ca14061e8ce60d9c6fd537faee9534a95f83521a/jupyter_server-2.16.0.tar.gz", hash = "sha256:65d4b44fdf2dcbbdfe0aa1ace4a842d4aaf746a2b7b168134d5aaed35621b7f6", size = 728177, upload-time = "2025-05-12T16:44:46.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1f/5ebbced977171d09a7b0c08a285ff9a20aafb9c51bde07e52349ff1ddd71/jupyter_server-2.16.0-py3-none-any.whl", hash = "sha256:3d8db5be3bc64403b1c65b400a1d7f4647a5ce743f3b20dbdefe8ddb7b55af9e", size = 386904, upload-time = "2025-05-12T16:44:43.335Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/89/695805a6564bafe08ef2505f3c473ae7140b8ba431d381436f11bdc2c266/jupyterlab-4.4.5.tar.gz", hash = "sha256:0bd6c18e6a3c3d91388af6540afa3d0bb0b2e76287a7b88ddf20ab41b336e595", size = 23037079, upload-time = "2025-07-20T09:21:30.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/74/e144ce85b34414e44b14c1f6bf2e3bfe17964c8e5670ebdc7629f2e53672/jupyterlab-4.4.5-py3-none-any.whl", hash = "sha256:e76244cceb2d1fb4a99341f3edc866f2a13a9e14c50368d730d75d8017be0863", size = 12267763, upload-time = "2025-07-20T09:21:26.37Z" }, +] + [[package]] name = "jupyterlab-pygments" version = "0.3.0" @@ -1006,6 +1235,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, ] +[[package]] +name = "jupyterlab-server" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/c9/a883ce65eb27905ce77ace410d83587c82ea64dc85a48d1f7ed52bcfa68d/jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4", size = 76173, upload-time = "2024-07-16T17:02:04.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700, upload-time = "2024-07-16T17:02:01.115Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7d/160595ca88ee87ac6ba95d82177d29ec60aaa63821d3077babb22ce031a5/jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b", size = 213149, upload-time = "2025-05-05T12:32:31.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571, upload-time = "2025-05-05T12:32:29.534Z" }, +] + [[package]] name = "jupytext" version = "1.17.2" @@ -1130,6 +1386,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + [[package]] name = "markdown" version = "3.8.2" @@ -1414,6 +1679,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] +[[package]] +name = "mkdocs-open-in-new-tab" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/0e/f72a506a21bdb27b807124e00c688226848a388d1fd3980b80ae3cc27203/mkdocs_open_in_new_tab-1.0.8.tar.gz", hash = "sha256:3e0dad08cc9938b0b13097be8e0aa435919de1eeb2d1a648e66b5dee8d57e048", size = 5791, upload-time = "2024-11-18T13:15:13.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/94/44f3c868495481c868d08eea065c82803f1affd8553d3383b782f497613c/mkdocs_open_in_new_tab-1.0.8-py3-none-any.whl", hash = "sha256:051d767a4467b12d89827e1fea0ec660b05b027c726317fe4fceee5456e36ad2", size = 7717, upload-time = "2024-11-18T13:15:12.286Z" }, +] + [[package]] name = "mkdocstrings" version = "0.30.0" @@ -1546,6 +1823,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/f5/487434b1792c4f28c63876e4a896f2b6e953e2dc1f0b3940e912bd087755/nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1", size = 39998139, upload-time = "2025-08-01T11:10:52.676Z" }, ] +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + [[package]] name = "numpy" version = "2.3.2" @@ -1639,6 +1928,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, ] +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1814,6 +2112,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.51" @@ -1977,6 +2284,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2005,6 +2321,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] +[[package]] +name = "pywinpty" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/df/429cc505dc5f77ab0612c4b60bca2e3dcc81f6c321844ee017d6dc0f4a95/pywinpty-3.0.0.tar.gz", hash = "sha256:68f70e68a9f0766ffdea3fc500351cb7b9b012bcb8239a411f7ff0fc8f86dcb1", size = 28551, upload-time = "2025-08-12T20:33:46.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/34/30727e8a97709f5033277457df9a293ccddf34d6eb7528e6a1e910265307/pywinpty-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:29daa71ac5dcbe1496ef99f4cde85a732b1f0a3b71405d42177dbcf9ee405e5a", size = 2051048, upload-time = "2025-08-12T20:37:18.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/d9/bd2249815c305ef8f879b326db1fe1effc8e5f22bd88e522b4b55231aa6f/pywinpty-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:1e0c4b01e5b03b1531d7c5d0e044b8c66dd0288c6d2b661820849f2a8d91aec3", size = 2051564, upload-time = "2025-08-12T20:37:09.128Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/358b1a97c1d0714f288949372ec64a70884a7eceb3f887042b9ae0bea388/pywinpty-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:828cbe756b7e3d25d886fbd5691a1d523cd59c5fb79286bb32bb75c5221e7ba1", size = 2050856, upload-time = "2025-08-12T20:36:09.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/4249cfb4eb4fdad2c76bc96db0642a40111847c375b92e5b9f4bf289ddd6/pywinpty-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de0cbe27b96e5a2cebd86c4a6b8b4139f978d9c169d44a8edc7e30e88e5d7a69", size = 2050082, upload-time = "2025-08-12T20:36:28.591Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2139,6 +2467,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + [[package]] name = "rich" version = "14.1.0" @@ -2298,6 +2659,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -2357,6 +2736,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -2480,6 +2873,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" }, ] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250809" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/53/07dac71db45fb6b3c71c2fd29a87cada2239eac7ecfb318e6ebc7da00a3b/types_python_dateutil-2.9.0.20250809.tar.gz", hash = "sha256:69cbf8d15ef7a75c3801d65d63466e46ac25a0baa678d89d0a137fc31a608cc1", size = 15820, upload-time = "2025-08-09T03:14:14.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/5e/67312e679f612218d07fcdbd14017e6d571ce240a5ba1ad734f15a8523cc/types_python_dateutil-2.9.0.20250809-py3-none-any.whl", hash = "sha256:768890cac4f2d7fd9e0feb6f3217fce2abbfdfc0cadd38d11fba325a815e4b9f", size = 17707, upload-time = "2025-08-09T03:14:13.314Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1" @@ -2489,6 +2891,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -2586,6 +2997,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] +[[package]] +name = "webcolors" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, +] + [[package]] name = "webencodings" version = "0.5.1" @@ -2595,6 +3015,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c9656b1843892052a31c36d37ad42812b5da45c62191f7e/widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af", size = 1097428, upload-time = "2025-04-10T13:01:25.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503, upload-time = "2025-04-10T13:01:23.086Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From 925617a7a53c914aaf4e684ee52d06c7aa730be6 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Fri, 15 Aug 2025 20:48:33 +1000 Subject: [PATCH 04/18] Update github workflows --- .github/workflows/{test.yml => ci.yml} | 0 .github/workflows/pre-commit.yml | 6 +----- 2 files changed, 1 insertion(+), 5 deletions(-) rename .github/workflows/{test.yml => ci.yml} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 8c24804d1..637fbad9d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,10 +1,6 @@ name: Pre-commit -on: - pull_request: - push: - branches: [main] - +on: [push] jobs: main: runs-on: ubuntu-latest From b9c755bc5208d54fee450d34e2f515642f4b7b81 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sat, 16 Aug 2025 21:25:57 +1000 Subject: [PATCH 05/18] Kernel: - code shuffle - add return types - add map_message_to_handler Job: - added received_time - removed msg_type Add docstrings. --- src/async_kernel/kernel.py | 268 +++++++++++++++++++++---------------- src/async_kernel/typing.py | 74 ++++++++-- 2 files changed, 213 insertions(+), 129 deletions(-) diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 08297e1d1..ab890a8f4 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -20,6 +20,7 @@ import traceback import uuid from contextlib import asynccontextmanager +from logging import Logger, LoggerAdapter from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self @@ -44,7 +45,7 @@ from async_kernel.typing import ExecuteContent, ExecuteMode, Job, MetadataKeys, MsgType, NoValue, SocketID, Tags if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable + from collections.abc import AsyncGenerator, Callable, Generator from types import CoroutineType, FrameType from anyio.abc import TaskStatus @@ -138,22 +139,37 @@ class KernelInterruptError(InterruptedError): class Kernel(ConnectionFileMixin): - """An async kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. + """An asynchronous kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. To start the kernel. - Direct + === Shell - ``` python - Kernel.start() + At the command prompt. + + ``` shell + async-kernel -f . ``` - Inside an already running asycio context. + === Normal + ``` python + from async_kernel.__main__ import main + + main() + ``` + + === Direct - ``` python - kernel = Kernel() - async with kernel.start_in_context(): - await anyio.sleep_forever() + ``` python + Kernel.start() + ``` + + === Asynchronously inside anyio event loop. + + ``` python + kernel = Kernel() + async with kernel.start_in_context(): + await anyio.sleep_forever() ``` """ @@ -167,8 +183,9 @@ class Kernel(ConnectionFileMixin): _stop_on_error_time: float = 0 _interrupts: Container[set[Callable[[], object]]] = Set() _sockets: Dict[SocketID, zmq.Socket] = Dict() - _shell_handlers = Dict() - _control_handlers = Dict() + message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, Callable[[Job], CoroutineType]]] = ( + Dict() + ) _execution_count = Int(0) anyio_backend = UseEnum(Backend) help_links = Tuple() @@ -189,11 +206,11 @@ def __new__(cls, **kwargs) -> Self: # noqa: ARG004 cls._instance = instance = super().__new__(cls) return instance - def __init__(self, **kwargs): - if self._shell_handlers: + def __init__(self, **kwargs) -> None: + if self.message_handlers: return # Only initialize once super().__init__(**kwargs) - self._shell_handlers = { + self.message_handlers[SocketID.shell] = { MsgType.kernel_info_request: self.kernel_info_request, MsgType.comm_info_request: self.comm_info_request, MsgType.interrupt_request: self.interrupt_request, @@ -205,7 +222,7 @@ def __init__(self, **kwargs): MsgType.comm_msg: self.comm_msg, MsgType.comm_close: self.comm_close, } - self._control_handlers = self._shell_handlers | { + self.message_handlers[SocketID.control] = self.message_handlers[SocketID.shell] | { MsgType.shutdown_request: self.control_shutdown_request, MsgType.debug_request: self.debug_request, } @@ -224,7 +241,7 @@ def job(self) -> Job | dict: return {} @property - def execution_count(self): + def execution_count(self) -> int: "The execution count in context of the current coroutine, else the current value if there isn't one in context." return self._execution_count_var.get(self._execution_count) @@ -267,11 +284,11 @@ def _default_help_links(self) -> tuple[dict[str, str], ...]: ) @default("log") - def _default_log(self): + def _default_log(self) -> LoggerAdapter[Logger]: return logging.LoggerAdapter(logging.getLogger(self.__class__.__name__)) @default("kernel_name") - def _default_kernel_name(self): + def _default_kernel_name(self) -> Literal[KernelName.trio, KernelName.asyncio]: try: if sniffio.current_async_library() == "trio": return KernelName.trio @@ -280,18 +297,18 @@ def _default_kernel_name(self): return KernelName.asyncio @default("comm_manager") - def _default_comm_manager(self): + def _default_comm_manager(self) -> CommManager: from async_kernel import comm # noqa: PLC0415 comm.set_comm() return comm.get_comm_manager() @default("session") - def _default_session(self): + def _default_session(self) -> Any: return Session(parent=self) @default("shell") - def _default_shell(self): + def _default_shell(self) -> AsyncInteractiveShell: return AsyncInteractiveShell.instance(parent=self, kernel=self) @classmethod @@ -338,7 +355,7 @@ async def start_in_context(self) -> AsyncGenerator[Self, Any]: finally: Context.instance().term() - def _signal_handler(self, signum, frame: FrameType | None): + def _signal_handler(self, signum, frame: FrameType | None) -> None: "Handle interrupt signals." if self._interrupt_requested: self._interrupt_requested = False @@ -351,7 +368,7 @@ def _signal_handler(self, signum, frame: FrameType | None): else: signal.default_int_handler(signum, frame) - async def _start_heartbeat(self, task_status: TaskStatus[None]): + async def _start_heartbeat(self, task_status: TaskStatus[None]) -> None: # Reference: https://jupyter-client.readthedocs.io/en/stable/messaging.html#heartbeat-for-kernels def heartbeat(): @@ -369,13 +386,13 @@ def heartbeat(): ready_event.wait(10) task_status.started() - async def _start_stdin(self, task_status: TaskStatus[None]): + async def _start_stdin(self, task_status: TaskStatus[None]) -> None: socket = Context.instance().socket(SocketType.ROUTER) with self._bind_socket(SocketID.stdin, socket), contextlib.suppress(self.CancelledError): task_status.started() await anyio.sleep_forever() - async def _start_iopub_proxy(self, task_status: TaskStatus[None]): + async def _start_iopub_proxy(self, task_status: TaskStatus[None]) -> None: """Provide an io proxy""" def pub_proxy(): @@ -399,20 +416,7 @@ def pub_proxy(): ready_event.wait(10) task_status.started() - async def _start_control_loop(self, task_status: TaskStatus[None]): - async def run_in_control_event_loop(): - await caller.taskgroup.start(self._receive_msg_loop, SocketID.control) - ready_event.set() - - self.control_thread_caller = caller = Caller.start_new( - backend=self.anyio_backend, name="ControlThread", protected=True - ) - ready_event = threading.Event() - caller.call_soon(run_in_control_event_loop) - ready_event.wait(10) - task_status.started() - - async def _start_iopub(self, task_status: TaskStatus[None]): + async def _start_iopub(self, task_status: TaskStatus[None]) -> None: # Save IO self._original_io = sys.stdout, sys.stderr, sys.displayhook, builtins.input, self.getpass @@ -444,6 +448,19 @@ def flusher(string: str, name=name): # Reset IO sys.stdout, sys.stderr, sys.displayhook, builtins.input, getpass.getpass = self._original_io + async def _start_control_loop(self, task_status: TaskStatus[None]) -> None: + async def run_in_control_event_loop(): + await caller.taskgroup.start(self._receive_msg_loop, SocketID.control) + ready_event.set() + + self.control_thread_caller = caller = Caller.start_new( + backend=self.anyio_backend, name="ControlThread", protected=True + ) + ready_event = threading.Event() + caller.call_soon(run_in_control_event_loop) + ready_event.wait(10) + task_status.started() + async def _wait_stopped(self, task_status: TaskStatus[None]) -> None: task_status.started() try: @@ -455,7 +472,7 @@ async def _wait_stopped(self, task_status: TaskStatus[None]) -> None: async def _receive_msg_loop( self, socket_id: Literal[SocketID.control, SocketID.shell], *, task_status: TaskStatus[None] ) -> None: - """Receive messages from the socket, unpack them and call the matching request handler.""" + """Receive shell and control messages over the socket and call [`process_job`][async_kernel.Kernel.process_job].""" if ( sys.platform == "win32" and sniffio.current_async_library() == "asyncio" @@ -464,8 +481,7 @@ async def _receive_msg_loop( ): from anyio._core._asyncio_selector_thread import get_selector # noqa: PLC0415 - selector = get_selector() - utils.mark_thread_pydev_do_not_trace(selector._thread) # pyright: ignore[reportPrivateUsage] + utils.mark_thread_pydev_do_not_trace(get_selector()._thread) # pyright: ignore[reportPrivateUsage] socket: Socket[Literal[SocketType.ROUTER]] = Context.instance().socket(SocketType.ROUTER) with self._bind_socket(socket_id, socket): try: @@ -479,23 +495,16 @@ async def _receive_msg_loop( if socket_id == SocketID.shell: # Reset the frame to show the main thread is not blocked. self._last_interrupt_frame = None - msg_type = msg["header"]["msg_type"] - self.log.debug("*** _receive_msg_loop %s*** '%s' %s", socket_id, msg_type, msg) - job = Job( - socket_id=socket_id, - socket=socket, - ident=ident, - msg=msg, # pyright: ignore[reportArgumentType] - msg_type=msg_type, + self.log.debug("*** _receive_msg_loop %s*** %s", socket_id, msg) + await self.map_message_to_handler( + Job( + socket_id=socket_id, + socket=socket, + ident=ident, + msg=msg, # pyright: ignore[reportArgumentType] + received_time=time.monotonic(), + ) ) - if msg_type == MsgType.execute_request: - if self.get_execute_mode(job) is ExecuteMode.queue: - await self._execute_request_queue.send((time.monotonic(), job)) - else: - Caller().taskgroup.start_soon(self.execute_request, time.monotonic(), job) - else: - hdlrs = self._shell_handlers if socket_id == SocketID.shell else self._control_handlers - await self._run_handler(hdlrs.get(msg_type), job) except Exception as e: self.log.debug("Bad message on %s: %s", socket_id, e) continue @@ -504,27 +513,10 @@ async def _receive_msg_loop( except (zmq.ContextTerminated, self.CancelledError): return - @staticmethod - def get_execute_mode(job: Job[ExecuteContent]) -> ExecuteMode: - """Extract `ExecuteMode` from the job.""" - if m := job["msg"]["content"].get("execute_mode"): - # Respect an existing mode - return ExecuteMode(m) - if (c := job["msg"]["content"]["code"].strip().split("\n")[0].strip()) in tuple(ExecuteMode): - mode = ExecuteMode(c) - else: - mode = ExecuteMode.queue - if ( - job["msg"]["content"].get("silent", True) or (job["socket_id"] is SocketID.control) - ) and mode is ExecuteMode.queue: - mode = ExecuteMode.task - job["msg"]["content"]["execute_mode"] = mode - return mode - - async def _run_handler(self, handler: Callable[[Job], CoroutineType] | None, job: Job): + async def _run_handler(self, handler: Callable[[Job], CoroutineType] | None, job: Job) -> None: self._job_var.set(job) if not handler: - self.log.error("Unknown message type: %r", job["msg_type"]) + self.log.error("Unknown message type: %r", job["msg"]["header"]) return try: self._publish_status("busy", job) @@ -536,7 +528,7 @@ async def _run_handler(self, handler: Callable[[Job], CoroutineType] | None, job self._publish_status("idle", job) @contextlib.contextmanager - def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket): + def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket) -> Generator[None, Any, None]: """Bind a zmq.Socket storing a reference to the socket and the port details and closing the socket on leaving the context.""" if socket_id in self._sockets: @@ -565,7 +557,7 @@ def iopub_send( parent: dict[str, Any] | None | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] ident: bytes | list[bytes] | None = None, buffers: list[bytes] | None = None, - ): + ) -> None: """Send a message on the zmq iopub socket.""" if socket := Caller.iopub_sockets.get(thread := threading.current_thread()): msg = self.session.send( @@ -592,7 +584,7 @@ def iopub_send( buffers=buffers, ) - def _publish_status(self, status: Literal["busy", "idle"], job: Job): + def _publish_status(self, status: Literal["busy", "idle"], job: Job) -> None: """send status (busy/idle) on IOPub""" self.iopub_send( msg_or_type="status", @@ -601,14 +593,14 @@ def _publish_status(self, status: Literal["busy", "idle"], job: Job): ident=self.topic("status"), ) - def _send_reply(self, job: Job, content: dict | None = None): + def _send_reply(self, job: Job, content: dict | None = None) -> None: """Send a reply to the job with the specified content.""" content = content or {} if "status" not in content: content["status"] = "ok" msg = self.session.send( stream=job["socket"], - msg_or_type=job["msg_type"].replace("request", "reply"), + msg_or_type=job["msg"]["header"]["msg_type"].replace("request", "reply"), content=content, parent=job["msg"]["header"], # pyright: ignore[reportArgumentType] ident=job["ident"], @@ -618,19 +610,7 @@ def _send_reply(self, job: Job, content: dict | None = None): "send_reply: '%s' msg_id: %s %s", msg["msg_type"], job["msg"]["header"]["msg_id"], msg["content"] ) - async def _shell_execute_request_queue(self, *, task_status: TaskStatus[None]): - self._execute_request_queue, queue = anyio.create_memory_object_stream[tuple[float, Job]](max_buffer_size=1000) - with contextlib.suppress(self.CancelledError): - async with queue as receive_stream: - task_status.started() - async for job, received_time in receive_stream: - await self.execute_request(job, received_time) - - def topic(self, topic) -> bytes: - """prefixed topic for IOPub messages""" - return (f"kernel.{topic}").encode() - - def _input_request(self, prompt: str, *, password=False): + def _input_request(self, prompt: str, *, password=False) -> Any: job = self.job if not job["msg"].get("content", {}).get("allow_stdin", False): msg = "Stdin is not allowed in this context!" @@ -654,11 +634,61 @@ def _input_request(self, prompt: str, *, password=False): raise KernelInterruptError return self.session.recv(socket)[1]["content"]["value"] # pyright: ignore[reportOptionalSubscript] - async def kernel_info_request(self, job: Job): + async def _shell_execute_request_queue(self, *, task_status: TaskStatus[None]) -> None: + self._execute_request_queue, queue = anyio.create_memory_object_stream[Job](max_buffer_size=1000) + with contextlib.suppress(self.CancelledError): + async with queue as receive_stream: + task_status.started() + async for job in receive_stream: + await self.execute_request(job) + + async def map_message_to_handler(self, job: Job) -> None: + """Maps the Job wrapped message to the handler based on the message type. + + [Execute requests][async_kernel.types.MsgType.execute_request] + + Args: + job: A dictionary containing the message to be processed, including its + type, socket ID, and content. + + Returns: + None + """ + match job["msg"]["header"]["msg_type"]: + case MsgType.execute_request: + if self.get_execute_mode(job) is ExecuteMode.queue: + await self._execute_request_queue.send(job) + else: + Caller().taskgroup.start_soon(self.execute_request, job) + case _ as msg_type: + await self._run_handler(self.message_handlers[job["socket_id"]].get(msg_type), job) + + @staticmethod + def get_execute_mode(job: Job[ExecuteContent]) -> ExecuteMode: + """Extract `ExecuteMode` from the job.""" + if m := job["msg"]["content"].get("execute_mode"): + # Respect an existing mode + return ExecuteMode(m) + if (c := job["msg"]["content"]["code"].strip().split("\n")[0].strip()) in tuple(ExecuteMode): + mode = ExecuteMode(c) + else: + mode = ExecuteMode.queue + if ( + job["msg"]["content"].get("silent", True) or (job["socket_id"] is SocketID.control) + ) and mode is ExecuteMode.queue: + mode = ExecuteMode.task + job["msg"]["content"]["execute_mode"] = mode + return mode + + def topic(self, topic) -> bytes: + """prefixed topic for IOPub messages""" + return (f"kernel.{topic}").encode() + + async def kernel_info_request(self, job: Job) -> None: """Handle a kernel info request.""" self._send_reply(job, self.kernel_info) - async def comm_info_request(self, job: Job): + async def comm_info_request(self, job: Job) -> None: """Handle a comm info request.""" content = job["msg"]["content"] target_name = content.get("target_name", None) @@ -669,9 +699,9 @@ async def comm_info_request(self, job: Job): } self._send_reply(job, {"comms": comms}) - async def execute_request(self, received_time: float, job: Job[ExecuteContent]): + async def execute_request(self, job: Job[ExecuteContent]) -> None: """Process the execute request.""" - if (received_time < self._stop_on_error_time) and not job["msg"]["content"]["silent"]: + if (job["received_time"] < self._stop_on_error_time) and not job["msg"]["content"]["silent"]: self.log.info("Aborting execute_request: %s", job) self._publish_status("busy", job) content: dict[str, str | list[str]] = error_to_dict(RuntimeError("Aborting due to prior exception")) @@ -681,7 +711,7 @@ async def execute_request(self, received_time: float, job: Job[ExecuteContent]): return await self._run_handler(self._execute_request_handler, job) - async def _execute_request_handler(self, job: Job[ExecuteContent]): + async def _execute_request_handler(self, job: Job[ExecuteContent]) -> None: """Perform the actual execute_request.""" content = job["msg"]["content"] metadata = job["msg"].get("metadata") or {} @@ -729,7 +759,7 @@ async def _execute_request_handler(self, job: Job[ExecuteContent]): self.log.info("An error occurred in a non-silent execution request") self._send_reply(job, content=reply_content) - async def interrupt_request(self, job: Job): + async def interrupt_request(self, job: Job) -> None: """Handle an interrupt request.""" self._interrupt_requested = True if sys.platform == "win32": @@ -741,18 +771,18 @@ async def interrupt_request(self, job: Job): interrupter() self._send_reply(job) - async def complete_request(self, job: Job): + async def complete_request(self, job: Job) -> None: """Handle a completion request.""" parent = job["msg"] matches = await self.do_complete(parent["content"]["code"], parent["content"]["cursor_pos"]) self._send_reply(job, matches) - async def is_complete_request(self, job: Job): + async def is_complete_request(self, job: Job) -> None: """Handle an is_complete request.""" reply_content = await self.do_is_complete(job["msg"]["content"]["code"]) self._send_reply(job, reply_content) - async def inspect_request(self, job: Job): + async def inspect_request(self, job: Job) -> None: """Handle an inspect request.""" content = job["msg"]["content"] reply_content = await self.do_inspect( @@ -763,32 +793,32 @@ async def inspect_request(self, job: Job): ) self._send_reply(job, reply_content) - async def history_request(self, job: Job): + async def history_request(self, job: Job) -> None: """Handle a history request.""" reply_content = await self.do_history(**job["msg"]["content"]) self._send_reply(job, reply_content) - async def comm_open(self, job: Job): + async def comm_open(self, job: Job) -> None: self.comm_manager.comm_open(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_msg(self, job: Job): + async def comm_msg(self, job: Job) -> None: self.comm_manager.comm_msg(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_close(self, job: Job): + async def comm_close(self, job: Job) -> None: self.comm_manager.comm_close(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def control_shutdown_request(self, job: Job): + async def control_shutdown_request(self, job: Job) -> None: """Handle a shutdown request.""" await self.debugger.disconnect() self._send_reply(job, {"status": "ok", "restart": job["msg"]["content"].get("restart", False)}) self.stop() - async def debug_request(self, job: Job): + async def debug_request(self, job: Job) -> None: """Handle a debug request.""" content = await self.debugger.process_request(job["msg"]["content"]) self._send_reply(job=job, content=content) - async def do_complete(self, code, cursor_pos): + async def do_complete(self, code, cursor_pos) -> dict[str, Any]: """Completions from IPython, using Jedi.""" cursor_pos = cursor_pos if cursor_pos is not None else len(code) with _provisionalcompleter(): @@ -812,7 +842,7 @@ async def do_complete(self, code, cursor_pos): "metadata": {"_jupyter_types_experimental": comps}, } - async def do_is_complete(self, code): + async def do_is_complete(self, code) -> dict[str, Any]: """Handle an is_complete request.""" status, indent_spaces = self.shell.input_transformer_manager.check_complete(code) r = {"status": status} @@ -820,7 +850,7 @@ async def do_is_complete(self, code): r["indent"] = " " * indent_spaces return r - async def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=()): + async def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=()) -> dict[str, Any]: """Handle code inspection.""" name = token_at_cursor(code, cursor_pos) reply_content: dict[str, Any] = {"status": "ok"} @@ -847,7 +877,7 @@ async def do_history( n=None, pattern=None, unique=False, - ): + ) -> dict[str, list[Any]]: """Handle code history.""" history_manager = self.shell.history_manager assert history_manager @@ -861,12 +891,12 @@ async def do_history( hist = [] return {"history": list(hist)} - def excepthook(self, etype, evalue, tb): + def excepthook(self, etype, evalue, tb) -> None: """Handle an exception.""" # write uncaught traceback to 'real' stderr, not zmq-forwarder traceback.print_exception(etype, evalue, tb, file=sys.__stderr__) - def unraisablehook(self, unraisable: sys.UnraisableHookArgs, /): + def unraisablehook(self, unraisable: sys.UnraisableHookArgs, /) -> None: "Handle unraisable exceptions (during gc for instance)." exc_info = ( unraisable.exc_type, @@ -875,7 +905,7 @@ def unraisablehook(self, unraisable: sys.UnraisableHookArgs, /): ) self.log.exception(unraisable.err_msg, exc_info=exc_info, extra={"object": unraisable.object}) - def raw_input(self, prompt=""): + def raw_input(self, prompt="") -> Any: """Forward raw_input to frontends. Raises @@ -884,6 +914,6 @@ def raw_input(self, prompt=""): """ return self._input_request(str(prompt), password=False) - def getpass(self, prompt=""): + def getpass(self, prompt="") -> Any: """Forward getpass to frontends.""" return self._input_request(prompt, password=True) diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 0ad561945..b4b812ce4 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -13,8 +13,7 @@ import zmq -__all__ = ["DebugMessage", "Job", "Message", "MsgHeader", "SocketID"] - +__all__ = ["DebugMessage", "ExecuteMode", "Job", "Message", "MetadataKeys", "MsgHeader", "MsgType", "SocketID", "Tags"] NoValue = Sentinel("NoValue") @@ -25,37 +24,68 @@ class SocketID(enum.StrEnum): + "Mapping of `Kernel.port_` for sockets. [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introduction)." heartbeat = "hb" + "" shell = "shell" + "" stdin = "stdin" + "" control = "control" + "" iopub = "iopub" + "" EXECUTE_MODE_PREFIX: Final = "##" - +"The Prefix used for [ExecuteMode][async_kernel.typing.ExecuteMode] identifiers." class ExecuteMode(enum.StrEnum): + "An Enum of the Execute modes available for altering how [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) are handled." + + queue = f"{EXECUTE_MODE_PREFIX}queue" + "Add to the execute_request queue (default)." task = f"{EXECUTE_MODE_PREFIX}task" + "Execute as a task in the MainThread." thread = f"{EXECUTE_MODE_PREFIX}thread" - queue = f"{EXECUTE_MODE_PREFIX}queue" + "Execute in a caller worker thread." class MsgType(enum.StrEnum): + """An enumeration of Message `msg_type` for [shell and control messages]( https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel). + + + + [Control channel](https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-control-router-dealer-channel) only + """ + kernel_info_request = "kernel_info_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info)" comm_info_request = "comm_info_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info)" execute_request = "execute_request" - interrupt_request = "interrupt_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)" complete_request = "complete_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion)" is_complete_request = "is_complete_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness)" inspect_request = "inspect_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introspection)" history_request = "history_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#history)" comm_open = "comm_open" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#opening-a-comm)" comm_msg = "comm_msg" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-messages)" comm_close = "comm_close" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#tearing-down-comms)" # Control + interrupt_request = "interrupt_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt) (control only)" shutdown_request = "shutdown_request" + "[ref](shutdown_request) (control only)" debug_request = "debug_request" + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request) (control only)" class MetadataKeys(enum.StrEnum): @@ -89,42 +119,66 @@ class Tags(enum.StrEnum): class MsgHeader(TypedDict): + "" # https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header msg_id: str session: str username: str date: str - msg_type: str + msg_type: MsgType version: str class Message(TypedDict, Generic[T]): + "A [message](https://jupyter-client.readthedocs.io/en/stable/messaging.html#general-message-format)." header: MsgHeader + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header)" parent_header: MsgHeader + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#parent-header)" metadata: Mapping[MetadataKeys | str, Any] + "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata)" content: T + """[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata) + + See also: + + - [ExecuteContent][async_kernel.typing.ExecuteContent] + """ buffers: list[bytearray | bytes] + "" class Job(TypedDict, Generic[T]): - "A message bundled with its details." + "An [async_kernel.typing.Message][] bundled with sockit_id, socket and ident." msg: Message[T] + "" socket_id: Literal[SocketID.control, SocketID.shell] + "" socket: zmq.Socket + "" ident: bytes | list[bytes] - msg_type: MsgType + "" + received_time: float + "The time the message was received." class ExecuteContent(TypedDict): - # ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute + "[Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute). see also: [Message][async_kernel.typing.Message]" code: str + "The code to execute." silent: bool + "Modifies how code is executed. See also [get_execute_mode][async_kernel.kernel.get_execute_mode]." store_history: bool + "See ref." user_expressions: dict[str, str] + "See ref." allow_stdin: bool + "See ref." stop_on_error: bool - execute_mode: ExecuteMode | None # Added by the kernel when the message is received + "See ref." + execute_mode: ExecuteMode + """The execute mode. See also [get_execute_mode][async_kernel.kernel.get_execute_mode].""" DebugMessage = dict[str, Any] From 2de29108c5daf0124d2fa3c5189d9e9885907187 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 17 Aug 2025 10:59:23 +1000 Subject: [PATCH 06/18] Async shell docstring updates --- .gitignore | 8 - .vscode/spellright.dict | 1 + CONTRIBUTING.md | 10 +- README.md | 4 +- docs/api.md | 31 -- docs/notebooks/caller.ipynb | 365 ++++++++++++++++++++++++ docs/{ => notebooks}/execute_mode.ipynb | 4 +- docs/notebooks/overview.md | 15 + docs/notebooks/simple_example.ipynb | 311 ++++++++++++++++++++ docs/reference/asyncshell.md | 3 + docs/reference/caller.md | 1 + docs/reference/kernel.md | 15 + docs/reference/main.md | 9 + docs/reference/overview.md | 8 + docs/reference/typing.md | 2 + docs/reference/utils.md | 1 + docs/simple_example.ipynb | 84 ------ docs/usage.md | 20 ++ mkdocs.yml | 34 ++- pyproject.toml | 1 + src/async_kernel/asyncshell.py | 71 +++-- src/async_kernel/caller.py | 63 ++-- tests/test_caller.py | 22 +- tests/test_message_spec.py | 1 + 24 files changed, 900 insertions(+), 184 deletions(-) delete mode 100644 docs/api.md create mode 100644 docs/notebooks/caller.ipynb rename docs/{ => notebooks}/execute_mode.ipynb (96%) create mode 100644 docs/notebooks/overview.md create mode 100644 docs/notebooks/simple_example.ipynb create mode 100644 docs/reference/asyncshell.md create mode 100644 docs/reference/caller.md create mode 100644 docs/reference/kernel.md create mode 100644 docs/reference/main.md create mode 100644 docs/reference/overview.md create mode 100644 docs/reference/typing.md create mode 100644 docs/reference/utils.md delete mode 100644 docs/simple_example.ipynb create mode 100644 docs/usage.md diff --git a/.gitignore b/.gitignore index 2c7c2e021..254ebce7b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,20 +4,12 @@ cover dist site _build -docs/man/*.gz -docs/source/api/generated -docs/source/config/options -docs/source/interactive/magics-generated.txt -docs/gh-pages -IPython/html/notebook/static/mathjax -IPython/html/static/style/*.map *.py[co] __pycache__ *.egg-info *~ *.bak .ipynb_checkpoints -.tox .DS_Store \#*# .#* diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 4664dd49e..d52233e84 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -6,3 +6,4 @@ asyncio zmq IPyKernel anyio's +Asyc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d101d552e..203750acf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -This project is in development. Feel free to create an issue to provide feedback. +This project is under active development. Feel free to create an issue to provide feedback. ## Development @@ -74,7 +74,7 @@ mkdocs serve API documentation is included using [mkdocstrings](https://mkdocstrings.github.io/). -Docstrings are written in [google format without types](https://mkdocstrings.github.io/griffe/reference/docstrings/?h=google#google-style). +Docstrings are written in docstring format [google-notypes](https://mkdocstrings.github.io/griffe/reference/docstrings/?h=google#google-style). Typing information is included automatically by [griff](https://mkdocstrings.github.io/griffe). #### See also @@ -92,6 +92,12 @@ These links are not relevant for docstrings. - [footnotes](https://squidfunk.github.io/mkdocs-material/reference/footnotes/#usage) - [tooltips](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#usage) +### Deploy manually + +``` +mkdocs gh-deploy --force +``` + ## Releasing Async kernel TODO diff --git a/README.md b/README.md index b96e7ebd9..1356a431b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ Async kernel is designed to run [execute requests](https://jupyter-client.readth - [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, etc - No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets -![Simple demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811) ## Installation @@ -40,6 +39,9 @@ async-kernel add async-trio async-kernel -a async-trio ``` +[![Link to demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811)](https://fleming79.github.io/async-kernel/simple_example/ "Show demo notebook.") + + [^non-blocking-execution]: Shell messaging runs in a task separate to execute requests in the main thread. This means shell messages (including comms) can pass freely whilst an execute request is busy awaiting a result. [^run-concurrent]: Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 1bc9b8977..000000000 --- a/docs/api.md +++ /dev/null @@ -1,31 +0,0 @@ -# API - -- **[Caller](#caller)** -- **[Utils](#utils)** -- **[async-kernel (command)](#main-command-line-handler)** - -## Caller - -::: async_kernel.caller -options: -show_submodules: true - -## Kernel - -::: async_kernel.Kernel - -## Utils - -Utility functions that are widely useful. - -::: async_kernel.utils -options: -show_submodules: true - -## main (command line handler) - -::: async_kernel.__main__.main - -## types - -::: async_kernel.typing diff --git a/docs/notebooks/caller.ipynb b/docs/notebooks/caller.ipynb new file mode 100644 index 000000000..81433c757 --- /dev/null +++ b/docs/notebooks/caller.ipynb @@ -0,0 +1,365 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Caller\n", + "\n", + "`Caller` is a class that makes it easy to call code in different threads/tasks. \n", + "\n", + "One caller instance is created per thread, and each of those instances can be retrieved by name using the `Caller.get_instance` class method or in the thread in which it is running simply by `Caller()`. \n", + "\n", + "`Caller` is used by the [kernel](#usage-by-the-kernel) internally for running code, but can also be used directly by the user. Each caller starts its own iopub zmq socket.\n", + "\n", + "## Threads\n", + "\n", + "The caller manages a pool of worker threads (not related to anyio worker threads) or you can specify the name of a new thread which you can manage yourself. Each thread has its own anyio event loop in which the code will be called.\n", + "\n", + "Most methods that perform execution return an `async_kernel.Future`.\n", + "\n", + "## Usage by the kernel\n", + "\n", + "The Kernel runs two threads; one each for the shell and control. The thread names are 'MainThread' and 'ControlThread' respectively. You can easily get the main thread caller by using the classmethod `Caller.get_instance()`.\n", + "\n", + "Kernel uses one of `Caller().call_soon` `Caller.to_thread` depending on the [header directive](kernel_directive.ipynb#Kernel-directive)\n", + "\n", + "## Caller of the current thread\n", + "\n", + "To get the caller for the current thread use Caller(). It will raise a Runtime error if the thread doesn't have a running instance. " + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "To get the caller for the main thread.\n", + "\n", + "Ensure you're running an **async kernel**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "%callers" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Example\n", + "**This example requires ipywidgets!**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "do-not-publish-error" + ] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4b616ff79a844d4c9594f843d063a5a2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "818ff86d5ad84ebc87ca924c47f165bd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4ed7cb4d4c0749e0b749ca12d41eb4a3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "59559c85fb434147bcf74d5ee5ed62bc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95b350f0fcdf4b169cb45c67ac81660d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4037458da04041c0946fc0b302c353bf", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a1fd3d61b01f496abf16cc74688d5ff4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "21625909a2f64ca48872f8ef2d918431", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: 2\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e4bf1c1c649f42caa4f4de371f11d3a5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "86cea41153254bfd864976eef3adb4a5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: 1055\r" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception in message handler:\n", + "Traceback (most recent call last):\n", + " File \"C:\\code\\ipykernel\\src\\async_kernel\\kernel.py\", line 531, in _run_handler\n", + " await handler(job)\n", + " File \"C:\\code\\ipykernel\\src\\async_kernel\\kernel.py\", line 710, in _execute_request_handler\n", + " result: ExecutionResult = await fut\n", + " ^^^^^^^^^\n", + " File \"C:\\code\\ipykernel\\src\\async_kernel\\caller.py\", line 95, in result\n", + " raise self._exception\n", + "async_kernel.caller.FutureCancelledError\n" + ] + } + ], + "source": [ + "import random\n", + "import time\n", + "\n", + "import ipywidgets as ipw\n", + "\n", + "from async_kernel import Caller\n", + "\n", + "outputs = {}\n", + "\n", + "\n", + "def my_func(n):\n", + " caller = Caller()\n", + " if not (out := outputs.get(caller)):\n", + " outputs[caller] = out = ipw.HTML(description=str(caller))\n", + " out.style.description_width = \"220px\"\n", + " display(out)\n", + " sleep_time = random.random() / 4\n", + " out.value = f\"{n=:04d} sleeping {sleep_time * 1000:03.0f} ms\"\n", + " time.sleep(sleep_time)\n", + " return n\n", + "\n", + "\n", + "async def run_forever():\n", + " n = 0\n", + " while True:\n", + " n += 1\n", + " yield Caller.to_thread(my_func, n)\n", + "\n", + "\n", + "async for fut in Caller.as_completed(run_forever()):\n", + " result = await fut\n", + " print(f\"Finished: {result}\", end=\"\\r\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t← current thread\n", + " ✓\t 🔐\t\tControlThread\t\n", + " ✓\t\t\tThread-3 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-4 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-5 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-6 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-7 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-8 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-9 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-10 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-11 (anyio_run_caller)\t\n", + " ✓\t\t\tThread-12 (anyio_run_caller)\t\n" + ] + } + ], + "source": [ + "%callers" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (async)", + "language": "python", + "name": "async" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13.final.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/execute_mode.ipynb b/docs/notebooks/execute_mode.ipynb similarity index 96% rename from docs/execute_mode.ipynb rename to docs/notebooks/execute_mode.ipynb index 7511d8685..46537539f 100644 --- a/docs/execute_mode.ipynb +++ b/docs/notebooks/execute_mode.ipynb @@ -15,7 +15,7 @@ "\n", "You can execute code concurrently by adding either `##thread` or `##task` at top of a code cell.\n", "\n", - "Executing multiple cells concurrently is possible if frontend supports it. Jupyterlab does and VScode does not.\n", + "Executing multiple cells concurrently is possible if the frontend supports it. Jupyterlab does and VScode does not.\n", "\n", "## Example\n", "\n", @@ -216,7 +216,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13.final.0" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/docs/notebooks/overview.md b/docs/notebooks/overview.md new file mode 100644 index 000000000..8e046c594 --- /dev/null +++ b/docs/notebooks/overview.md @@ -0,0 +1,15 @@ +# Overview + +Notebooks in this documentation show the result of each cell after executing for a short duration (~100ms). + +You can download the notebook with the button at the top right of the page for the notebook. + +### Issues +#### Inter-notebook links + +Unfortunately, links between notebooks in the documentation don't work: see issue [#157:](https://github.com/danielfrg/mkdocs-jupyter/issues/157). + +#### Widgets + +Unfortunately, widgets don't render correctly. They wouldn't be functional even if the did render, so no big deal. See issue:[#180](https://github.com/danielfrg/mkdocs-jupyter/issues/180). + \ No newline at end of file diff --git a/docs/notebooks/simple_example.ipynb b/docs/notebooks/simple_example.ipynb new file mode 100644 index 000000000..7f86b4b4e --- /dev/null +++ b/docs/notebooks/simple_example.ipynb @@ -0,0 +1,311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "10ab52e6", + "metadata": {}, + "source": [ + "### Overview\n", + "\n", + "This example demonstrates different ways that the same code can be executed.\n", + "\n", + "1. Define the coroutine function. \n", + " 1. Cell 'magic' `%callers` prints a list of [Caller](https://fleming79.github.io/async-kernel/api/#async_kernel.caller.Caller) instances and the thread in which it is executing.\n", + " 2. A button is created and and it runs a loop where the user s\n", + "2. Execute `demo` normally.\n", + "3. Execute `demo` concurrently in a task.\n", + "4. Execute `demo` in a task.\n", + "\n", + "![Simple demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "import anyio\n", + "import ipywidgets as ipw\n", + "\n", + "from async_kernel import Caller\n", + "\n", + "\n", + "async def demo():\n", + " %callers\n", + " caller = Caller() # Use caller set the event in the waiting thread\n", + " b = ipw.Button(description=\"Continue\")\n", + " display(b)\n", + " for i in range(1, 4):\n", + " b.description = f\"Continue {i}\"\n", + " event = anyio.Event()\n", + " b.on_click(lambda _: caller.call_soon(event.set)) # noqa: B023\n", + " print(f\"Waiting {i}\", end=\"\\r\")\n", + " await event.wait()\n", + " b.close()\n", + " print(\"\\nDone!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t← current thread\n", + " ✓\t 🔐\t\tControlThread\t\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2b0c5b62fe5f41d197e30681c6b663ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Continue', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting 3\n", + "Done!\n" + ] + } + ], + "source": [ + "await demo()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t← current thread\n", + " ✓\t 🔐\t\tControlThread\t\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ec8d324b96f14ca8824299d785143970", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Continue', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting 3\n", + "Done!\n" + ] + } + ], + "source": [ + "##task\n", + "await demo()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t\n", + " ✓\t 🔐\t\tControlThread\t\n", + " ✓\t\t\tThread-3 (anyio_run_caller)\t← current thread\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2c586e969b3f43b2b99cc208bbcee54c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Continue', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting 3\n", + "Done!\n" + ] + } + ], + "source": [ + "##thread\n", + "await demo()" + ] + }, + { + "cell_type": "markdown", + "id": "76720a7e-8c90-49f1-bfe9-18ce893b481a", + "metadata": {}, + "source": [ + "[test](callers.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "18abc375", + "metadata": {}, + "source": [ + "## Caller.as_completed\n", + "\n", + "See also: the [caller](https://fleming79.github.io/async-kernel/caller/) notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "58d08376", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t← current thread\n", + " ✓\t 🔐\t\tControlThread\t\n", + " ✓\t\t\tThread-3 (anyio_run_caller)\t\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e4d853b05e5b405ba74a7cd3f54e74b9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Continue', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active\tProtected\t\t\tName\n", + "──────────────────────────────────────────────────────────────────────\n", + " ✓\t 🔐\t\tMainThread\t← current thread\n", + " ✓\t 🔐\t\tControlThread\t\n", + " ✓\t\t\tThread-3 (anyio_run_caller)\t\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c127b6a7796e45ee905124174b89f953", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Continue', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting 3\n", + "Done!\n", + "\n", + "Done!\n" + ] + } + ], + "source": [ + "# \n", + "\n", + "async for _ in Caller.as_completed( Caller().call_soon(demo) for _ in range(2)):\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad04634d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (async)", + "language": "python", + "name": "async" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13.final.0" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/reference/asyncshell.md b/docs/reference/asyncshell.md new file mode 100644 index 000000000..50391f955 --- /dev/null +++ b/docs/reference/asyncshell.md @@ -0,0 +1,3 @@ +# Shell module + +::: async_kernel.asyncshell \ No newline at end of file diff --git a/docs/reference/caller.md b/docs/reference/caller.md new file mode 100644 index 000000000..04541fbde --- /dev/null +++ b/docs/reference/caller.md @@ -0,0 +1 @@ +::: async_kernel.caller \ No newline at end of file diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md new file mode 100644 index 000000000..c6cdfbb7f --- /dev/null +++ b/docs/reference/kernel.md @@ -0,0 +1,15 @@ +# Kernel module + +::: async_kernel.kernel + +# Kernel messaging + +`shell` and `control` messages are processed with the [_receive_msg_loop][async_kernel.Kernel._receive_msg_loop] running running in event loops in separate threads. The `shell` thread the "MainThread" and `control`thread is provided by a *protected* [Caller][async_kernel.Caller] thread named "CallerThread". + +The + +::: async_kernel.Kernel._receive_msg_loop + +The + +::: async_kernel.Kernel._shell_execute_request_queue diff --git a/docs/reference/main.md b/docs/reference/main.md new file mode 100644 index 000000000..7f48cf7b5 --- /dev/null +++ b/docs/reference/main.md @@ -0,0 +1,9 @@ +## Command prompt + +`async-kernel` is installed at the command prompt. You can use it to: + +- Launch a kernel manually +- Define a new kernel spec +- Remove an existing kernel spec + +:::async_kernel.__main__.main \ No newline at end of file diff --git a/docs/reference/overview.md b/docs/reference/overview.md new file mode 100644 index 000000000..5f5a31d9d --- /dev/null +++ b/docs/reference/overview.md @@ -0,0 +1,8 @@ +# Overview + +The reference section provides documentation for each module in async kernel. + +## Highlights + +- [Caller][async_kernel.caller.Caller] +- [command prompt (main)][async_kernel.__main__.main] \ No newline at end of file diff --git a/docs/reference/typing.md b/docs/reference/typing.md new file mode 100644 index 000000000..af380911d --- /dev/null +++ b/docs/reference/typing.md @@ -0,0 +1,2 @@ +::: async_kernel.typing + \ No newline at end of file diff --git a/docs/reference/utils.md b/docs/reference/utils.md new file mode 100644 index 000000000..4e7a87776 --- /dev/null +++ b/docs/reference/utils.md @@ -0,0 +1 @@ +::: async_kernel.utils \ No newline at end of file diff --git a/docs/simple_example.ipynb b/docs/simple_example.ipynb deleted file mode 100644 index 474d09d51..000000000 --- a/docs/simple_example.ipynb +++ /dev/null @@ -1,84 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "0", - "metadata": {}, - "outputs": [], - "source": [ - "async def demo():\n", - " %callers\n", - " import anyio\n", - " import ipywidgets as ipw\n", - "\n", - " from async_kernel import Caller\n", - "\n", - " caller = Caller() # Use caller set the event in the waiting thread\n", - " b = ipw.Button(description=\"Continue\")\n", - " display(b)\n", - " for i in range(1, 4):\n", - " b.description = f\"Continue {i}\"\n", - " event = anyio.Event()\n", - " b.on_click(lambda _: caller.call_soon(event.set)) # noqa: B023\n", - " print(f\"Waiting {i}\", end=\"\\r\")\n", - " await event.wait()\n", - " b.close()\n", - " print(\"\\nDone!\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [ - "await demo()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ - "##task\n", - "await demo()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3", - "metadata": {}, - "outputs": [], - "source": [ - "##thread\n", - "await demo()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python (async)", - "language": "python", - "name": "async" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.13.final.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 000000000..d44720be4 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,20 @@ +# Usage + +Async kernel is asynchronous by design, shell messaging runs in a loop in the main thread + +## Tips + +
+ +- Use [async_kernel.Caller.call_soon][] or [async_kernel.Caller.call_later][] to run code in tasks to support either backend.(1) +- Use [anyio](https://anyio.readthedocs.io) or async functions corresponding to the anyio backend(2) freely in the main thread. +- Use [async_kernel.Caller.start_new][] to start a thread with the opposite backend. +- Start a new Caller if there are functions require the opposite asynchronous backend. + +
+1. Caller provides methods for thread safe scheduling and awaiting the result using [`Futures`][async_kernel.caller.Future]) + 1. Use `Caller.get_instance()` to get the `Caller` for the main thread. + 2. Use `Caller()` to get the `Caller` for the current thread. + +2. Async-kernel runs in anyio. +3. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 60c79c3b9..5e8217c2b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,6 +4,7 @@ site_author: "" site_url: https://fleming79.github.io/async-kernel/ repo_name: fleming79/async-kernel repo_url: https://github.com/fleming79/async-kernel +edit_uri: edit/main/docs/ copyright: "Copyright © 2025-present" extra: version: @@ -35,6 +36,7 @@ theme: - search.suggest # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-suggestions - search.highlight # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-highlighting - navigation.instant # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#instant-loading + icon: annotation: material/information-outline font: @@ -70,12 +72,16 @@ plugins: # include_requirejs: true highlight_extra_classes: custom-css-classes include: ["*.ipynb"] + - git-revision-date-localized: + type: timeago + enabled: !ENV [CI, false] + enable_creation_date: true + # - privacy: # enabled: !ENV # links_attr_map: # target: _blank # sponsors only - https://squidfunk.github.io/mkdocs-material/plugins/privacy/?h=external+lin#external-links # - open-in-new-tab # https://github.com/JakubAndrysek/mkdocs-open-in-new-tab - # add_icon: true - mkdocstrings: enabled: true # custom_templates: templates @@ -145,11 +151,27 @@ markdown_extensions: nav: - Home: index.md - Usage: - - Simple example: simple_example.ipynb - - Execute mode: execute_mode.ipynb - - Caller notebook: caller.ipynb - - Command line: command_line.md - - API: api.md + - usage.md + - Notebooks: + - Overview: notebooks/overview.md + - Simple example: notebooks/simple_example.ipynb + - Execute mode: notebooks/execute_mode.ipynb + - Caller notebook: notebooks/caller.ipynb + - Command line: command_line.md + - Reference: + - reference/overview.md + - Caller & Future: + - reference/caller.md + - Command prompt: + - reference/main.md + - Kernel: + - reference/kernel.md + - Async shell: + - reference/asyncshell.md + - Types: + - reference/typing.md + - Utils: + - reference/utils.md - About: - Contributing: contributing.md - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index a45f503b6..1829de5c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ docs = ["mkdocs-material", "mkdocstrings[python]", "mkdocs-jupyter", "mkdocs-open-in-new-tab", + "mkdocs-git-revision-date-localized-plugin", "jupyterlab", "ipywidgets" ] diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index 7a48ae0f6..fb825db3d 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -34,8 +34,9 @@ class AsyncDisplayHook(DisplayHook): - """A displayhook subclass that publishes data using ZeroMQ. This is intended - to work with an InteractiveShell instance. It sends a dict of different + """A displayhook subclass that publishes data using ZeroMQ. + + This is intended to work with an InteractiveShell instance. It sends a dict of different representations of the object.""" kernel: Instance[Kernel] = Instance("async_kernel.Kernel", ()) @@ -43,27 +44,27 @@ class AsyncDisplayHook(DisplayHook): @property @override - def prompt_count(self): + def prompt_count(self) -> int: return self.kernel.execution_count @override - def start_displayhook(self): + def start_displayhook(self) -> None: """Start the display hook.""" self.content = {} @override - def write_output_prompt(self): + def write_output_prompt(self) -> None: """Write the output prompt.""" self.content["execution_count"] = self.prompt_count @override - def write_format_data(self, format_dict, md_dict=None): + def write_format_data(self, format_dict, md_dict=None) -> None: """Write format data to the message.""" self.content["data"] = format_dict self.content["metadata"] = md_dict @override - def finish_displayhook(self): + def finish_displayhook(self) -> None: """Finish up all displayhook activities.""" if self.content: self.kernel.iopub_send("display_data", content=self.content) @@ -104,7 +105,7 @@ def publish( # pyright: ignore[reportIncompatibleMethodOverride] ) @override - def clear_output(self, wait=False): + def clear_output(self, wait=False) -> None: """Clear output associated with the current execution (cell). Args: @@ -116,7 +117,7 @@ def clear_output(self, wait=False): class AsyncInteractiveShell(InteractiveShell): - """A subclass of InteractiveShell for ZMQ.""" + """A modified IPython [InteractiveShell][IPython.core.interactiveshell.InteractiveShell] to work with [Async kernel][async_kernel.Kernel].""" displayhook_class = Type(AsyncDisplayHook) display_pub_class = Type(AsyncDisplayPublisher) @@ -130,7 +131,7 @@ class AsyncInteractiveShell(InteractiveShell): _execute_request_timeout: ContextVar[float | None] = ContextVar("execute_request_timeout", default=None) @default("banner1") - def _default_banner1(self): + def _default_banner1(self) -> str: return ( f"Python {sys.version}\n" f"Async kernel ({self.kernel.kernel_name})\n" @@ -146,53 +147,59 @@ def _default_banner1(self): autoindent = CBool(False) @property - def execute_request_timeout(self): + def execute_request_timeout(self) -> float | None: + """A timeout in context of the [run_cell_async][async_kernel.Kernel.AsyncInteractiveShell]. + + See also: + - [async_kernel.typing.MetadataKeys.timeout][]. + - + """ return self._execute_request_timeout.get() @execute_request_timeout.setter - def execute_request_timeout(self, value: float | None): + def execute_request_timeout(self, value: float | None) -> None: self._execute_request_timeout.set(value) @observe("exit_now") - def _update_exit_now(self, _): + def _update_exit_now(self, _) -> None: """stop eventloop when exit_now fires""" if self.exit_now: self.kernel.stop() - def ask_exit(self): + def ask_exit(self) -> None: if self.kernel.raw_input("Are you sure you want to stop the kernel?\ny/[n]\n") == "y": self.exit_now = True @override - def init_create_namespaces(self, user_module=None, user_ns=None): + def init_create_namespaces(self, user_module=None, user_ns=None) -> None: return @override - def save_sys_module_state(self): + def save_sys_module_state(self) -> None: return @override - def init_sys_modules(self): + def init_sys_modules(self) -> None: return @property @override - def execution_count(self): + def execution_count(self) -> int: return self.kernel.execution_count @execution_count.setter - def execution_count(self, value): + def execution_count(self, value) -> None: return @property @override - def user_ns(self): + def user_ns(self) -> dict[Any, Any]: if not hasattr(self, "_user_ns"): self.user_ns = {} return self._user_ns @user_ns.setter - def user_ns(self, ns: dict): + def user_ns(self, ns: dict) -> None: assert hasattr(ns, "clear") assert isinstance(ns, dict) self._user_ns = ns @@ -200,12 +207,12 @@ def user_ns(self, ns: dict): @property @override - def user_global_ns(self): + def user_global_ns(self) -> dict[Any, Any]: return self.user_ns @property @override - def ns_table(self): + def ns_table(self) -> dict[str, dict[Any, Any] | dict[str, Any]]: return {"user_global": self.user_ns, "user_local": self.user_ns, "builtin": builtins.__dict__} @override @@ -220,6 +227,11 @@ async def run_cell_async( preprocessing_exc_tuple: tuple | None = None, cell_id: str | None = None, ) -> ExecutionResult: + """Run a complete IPython cell asynchronously. + + This function runs [execute requests][async_kernel.Kernel.execute_request] for the kernel + wrapping [InteractiveShell][IPython.core.interactiveshell.InteractiveShell.run_cell_async]. + """ with anyio.fail_after(delay=self.execute_request_timeout): result: ExecutionResult = await super().run_cell_async( raw_cell=raw_cell, @@ -236,7 +248,7 @@ async def run_cell_async( return result @override - def _showtraceback(self, etype, evalue, stb): + def _showtraceback(self, etype, evalue, stb) -> None: if Tags.do_not_publish_error in async_kernel.utils.get_tags(): return if self.execute_request_timeout is not None and etype is self.kernel.CancelledError: @@ -247,22 +259,22 @@ def _showtraceback(self, etype, evalue, stb): ) @override - def init_magics(self): + def init_magics(self) -> None: """Initialize magics.""" super().init_magics() self.register_magics(KernelMagics) @override - def enable_gui(self, gui=None): + def enable_gui(self, gui=None) -> None: pass @magics_class class KernelMagics(Magics): - """Kernel magics.""" + """Extra magics for async kernel.""" @line_magic - def connect_info(self, _): + def connect_info(self, _) -> None: """Print information for connecting other clients to this kernel.""" kernel = async_kernel.Kernel() @@ -285,7 +297,8 @@ def connect_info(self, _): ) @line_magic - def callers(self, _): + def callers(self, _) -> None: + "Print a table of [Callers][async_kernel.Callers], indicating if it is acttive, protect and on the current thread." lines = ["\t".join(["Active", "Protected", "\t", "Name"]), "─" * 70] for caller in Caller.all_callers(active_only=False): symbol = " ✓" if caller.active else " ✗" diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index dff8576b3..decbfc62c 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -31,23 +31,24 @@ from async_kernel.typing import P -__all__ = ["Caller", "Future", "FutureCancelledError"] +__all__ = ["Caller", "Future", "FutureCancelledError", "InvalidStateError"] class FutureCancelledError(anyio.ClosedResourceError): - "Used to indicate a Future is cancelled." + "Used to indicate a `Future` is cancelled." class InvalidStateError(RuntimeError): - pass + "An invalid state of a [Future][async_kernel.caller.Future]." class Future(Awaitable[T]): """ - A class representing a future result modelled on the asyncio class [`Future`](https://docs.python.org/3/library/asyncio-future.html#futures) . + A class representing a future result modelled on Asyncio's [`Future`](https://docs.python.org/3/library/asyncio-future.html#futures). This class provides an anyio compatible Future primitive. It is designed - to work with the Caller to enable thread-safe, event loop function calls. + to work with `Caller` to enable thread-safe calling, setting and awaiting + execution results. """ __slots__ = [ @@ -104,9 +105,11 @@ def wait_sync(self) -> T: return self._result def set_result(self, value: T) -> None: + "Set the result (thread-safe using Caller)." self._set_value("result", value) def set_exception(self, exception: BaseException) -> None: + "Set the exception (thread-safe using Caller)." self._set_value("exception", exception) def _set_value(self, mode: Literal["result", "exception"], value) -> None: @@ -133,7 +136,7 @@ def set_value(): Caller(self.thread).call_no_context(func=set_value) except RuntimeError: msg = ( - f"The current thread is not {self.thread.name} and a caller does not exist for that thread either." + f"The current thread is not {self.thread.name} and a `Caller` does not exist for that thread either." ) raise RuntimeError(msg) from None else: @@ -142,19 +145,24 @@ def set_value(): def done(self) -> bool: """Return True if the Future is done. - Done means either that a result / exception are available.""" + Done means either that a result / exception is available.""" return self._event_done.is_set() def add_done_callback(self, fn: Callable[[Self], object]) -> None: """Add a callback for when the callback is done (not thread-safe). + + If the Future is already done it will be scheduled for calling. The result of the future and done callbacks are always called for the futures thread. - Callbacks are called in the reverse order in which they were added. + Callbacks are called in the reverse order in which they were added in the owning thread. """ - self._done_callbacks.append(fn) + if not self.done(): + self._done_callbacks.append(fn) + else: + self.get_caller().call_no_context(fn, self) def cancel(self) -> bool: - """Cancel the Future and schedule callbacks. + """Cancel the Future and schedule callbacks (thread-safe using Caller). Returns if it has been cancelled. """ @@ -168,10 +176,20 @@ def cancel(self) -> bool: return self.cancelled() def cancelled(self) -> bool: + """Return True if the Future is cancelled.""" return self._cancelled def exception(self) -> BaseException | None: - "Return the exception that was set on this Future." + """Return the exception that was set on the Future. + + If the Future has been cancelled, this method raises a [FutureCancelledError][async_kernel.caller.FutureCancelledError] exception. + + If the Future isn’t done yet, this method raises an [InvalidStateError][async_kernel.caller.InvalidStateError] exception. + """ + if self._cancelled: + raise FutureCancelledError + if not self.done(): + raise InvalidStateError return self._exception def remove_done_callback(self, fn: Callable[[Self], object], /) -> int: @@ -191,6 +209,10 @@ def set_cancel_scope(self, scope: anyio.CancelScope) -> None: scope.cancel() self._cancel_scope = scope + def get_caller(self) -> Caller: + "The the Caller the Future's thread corresponds." + return Caller(self.thread) + class Caller: """A class to enable calling functions and coroutines between anyio event loops. @@ -519,13 +541,16 @@ async def as_completed( max_concurrent: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] ) -> AsyncGenerator[Future[T], Any]: """An iterator to get Futures as they complete. - - Pass a generator should you wish to limit the number future jobs when calling to_thread/to_task etc. - Pass a set/list/tuple to ensure all get monitored at once. - Args: items: Either a container with existing futures or generator of Futures. - max_concurrent: The maximum number of concurrent futures to monitor at a time. This is useful when `items` is a generator utilising Caller.to_thread. By default this will limit to `Caller.MAX_IDLE_POOL_INSTANCES`. + max_concurrent: The maximum number of concurrent futures to monitor at a time. + This is useful when `items` is a generator utilising Caller.to_thread. + By default this will limit to `Caller.MAX_IDLE_POOL_INSTANCES`. + + + !!! Tip: + 1. Pass a generator should you wish to limit the number future jobs when calling to_thread/to_task etc. + 2. Pass a set/list/tuple to ensure all get monitored at once. """ event_future_ready = threading.Event() has_result: deque[Future[T]] = deque() @@ -584,5 +609,9 @@ async def iter_items(task_status: TaskStatus[None]): @classmethod def all_callers(cls, active_only=True) -> list[Caller]: - "Get a list of the callers." + """A classmethod to get a list of the callers. + + Args: + active_only: Restrict the list to callers that are active (running in an async context). + """ return [caller for caller in Caller._instances.values() if caller.active or not active_only] diff --git a/tests/test_caller.py b/tests/test_caller.py index ef62f8da3..50635ffd1 100644 --- a/tests/test_caller.py +++ b/tests/test_caller.py @@ -3,6 +3,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + import contextlib import inspect import threading @@ -18,7 +19,7 @@ import zmq from anyio.abc import TaskStatus -from async_kernel.caller import Caller, Future, FutureCancelledError +from async_kernel.caller import Caller, Future, FutureCancelledError, InvalidStateError @pytest.fixture(scope="module", params=["asyncio", "trio"]) @@ -37,10 +38,13 @@ async def test_set_and_wait_result(self): fut = Future[int]() assert inspect.isawaitable(fut) done_called = False + after_done = anyio.Event() def callback(obj): nonlocal done_called assert obj is fut + if done_called: + after_done.set() done_called = True fut.add_done_callback(callback) @@ -48,6 +52,11 @@ def callback(obj): result = await fut assert result == 42 assert done_called + async with Caller(create=True): + fut.add_done_callback(callback) + await after_done.wait() + + async def test_set_and_wait_exception(self): fut = Future() @@ -83,7 +92,8 @@ async def test_set_exception_twice_raises(self): async def test_set_result_after_exception_raises(self): fut = Future() - assert fut.exception() is None + with pytest.raises(InvalidStateError): + fut.exception() fut.set_exception(ValueError()) assert isinstance(fut.exception(), ValueError) with pytest.raises(RuntimeError): @@ -95,9 +105,12 @@ async def test_set_exception_after_result_raises(self): with pytest.raises(RuntimeError): fut.set_exception(ValueError()) - def test_cancel(self): + async def test_cancel(self): fut = Future() assert fut.cancel() + with pytest.raises(FutureCancelledError): + fut.exception() + def test_error_from_non_thread(self): fut = Future(thread=threading.Thread()) @@ -421,4 +434,5 @@ async def async_func(): await anyio.sleep(0) tg.cancel_scope.cancel() await anyio.sleep(0) - assert isinstance(fut.exception(), FutureCancelledError) # pyright: ignore[reportPossiblyUnboundVariable] + with pytest.raises(FutureCancelledError): + fut.exception() # pyright: ignore[reportPossiblyUnboundVariable] diff --git a/tests/test_message_spec.py b/tests/test_message_spec.py index 2fad622cf..92a50b782 100644 --- a/tests/test_message_spec.py +++ b/tests/test_message_spec.py @@ -257,6 +257,7 @@ async def test_stream(client): @pytest.mark.parametrize("clear", [True, False]) async def test_display_data(client, clear: bool): # kernel.display_formatter + await utils.clear_iopub(client) msg_id, _ = await utils.execute( client, f"from IPython.display import display; display(1, clear={clear})", clear_pub=False ) From 4ab3c247aac5529959eb5c5062988440540218ed Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 17 Aug 2025 20:17:49 +1000 Subject: [PATCH 07/18] Add Caller methods: - has_execution_queue - queue_call - queue_close Replace Kernel._shell_execute_request_queue with queue_call. --- src/async_kernel/asyncshell.py | 4 +- src/async_kernel/caller.py | 116 +++++++++++++++++++++++++-------- src/async_kernel/kernel.py | 36 ++++------ src/async_kernel/typing.py | 26 ++++++-- tests/test_caller.py | 41 +++++++++--- tests/test_kernel.py | 14 ++-- 6 files changed, 163 insertions(+), 74 deletions(-) diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index fb825db3d..e24ad2068 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -151,8 +151,8 @@ def execute_request_timeout(self) -> float | None: """A timeout in context of the [run_cell_async][async_kernel.Kernel.AsyncInteractiveShell]. See also: + - [async_kernel.typing.MetadataKeys.timeout][]. - - """ return self._execute_request_timeout.get() @@ -227,7 +227,7 @@ async def run_cell_async( preprocessing_exc_tuple: tuple | None = None, cell_id: str | None = None, ) -> ExecutionResult: - """Run a complete IPython cell asynchronously. + """Run a complete IPython cell asynchronously. This function runs [execute requests][async_kernel.Kernel.execute_request] for the kernel wrapping [InteractiveShell][IPython.core.interactiveshell.InteractiveShell.run_cell_async]. diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index decbfc62c..0bbe34665 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -20,7 +20,7 @@ from typing_extensions import override from zmq import Context, Socket, SocketType -from async_kernel.typing import NoValue, T +from async_kernel.typing import NoValue, PosArgsT, T from async_kernel.utils import wait_thread_event if TYPE_CHECKING: @@ -135,9 +135,7 @@ def set_value(): try: Caller(self.thread).call_no_context(func=set_value) except RuntimeError: - msg = ( - f"The current thread is not {self.thread.name} and a `Caller` does not exist for that thread either." - ) + msg = f"The current thread is not {self.thread.name} and a `Caller` does not exist for that thread either." raise RuntimeError(msg) from None else: set_value() @@ -150,7 +148,7 @@ def done(self) -> bool: def add_done_callback(self, fn: Callable[[Self], object]) -> None: """Add a callback for when the callback is done (not thread-safe). - + If the Future is already done it will be scheduled for calling. The result of the future and done callbacks are always called for the futures thread. @@ -181,10 +179,10 @@ def cancelled(self) -> bool: def exception(self) -> BaseException | None: """Return the exception that was set on the Future. - + If the Future has been cancelled, this method raises a [FutureCancelledError][async_kernel.caller.FutureCancelledError] exception. - If the Future isn’t done yet, this method raises an [InvalidStateError][async_kernel.caller.InvalidStateError] exception. + If the Future isn't done yet, this method raises an [InvalidStateError][async_kernel.caller.InvalidStateError] exception. """ if self._cancelled: raise FutureCancelledError @@ -236,7 +234,9 @@ class Caller: _outstanding = 0 _to_thread_pool: ClassVar[deque[Self]] = deque() _pool_instances: ClassVar[weakref.WeakSet[Self]] = weakref.WeakSet() + _executor_queue: dict MAX_IDLE_POOL_INSTANCES = 10 + MAX_BUFFER_SIZE = 1000 _taskgroup: TaskGroup | None = None _jobs: deque[tuple[contextvars.Context, tuple[Future, float, float, Callable, tuple, dict]] | Callable[[], Any]] _jobs_added: threading.Event @@ -265,7 +265,9 @@ def __new__( inst._jobs = deque() inst._jobs_added = threading.Event() inst._protected = protected + inst._executor_queue = {} cls._instances[thread] = inst + return inst @override @@ -287,12 +289,11 @@ async def __aexit__(self, exc_type, exc_value, exc_tb) -> None: await self.__stack.__aexit__(exc_type, exc_value, exc_tb) async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> None: - thread = threading.current_thread() socket = Context.instance().socket(SocketType.PUB) socket.linger = 500 socket.connect(self.iopub_url) try: - self.iopub_sockets[thread] = socket + self.iopub_sockets[self.thread] = socket task_status.started() while not self._stopped: while len(self._jobs): @@ -313,8 +314,8 @@ async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> No if not callable(job): job[1][0].set_exception(FutureCancelledError()) socket.close() - self.iopub_sockets.pop(thread, None) - self.taskgroup.cancel_scope.cancel() + self.iopub_sockets.pop(self.thread, None) + tg.cancel_scope.cancel() async def _wrap_call( self, @@ -359,12 +360,15 @@ def _to_thread_on_done(self, _) -> None: else: self.stop() + def _check_in_thread(self): + if self.thread is not threading.current_thread(): + msg = "This function must be called from its own thread. Tip: Use `call_no_context` to call this method from another thread." + raise RuntimeError(msg) + @property - def taskgroup(self) -> TaskGroup: - if tg := self._taskgroup: - return tg - msg = f"{self} is not currently open in an async context." - raise RuntimeError(msg) + def protected(self) -> bool: + "Returns `True` when the instance is protected from stopping." + return self._protected @property def stopped(self) -> bool: @@ -426,10 +430,70 @@ def call_no_context(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwa self._jobs.append(functools.partial(func, *args, **kwargs)) self._jobs_added.set() - @property - def protected(self) -> bool: - "Returns `True` when the instance is protected from stopping." - return self._protected + def has_execution_queue(self, func: Callable) -> bool: + "Returns True if an execution queue exists for `func`." + return func in self._executor_queue + + async def queue_call( + self, + func: Callable[[*PosArgsT], Awaitable[Any]], + *args: *PosArgsT, + max_buffer_size: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] + ) -> None: + """Queues the execution of func with the given arguments (not thread-safe). + + The args are added to a queue associated with the provided `func`. If queue does not already exist for + func, a new queue is created with a specified maximum buffer size. The arguments are then sent to the queue, + and an `execute_loop` coroutine is started to consume the queue and execute the function with the received + arguments. Exceptions during execution are caught and logged. + + Args: + func: The asynchronous function to execute. + *args: The arguments to pass to the function. + max_buffer_size: The maximum buffer size for the queue. If NoValue, defaults to [async_kernel.Caller.MAX_BUFFER_SIZE]. + + For usage see [handle_message_request][async_kernel.Kernel.handle_message_request]. + """ + self._check_in_thread() + if not self.has_execution_queue(func): + max_buffer_size = self.MAX_BUFFER_SIZE if max_buffer_size is NoValue else max_buffer_size + sender, queue = anyio.create_memory_object_stream[tuple[*PosArgsT]](max_buffer_size=max_buffer_size) + + async def execute_loop(): + try: + with contextlib.suppress(anyio.get_cancelled_exc_class()): + async with queue as receive_stream: + async for args in receive_stream: + try: + await func(*args) + except Exception as e: + self.log.exception("Execution %f failed", func, exc_info=e) + finally: + self._executor_queue.pop(execute_loop, None) + + self._executor_queue[func] = {"queue": sender, "future": self.call_soon(execute_loop)} + await self._executor_queue[func]["queue"].send(args) + + async def queue_close(self, func: Callable, *, force=False) -> bool: + """Close the execution queue associated with func (not thread-safe). + + Args: + func: The queue of the function to close. + force: Shutdown without waiting pending tasks to complete. + + Returns: + True if a queue was closed. + """ + self._check_in_thread() + if queue_map := self._executor_queue.pop(func, None): + if force: + queue_map["future"].cancel() + else: + await queue_map["queue"].aclose() + with contextlib.suppress(FutureCancelledError): + await queue_map["future"] + return True + return False @classmethod def stop_all(cls, *, _stop_protected=False) -> None: @@ -478,10 +542,6 @@ def to_thread_by_name( Returns: A future that can be awaited for the result of func. - - - - """ caller = ( cls._to_thread_pool.popleft() @@ -541,13 +601,13 @@ async def as_completed( max_concurrent: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] ) -> AsyncGenerator[Future[T], Any]: """An iterator to get Futures as they complete. + Args: items: Either a container with existing futures or generator of Futures. - max_concurrent: The maximum number of concurrent futures to monitor at a time. - This is useful when `items` is a generator utilising Caller.to_thread. + max_concurrent: The maximum number of concurrent futures to monitor at a time. + This is useful when `items` is a generator utilising Caller.to_thread. By default this will limit to `Caller.MAX_IDLE_POOL_INSTANCES`. - !!! Tip: 1. Pass a generator should you wish to limit the number future jobs when calling to_thread/to_task etc. 2. Pass a set/list/tuple to ensure all get monitored at once. @@ -610,7 +670,7 @@ async def iter_items(task_status: TaskStatus[None]): @classmethod def all_callers(cls, active_only=True) -> list[Caller]: """A classmethod to get a list of the callers. - + Args: active_only: Restrict the list to callers that are active (running in an async context). """ diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index ab890a8f4..11f0e5ae3 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -199,6 +199,7 @@ class Kernel(ConnectionFileMixin): ["tcp", "ipc"] if sys.platform == "linux" else ["tcp"], default_value="tcp", config=True ) cell_execute_timeout = Float(None, allow_none=True) + "A default timeout to apply to use for non-silent [execute requests][async_kernel.Kernel.execute_request]." def __new__(cls, **kwargs) -> Self: # noqa: ARG004 # There is only one instance. @@ -329,15 +330,15 @@ async def start_in_context(self) -> AsyncGenerator[Self, Any]: if self.connection_file and Path(self.connection_file).exists(): self.load_connection_file() try: - async with Caller(log=self.log, create=True, protected=True) as tc: - tg = tc.taskgroup + async with Caller(log=self.log, create=True, protected=True) as caller: + tg = caller._taskgroup # pyright: ignore[reportPrivateUsage] + assert tg await tg.start(self._wait_stopped) try: await tg.start(self._start_heartbeat) await tg.start(self._start_stdin) await tg.start(self._start_iopub_proxy) await tg.start(self._start_control_loop) - await tg.start(self._shell_execute_request_queue) await tg.start(self._receive_msg_loop, SocketID.shell) assert len(self._sockets) == len(SocketID) if not self.connection_file: @@ -450,7 +451,8 @@ def flusher(string: str, name=name): async def _start_control_loop(self, task_status: TaskStatus[None]) -> None: async def run_in_control_event_loop(): - await caller.taskgroup.start(self._receive_msg_loop, SocketID.control) + assert caller._taskgroup # pyright: ignore[reportPrivateUsage] + await caller._taskgroup.start(self._receive_msg_loop, SocketID.control) # pyright: ignore[reportPrivateUsage] ready_event.set() self.control_thread_caller = caller = Caller.start_new( @@ -496,7 +498,7 @@ async def _receive_msg_loop( # Reset the frame to show the main thread is not blocked. self._last_interrupt_frame = None self.log.debug("*** _receive_msg_loop %s*** %s", socket_id, msg) - await self.map_message_to_handler( + await self.handle_message_request( Job( socket_id=socket_id, socket=socket, @@ -634,32 +636,18 @@ def _input_request(self, prompt: str, *, password=False) -> Any: raise KernelInterruptError return self.session.recv(socket)[1]["content"]["value"] # pyright: ignore[reportOptionalSubscript] - async def _shell_execute_request_queue(self, *, task_status: TaskStatus[None]) -> None: - self._execute_request_queue, queue = anyio.create_memory_object_stream[Job](max_buffer_size=1000) - with contextlib.suppress(self.CancelledError): - async with queue as receive_stream: - task_status.started() - async for job in receive_stream: - await self.execute_request(job) - - async def map_message_to_handler(self, job: Job) -> None: - """Maps the Job wrapped message to the handler based on the message type. - - [Execute requests][async_kernel.types.MsgType.execute_request] + async def handle_message_request(self, job: Job) -> None: + """The main handler for all shell and control messages. Args: - job: A dictionary containing the message to be processed, including its - type, socket ID, and content. - - Returns: - None + job: The packed [message][async_kernel.typing.Message] for handling. """ match job["msg"]["header"]["msg_type"]: case MsgType.execute_request: if self.get_execute_mode(job) is ExecuteMode.queue: - await self._execute_request_queue.send(job) + await Caller().queue_call(self.execute_request, job) else: - Caller().taskgroup.start_soon(self.execute_request, job) + Caller().call_soon(self.execute_request, job) case _ as msg_type: await self._run_handler(self.message_handlers[job["socket_id"]].get(msg_type), job) diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index b4b812ce4..6ae594074 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -4,7 +4,7 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING, Any, Final, Generic, Literal, ParamSpec, TypedDict, TypeVar +from typing import TYPE_CHECKING, Any, Final, Generic, Literal, ParamSpec, TypedDict, TypeVar, TypeVarTuple from typing_extensions import Sentinel @@ -13,7 +13,17 @@ import zmq -__all__ = ["DebugMessage", "ExecuteMode", "Job", "Message", "MetadataKeys", "MsgHeader", "MsgType", "SocketID", "Tags"] +__all__ = [ + "DebugMessage", + "ExecuteMode", + "Job", + "Message", + "MetadataKeys", + "MsgHeader", + "MsgType", + "SocketID", + "Tags", +] NoValue = Sentinel("NoValue") @@ -21,10 +31,12 @@ T = TypeVar("T") D = TypeVar("D", bound=dict) P = ParamSpec("P") +PosArgsT = TypeVarTuple("PosArgsT") class SocketID(enum.StrEnum): "Mapping of `Kernel.port_` for sockets. [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introduction)." + heartbeat = "hb" "" shell = "shell" @@ -40,6 +52,7 @@ class SocketID(enum.StrEnum): EXECUTE_MODE_PREFIX: Final = "##" "The Prefix used for [ExecuteMode][async_kernel.typing.ExecuteMode] identifiers." + class ExecuteMode(enum.StrEnum): "An Enum of the Execute modes available for altering how [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) are handled." @@ -53,9 +66,9 @@ class ExecuteMode(enum.StrEnum): class MsgType(enum.StrEnum): """An enumeration of Message `msg_type` for [shell and control messages]( https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel). - - - + + + [Control channel](https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-control-router-dealer-channel) only """ @@ -120,6 +133,7 @@ class Tags(enum.StrEnum): class MsgHeader(TypedDict): "" + # https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header msg_id: str session: str @@ -131,6 +145,7 @@ class MsgHeader(TypedDict): class Message(TypedDict, Generic[T]): "A [message](https://jupyter-client.readthedocs.io/en/stable/messaging.html#general-message-format)." + header: MsgHeader "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header)" parent_header: MsgHeader @@ -165,6 +180,7 @@ class Job(TypedDict, Generic[T]): class ExecuteContent(TypedDict): "[Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute). see also: [Message][async_kernel.typing.Message]" + code: str "The code to execute." silent: bool diff --git a/tests/test_caller.py b/tests/test_caller.py index 50635ffd1..32922d12e 100644 --- a/tests/test_caller.py +++ b/tests/test_caller.py @@ -3,7 +3,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - import contextlib import inspect import threading @@ -56,8 +55,6 @@ def callback(obj): fut.add_done_callback(callback) await after_done.wait() - - async def test_set_and_wait_exception(self): fut = Future() done_called = False @@ -111,7 +108,6 @@ async def test_cancel(self): with pytest.raises(FutureCancelledError): fut.exception() - def test_error_from_non_thread(self): fut = Future(thread=threading.Thread()) with pytest.raises(RuntimeError): @@ -170,7 +166,7 @@ async def my_func(is_called: anyio.Event, *args, **kwargs): assert (await fut) == args_kwargs async def test_anyio_to_thread(self): - # Test the call works from another thread + # Test the call works from an anyio thread async with Caller(create=True) as caller: assert caller.active assert caller in Caller.all_callers() @@ -334,10 +330,39 @@ async def cancelled(task_status: TaskStatus[None]): with pytest.raises(FutureCancelledError): await item - async def test_call_early(self, anyio_backend): + async def test__check_in_thread(self, anyio_backend): + Caller.to_thread(anyio.sleep, 0.1) + worker = next(iter(Caller.all_callers())) + assert not worker.protected + with pytest.raises(RuntimeError): + worker._check_in_thread() # pyright: ignore[reportPrivateUsage] + + async def test_execution_queue(self, anyio_backend): + results = [] + + async def my_func(a, b, c): + await anyio.sleep(0.01) + assert c == a + b + results.append(c) + + async with Caller(create=True) as caller: + assert not caller.has_execution_queue(my_func) + assert not await caller.queue_close(my_func) + for i in range(4): + force = bool(i % 2) + results.clear() + await caller.queue_call(my_func, 0, 0, 0, max_buffer_size=i) + assert caller.has_execution_queue(my_func) + for j in range(1, 20): + await caller.queue_call(my_func, 0, j, j) + assert await caller.queue_close(my_func, force=force) + if force: + assert len(results) < 20, "Should exit early" + else: + assert results == list(range(20)), "Should empty queue prior" + + async def test_call_early(self, anyio_backend) -> None: caller = Caller(create=True) - with pytest.raises(RuntimeError, match=".*not currently open in an async context"): - caller.taskgroup # noqa: B018 fut = caller.call_soon(time.sleep, 0.1) await anyio.sleep(0.1) assert not fut.done() diff --git a/tests/test_kernel.py b/tests/test_kernel.py index e9836405b..f0de6bf9d 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -62,6 +62,13 @@ def pubio_subscribe(): ctx.term() +async def test_execute_request_success(client): + reply = await utils.send_shell_message(client, "execute_request", {"code": "1 + 1", "silent": False}) + assert reply["header"]["msg_type"] == "execute_reply" + assert reply["content"]["status"] == "ok" + await utils.clear_iopub(client) + + @pytest.mark.parametrize("quiet", [True, False]) async def test_simple_print(kernel, client, quiet: bool): """simple print statement in kernel""" @@ -213,13 +220,6 @@ async def test_message_order(client): await utils.clear_iopub(client) -async def test_execute_request_success(client): - reply = await utils.send_shell_message(client, "execute_request", {"code": "1 + 1", "silent": False}) - assert reply["header"]["msg_type"] == "execute_reply" - assert reply["content"]["status"] == "ok" - await utils.clear_iopub(client) - - async def test_execute_request_error_tag_ignore_error(client): metadata = {"tags": [Tags.suppress_error]} _, content = await utils.execute(client, "ignore error", metadata=metadata) From 2411012d5411c6cf62c2fc2a253624452fa7c442 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Sun, 17 Aug 2025 21:34:00 +1000 Subject: [PATCH 08/18] Switch from ExecuteMode to RunMode. Move the parameter from ExecuteContent to Job to make it generic. It could easily be moved elsewhere such as header of metadata... In Kernel change get_execute_mode to get_run_mode and make it possble for run mode to be dynamic. --- src/async_kernel/asyncshell.py | 2 +- src/async_kernel/kernel.py | 112 +++++++++++++++---------- src/async_kernel/typing.py | 46 ++++++---- tests/test_kernel.py | 149 +++++++++++++++++---------------- tests/test_kernelspec.py | 11 +-- tests/utils.py | 13 ++- 6 files changed, 185 insertions(+), 148 deletions(-) diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index e24ad2068..cf77b5732 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -151,7 +151,7 @@ def execute_request_timeout(self) -> float | None: """A timeout in context of the [run_cell_async][async_kernel.Kernel.AsyncInteractiveShell]. See also: - + - [async_kernel.typing.MetadataKeys.timeout][]. """ return self._execute_request_timeout.get() diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 11f0e5ae3..b19e8af79 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -9,6 +9,7 @@ import contextlib import contextvars import errno +import functools import getpass import logging import os @@ -42,11 +43,22 @@ from async_kernel.debugger import Debugger from async_kernel.iostream import OutStream from async_kernel.kernelspec import Backend, KernelName -from async_kernel.typing import ExecuteContent, ExecuteMode, Job, MetadataKeys, MsgType, NoValue, SocketID, Tags +from async_kernel.typing import ( + CODE_MODE_MAPPINGS, + ExecuteContent, + HandlerType, + Job, + MetadataKeys, + MsgType, + NoValue, + RunMode, + SocketID, + Tags, +) if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Generator - from types import CoroutineType, FrameType + from types import FrameType from anyio.abc import TaskStatus from IPython.core.interactiveshell import ExecutionResult @@ -57,7 +69,7 @@ __all__ = ["Kernel", "KernelInterruptError"] -def error_to_dict(error: BaseException): +def error_to_dict(error: BaseException) -> dict[str, str | list[str]]: """Convert the error to a dict. ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply @@ -130,6 +142,13 @@ def _try_bind_socket(port: int): msg = f"Failed to bind {socket} for {transport=}" + (f" to {port=}!" if port else "!") raise RuntimeError(msg) from e +@functools.cache +def wrap_handler(run_handler:Callable[[HandlerType, Job]], handler:HandlerType) -> HandlerType: + + async def queued_handler(job: Job) -> None: + await run_handler(handler, job) + return queued_handler + class KernelInterruptError(InterruptedError): "Raised to interrupt the kernel." @@ -183,9 +202,7 @@ class Kernel(ConnectionFileMixin): _stop_on_error_time: float = 0 _interrupts: Container[set[Callable[[], object]]] = Set() _sockets: Dict[SocketID, zmq.Socket] = Dict() - message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, Callable[[Job], CoroutineType]]] = ( - Dict() - ) + message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, HandlerType]] = Dict() _execution_count = Int(0) anyio_backend = UseEnum(Backend) help_links = Tuple() @@ -212,6 +229,7 @@ def __init__(self, **kwargs) -> None: return # Only initialize once super().__init__(**kwargs) self.message_handlers[SocketID.shell] = { + MsgType.execute_request: self.execute_request, MsgType.kernel_info_request: self.kernel_info_request, MsgType.comm_info_request: self.comm_info_request, MsgType.interrupt_request: self.interrupt_request, @@ -474,7 +492,7 @@ async def _wait_stopped(self, task_status: TaskStatus[None]) -> None: async def _receive_msg_loop( self, socket_id: Literal[SocketID.control, SocketID.shell], *, task_status: TaskStatus[None] ) -> None: - """Receive shell and control messages over the socket and call [`process_job`][async_kernel.Kernel.process_job].""" + """Receive shell and control messages over zmq sockets.""" if ( sys.platform == "win32" and sniffio.current_async_library() == "asyncio" @@ -515,11 +533,8 @@ async def _receive_msg_loop( except (zmq.ContextTerminated, self.CancelledError): return - async def _run_handler(self, handler: Callable[[Job], CoroutineType] | None, job: Job) -> None: + async def _run_handler(self, handler: HandlerType, job: Job) -> None: self._job_var.set(job) - if not handler: - self.log.error("Unknown message type: %r", job["msg"]["header"]) - return try: self._publish_status("busy", job) await handler(job) @@ -608,9 +623,8 @@ def _send_reply(self, job: Job, content: dict | None = None) -> None: ident=job["ident"], ) if msg: - self.log.debug( - "send_reply: '%s' msg_id: %s %s", msg["msg_type"], job["msg"]["header"]["msg_id"], msg["content"] - ) + self.log.debug("*** _send_reply %s*** %s", job['socket_id'], msg) + def _input_request(self, prompt: str, *, password=False) -> Any: job = self.job @@ -636,36 +650,46 @@ def _input_request(self, prompt: str, *, password=False) -> Any: raise KernelInterruptError return self.session.recv(socket)[1]["content"]["value"] # pyright: ignore[reportOptionalSubscript] - async def handle_message_request(self, job: Job) -> None: + async def handle_message_request(self, job: Job, /) -> None: """The main handler for all shell and control messages. Args: job: The packed [message][async_kernel.typing.Message] for handling. """ - match job["msg"]["header"]["msg_type"]: - case MsgType.execute_request: - if self.get_execute_mode(job) is ExecuteMode.queue: - await Caller().queue_call(self.execute_request, job) - else: - Caller().call_soon(self.execute_request, job) - case _ as msg_type: - await self._run_handler(self.message_handlers[job["socket_id"]].get(msg_type), job) + msg_type: MsgType = job["msg"]["header"]["msg_type"] + if not (handler := self.message_handlers[job["socket_id"]].get(msg_type)): + self.log.error("Unknown message type: %r", job["msg"]["header"]) + return + func = wrap_handler(self._run_handler, handler) + match self.get_run_mode(job): + case RunMode.queue: + await Caller().queue_call(func, job) + case RunMode.thread: + Caller.to_thread(func, job) + case RunMode.task: + Caller().call_soon(func, job) + case RunMode.wait: + await func(job) + + @staticmethod - def get_execute_mode(job: Job[ExecuteContent]) -> ExecuteMode: - """Extract `ExecuteMode` from the job.""" - if m := job["msg"]["content"].get("execute_mode"): + def get_run_mode(job: Job[Any]) -> RunMode: + """Determine `RunMode` from the job.""" + if m := job.get("run_mode"): # Respect an existing mode - return ExecuteMode(m) - if (c := job["msg"]["content"]["code"].strip().split("\n")[0].strip()) in tuple(ExecuteMode): - mode = ExecuteMode(c) - else: - mode = ExecuteMode.queue - if ( - job["msg"]["content"].get("silent", True) or (job["socket_id"] is SocketID.control) - ) and mode is ExecuteMode.queue: - mode = ExecuteMode.task - job["msg"]["content"]["execute_mode"] = mode + return RunMode(m) + msg_type = job["msg"]["header"]["msg_type"] + mode = RunMode.wait + content = job["msg"].get("content") + if msg_type == MsgType.execute_request: + if mode_ := CODE_MODE_MAPPINGS.get(content.get("code", "").strip().split("\n")[0].strip()): + mode = mode_ + elif content.get("silent", True) or (job["socket_id"] is SocketID.control): + mode = RunMode.task + else: + mode = RunMode.queue + job["run_mode"] = mode return mode def topic(self, topic) -> bytes: @@ -689,19 +713,15 @@ async def comm_info_request(self, job: Job) -> None: async def execute_request(self, job: Job[ExecuteContent]) -> None: """Process the execute request.""" - if (job["received_time"] < self._stop_on_error_time) and not job["msg"]["content"]["silent"]: + content = job["msg"]["content"] + if (job["received_time"] < self._stop_on_error_time) and not content.get("silent", False): self.log.info("Aborting execute_request: %s", job) + c_ = error_to_dict(RuntimeError("Aborting due to prior exception")) + c_ |= {"execution_count": self.execution_count} self._publish_status("busy", job) - content: dict[str, str | list[str]] = error_to_dict(RuntimeError("Aborting due to prior exception")) - content["execution_count"] = self.execution_count # pyright: ignore[reportArgumentType] - self._send_reply(job, content) + self._send_reply(job, content=c_) self._publish_status("idle", job) return - await self._run_handler(self._execute_request_handler, job) - - async def _execute_request_handler(self, job: Job[ExecuteContent]) -> None: - """Perform the actual execute_request.""" - content = job["msg"]["content"] metadata = job["msg"].get("metadata") or {} if not (silent := content["silent"]): self._execution_count += 1 @@ -713,7 +733,7 @@ async def _execute_request_handler(self, job: Job[ExecuteContent]) -> None: parent=job["msg"], ident=self.topic("execute_input"), ) - fut = (Caller.to_thread if self.get_execute_mode(job) is ExecuteMode.thread else Caller().call_soon)( + fut = (Caller.to_thread if self.get_run_mode(job) is RunMode.thread else Caller().call_soon)( self.shell.run_cell_async, raw_cell=content["code"], store_history=content.get("store_history", False), diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 6ae594074..ed6ed77dd 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -4,7 +4,9 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING, Any, Final, Generic, Literal, ParamSpec, TypedDict, TypeVar, TypeVarTuple +from collections.abc import Callable +from types import CoroutineType +from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NotRequired, ParamSpec, TypedDict, TypeVar, TypeVarTuple from typing_extensions import Sentinel @@ -14,13 +16,15 @@ import zmq __all__ = [ + "CODE_MODE_MAPPINGS", + "RUN_MODE_PREFIX", "DebugMessage", - "ExecuteMode", "Job", "Message", "MetadataKeys", "MsgHeader", "MsgType", + "RunMode", "SocketID", "Tags", ] @@ -49,19 +53,25 @@ class SocketID(enum.StrEnum): "" -EXECUTE_MODE_PREFIX: Final = "##" -"The Prefix used for [ExecuteMode][async_kernel.typing.ExecuteMode] identifiers." +RUN_MODE_PREFIX: Final = "##" # "The Prefix used for [RunMode][async_kernel.typing.RunMode] identifiers." -class ExecuteMode(enum.StrEnum): - "An Enum of the Execute modes available for altering how [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) are handled." +class RunMode(enum.StrEnum): + "An Enum of the Run modes available for altering how jobs are(https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) are handled." - queue = f"{EXECUTE_MODE_PREFIX}queue" - "Add to the execute_request queue (default)." - task = f"{EXECUTE_MODE_PREFIX}task" + queue = "queue" + "Add to the execute_request queue." + task = "task" "Execute as a task in the MainThread." - thread = f"{EXECUTE_MODE_PREFIX}thread" + thread = "thread" "Execute in a caller worker thread." + wait = "wait" + """Wait for the message to execute. + + This blocks the message loop""" + + +CODE_MODE_MAPPINGS: Final[dict[str, RunMode]] = {f"{RUN_MODE_PREFIX}{mode}": mode for mode in RunMode} class MsgType(enum.StrEnum): @@ -132,15 +142,20 @@ class Tags(enum.StrEnum): class MsgHeader(TypedDict): - "" + "A [message header](https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header)." - # https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header msg_id: str + "" session: str + "" username: str + "" date: str + "" msg_type: MsgType + "" version: str + "" class Message(TypedDict, Generic[T]): @@ -176,6 +191,8 @@ class Job(TypedDict, Generic[T]): "" received_time: float "The time the message was received." + run_mode: NotRequired[RunMode] + """The run mode.""" class ExecuteContent(TypedDict): @@ -184,7 +201,7 @@ class ExecuteContent(TypedDict): code: str "The code to execute." silent: bool - "Modifies how code is executed. See also [get_execute_mode][async_kernel.kernel.get_execute_mode]." + "Modifies how code is executed. See also [get_run_mode][async_kernel.kernel.get_run_mode]." store_history: bool "See ref." user_expressions: dict[str, str] @@ -193,8 +210,7 @@ class ExecuteContent(TypedDict): "See ref." stop_on_error: bool "See ref." - execute_mode: ExecuteMode - """The execute mode. See also [get_execute_mode][async_kernel.kernel.get_execute_mode].""" DebugMessage = dict[str, Any] +HandlerType = Callable[[Job], CoroutineType] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index f0de6bf9d..1ebc8150d 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -9,7 +9,7 @@ import pathlib import threading import time -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, Any, Literal, cast import anyio import pytest @@ -18,7 +18,17 @@ import async_kernel.utils from async_kernel.caller import Caller from async_kernel.comm import Comm -from async_kernel.typing import EXECUTE_MODE_PREFIX, ExecuteContent, ExecuteMode, Job, SocketID, Tags +from async_kernel.typing import ( + RUN_MODE_PREFIX, + ExecuteContent, + Job, + Message, + MsgHeader, + MsgType, + RunMode, + SocketID, + Tags, +) from tests import utils if TYPE_CHECKING: @@ -63,7 +73,7 @@ def pubio_subscribe(): async def test_execute_request_success(client): - reply = await utils.send_shell_message(client, "execute_request", {"code": "1 + 1", "silent": False}) + reply = await utils.send_shell_message(client, MsgType.execute_request, {"code": "1 + 1", "silent": False}) assert reply["header"]["msg_type"] == "execute_reply" assert reply["content"]["status"] == "ok" await utils.clear_iopub(client) @@ -107,8 +117,8 @@ async def test_execute_kernel_timeout(client, kernel: Kernel, mode: str): async def test_bad_message(client): - client.shell_channel.socket.send(b"") - client.control_channel.socket.send(b"") + await utils.send_shell_message(client, "bad_message", reply=False) # pyright: ignore[reportArgumentType] + await utils.send_control_message(client, "bad_message", reply=False) # pyright: ignore[reportArgumentType] await utils.execute(client, "") @@ -140,7 +150,7 @@ async def test_input( assert content["prompt"] == theprompt # interrupt if test_mode == "interrupt": - await utils.send_control_message(client, "interrupt_request") + await utils.send_control_message(client, MsgType.interrupt_request) reply = await utils.get_reply(client, msg_id, clear_pub=False) assert reply["content"]["status"] == "error" return @@ -228,7 +238,9 @@ async def test_execute_request_error_tag_ignore_error(client): async def test_execute_request_error(client): - reply = await utils.send_shell_message(client, "execute_request", {"code": "some invalid code", "silent": False}) + reply = await utils.send_shell_message( + client, MsgType.execute_request, {"code": "some invalid code", "silent": False} + ) assert reply["header"]["msg_type"] == "execute_reply" assert reply["content"]["status"] == "error" await utils.clear_iopub(client) @@ -236,19 +248,21 @@ async def test_execute_request_error(client): async def test_execute_request_stop_on_error(client, kernel): kernel._stop_on_error_time = time.monotonic() + 10 - reply = await utils.send_shell_message(client, "execute_request", {"code": "some invalid code", "silent": False}) + reply = await utils.send_shell_message( + client, MsgType.execute_request, {"code": "some invalid code", "silent": False} + ) assert reply["header"]["msg_type"] == "execute_reply" assert reply["content"]["status"] == "error" kernel._stop_on_error_time = 0 async def test_complete_request(client): - reply = await utils.send_shell_message(client, "complete_request", {"code": "hello", "cursor_pos": 0}) + reply = await utils.send_shell_message(client, MsgType.complete_request, {"code": "hello", "cursor_pos": 0}) assert reply["header"]["msg_type"] == "complete_reply" async def test_inspect_request(client): - reply = await utils.send_shell_message(client, "inspect_request", {"code": "hello", "cursor_pos": 0}) + reply = await utils.send_shell_message(client, MsgType.inspect_request, {"code": "hello", "cursor_pos": 0}) assert reply["header"]["msg_type"] == "inspect_reply" @@ -257,24 +271,26 @@ async def test_history_request(client, kernel): # assert kernel.shell.history_manager # kernel.shell.history_manager.db = DummyDB() - reply = await utils.send_shell_message(client, "history_request", {"hist_access_type": "", "output": "", "raw": ""}) + reply = await utils.send_shell_message( + client, MsgType.history_request, {"hist_access_type": "", "output": "", "raw": ""} + ) assert reply["header"]["msg_type"] == "history_reply" reply = await utils.send_shell_message( - client, "history_request", {"hist_access_type": "tail", "output": "", "raw": ""} + client, MsgType.history_request, {"hist_access_type": "tail", "output": "", "raw": ""} ) assert reply["header"]["msg_type"] == "history_reply" reply = await utils.send_shell_message( - client, "history_request", {"hist_access_type": "range", "output": "", "raw": ""} + client, MsgType.history_request, {"hist_access_type": "range", "output": "", "raw": ""} ) assert reply["header"]["msg_type"] == "history_reply" reply = await utils.send_shell_message( - client, "history_request", {"hist_access_type": "search", "output": "", "raw": ""} + client, MsgType.history_request, {"hist_access_type": "search", "output": "", "raw": ""} ) assert reply["header"]["msg_type"] == "history_reply" async def test_comm_info_request(client): - reply = await utils.send_shell_message(client, "comm_info_request") + reply = await utils.send_shell_message(client, MsgType.comm_info_request) assert reply["header"]["msg_type"] == "comm_info_reply" @@ -289,22 +305,22 @@ def cb(comm_, _): # open a comm with anyio.move_on_after(0.1): await utils.send_shell_message( - client, "comm_open", {"content": {}, "comm_id": "comm id", "target_name": "my target"} + client, MsgType.comm_open, {"content": {}, "comm_id": "comm id", "target_name": "my target"} ) assert isinstance(comm, Comm) comm = cast("Comm", comm) - reply = await utils.send_shell_message(client, "comm_info_request") + reply = await utils.send_shell_message(client, MsgType.comm_info_request) assert reply["header"]["msg_type"] == "comm_info_reply" assert reply["content"]["comms"].get("comm id") == {"target_name": "my target"} msg_received = mocker.patch.object(comm, "handle_msg") with anyio.move_on_after(0.1): - await utils.send_shell_message(client, "comm_msg", {"comm_id": comm.comm_id}) + await utils.send_shell_message(client, MsgType.comm_msg, {"comm_id": comm.comm_id}) assert msg_received.call_count == 1 # close comm closed = mocker.patch.object(comm, "handle_close") with anyio.move_on_after(0.1): - await utils.send_shell_message(client, "comm_close", {"comm_id": comm.comm_id}) + await utils.send_shell_message(client, MsgType.comm_close, {"comm_id": comm.comm_id}) assert closed.call_count == 1 kernel.comm_manager.unregister_target("my target", cb) @@ -312,7 +328,7 @@ def cb(comm_, _): async def test_interrupt_request(client, kernel): event = threading.Event() kernel._interrupts.add(event.set) - reply = await utils.send_control_message(client, "interrupt_request") + reply = await utils.send_control_message(client, MsgType.interrupt_request) assert reply["header"]["msg_type"] == "interrupt_reply" assert reply["content"] == {"status": "ok"} assert event.is_set() @@ -322,7 +338,7 @@ async def test_interrupt_request_async_request(subprocess_kernels_client): client = subprocess_kernels_client msg_id = client.execute("await anyio.sleep(100)") await anyio.sleep(0.1) - reply = await utils.send_control_message(client, "interrupt_request") + reply = await utils.send_control_message(client, MsgType.interrupt_request) reply = await utils.get_reply(client, msg_id) assert reply["content"]["status"] == "error" @@ -331,39 +347,26 @@ async def test_interrupt_request_blocking_exec_request(subprocess_kernels_client client = subprocess_kernels_client msg_id = client.execute("import time;time.sleep(100)") await anyio.sleep(0.1) - reply = await utils.send_control_message(client, "interrupt_request") + reply = await utils.send_control_message(client, MsgType.interrupt_request) reply = await utils.get_reply(client, msg_id) assert reply["content"]["status"] == "error" assert reply["content"]["ename"] == "FutureCancelledError" async def test_interrupt_request_blocking_task(subprocess_kernels_client): - code = """ -async def test(): - import time - from async_kernel.kernel import KernelInterruptError - started.set() - await anyio.sleep(0.01) - try: - time.sleep(100) - except KernelInterruptError: - print("KernelInterruptError") - print("Failed") -import anyio -started = anyio.Event() -from async_kernel import Caller -Caller().call_soon(test) -await started.wait() -""" + code = f""" + {RUN_MODE_PREFIX}{RunMode.task} + time.sleep(100) + """ client = subprocess_kernels_client - _, reply = await utils.execute(client, code) - assert reply["status"] == "ok" - await anyio.sleep(0.011) + msg_id = client.execute(code, reply=False) + await utils.check_pub_message(client, msg_id, execution_state="busy") + await utils.check_pub_message(client, msg_id, msg_type="execute_input") for _ in range(2): # Blocking calls in tasks need to be interrupted twice - await utils.send_control_message(client, "interrupt_request", clear_pub=False) - stdout, _ = await utils.assemble_output(client, timeout=1) - assert "KernelInterruptError" in stdout - await utils.clear_iopub(client) + await utils.send_control_message(client, MsgType.interrupt_request) + reply = await utils.get_reply(client, msg_id) + assert reply["content"]["status"] == "error" + assert reply["content"]["ename"] == "FutureCancelledError" @pytest.mark.parametrize("response", ["y", ""]) @@ -377,7 +380,7 @@ async def test_user_exit(client, kernel, mocker, response: Literal["y", ""]): async def test_is_complete_request(client): - reply = await utils.send_shell_message(client, "is_complete_request", {"code": "hello"}) + reply = await utils.send_shell_message(client, MsgType.is_complete_request, {"code": "hello"}) assert reply["header"]["msg_type"] == "is_complete_reply" @@ -389,14 +392,14 @@ async def test_debug_static(client, command: str, mocker): mocker.patch.object(async_kernel.utils, "LAUNCHED_BY_DEBUGPY", new=True) assert async_kernel.utils.LAUNCHED_BY_DEBUGPY reply = await utils.send_control_message( - client, "debug_request", {"type": "request", "seq": 1, "command": command, "arguments": {"code": code}} + client, MsgType.debug_request, {"type": "request", "seq": 1, "command": command, "arguments": {"code": code}} ) assert reply["content"]["status"] == "ok" if command == "dumpCell": path = reply["content"]["body"]["sourcePath"] reply = await utils.send_control_message( client, - "debug_request", + MsgType.debug_request, {"type": "request", "seq": 1, "command": "source", "arguments": {"source": {"path": path}}}, ) assert reply["content"]["status"] == "ok" @@ -410,7 +413,7 @@ async def test_debug_raises_no_socket(kernel): async def test_debug_not_connected(client): reply = await utils.send_control_message( - client, "debug_request", {"type": "request", "seq": 1, "command": "disconnect", "arguments": {}} + client, MsgType.debug_request, {"type": "request", "seq": 1, "command": "disconnect", "arguments": {}} ) assert reply["content"]["status"] == "error" assert reply["content"]["evalue"] == "Debugy client not connected." @@ -421,7 +424,7 @@ async def test_debug_static_richInspectVariables(client, variable_name): # These are tests on the debugger that don't required the debugger to be connected. reply = await utils.send_control_message( client, - "debug_request", + MsgType.debug_request, { "type": "request", "seq": 1, @@ -470,10 +473,10 @@ async def test_shell_can_set_namespace(kernel): assert set(kernel.shell.user_ns) == {"Out", "_oh", "In", "exit", "_dh", "open", "get_ipython", "_ih", "quit"} -@pytest.mark.parametrize("mode", ExecuteMode) -async def test_header_mode(client, mode: ExecuteMode): +@pytest.mark.parametrize("mode", RunMode) +async def test_header_mode(client, mode: RunMode): code = f""" -{mode} +{RUN_MODE_PREFIX}{mode} import time time.sleep(0.1) print("{mode.name}") @@ -514,27 +517,25 @@ async def test_invalid_message(client, channel): @pytest.mark.parametrize( ("code", "silent", "socket_id", "expected"), [ - (f"{ExecuteMode.task}", False, SocketID.shell, ExecuteMode.task), - (f" {ExecuteMode.task}", False, SocketID.shell, ExecuteMode.task), - ("print(1)", False, SocketID.shell, ExecuteMode.queue), - ("", True, SocketID.shell, ExecuteMode.task), - (f"{ExecuteMode.thread}\nprint('hello')", False, SocketID.shell, ExecuteMode.thread), - ("", False, SocketID.control, ExecuteMode.task), - (f"{EXECUTE_MODE_PREFIX}threads", False, SocketID.shell, ExecuteMode.queue), - (f"{EXECUTE_MODE_PREFIX}Task", False, SocketID.shell, ExecuteMode.queue), + (f"{RUN_MODE_PREFIX}{RunMode.task}", False, SocketID.shell, RunMode.task), + (f" {RUN_MODE_PREFIX}{RunMode.task}", False, SocketID.shell, RunMode.task), + ("print(1)", False, SocketID.shell, RunMode.queue), + ("", True, SocketID.shell, RunMode.task), + (f"{RUN_MODE_PREFIX}{RunMode.thread}\nprint('hello')", False, SocketID.shell, RunMode.thread), + ("", False, SocketID.control, RunMode.task), + (f"{RUN_MODE_PREFIX}threads", False, SocketID.shell, RunMode.queue), + (f"{RUN_MODE_PREFIX}Task", False, SocketID.shell, RunMode.queue), ], ) -def test_get_execute_mode(code: str, silent: bool, socket_id, expected: ExecuteMode): +def test_get_run_mode_execute_request(code: str, silent: bool, socket_id, expected: RunMode): content = ExecuteContent( - code=code, - silent=silent, - store_history=True, - user_expressions={}, - allow_stdin=False, - stop_on_error=True, - execute_mode=None, + code=code, silent=silent, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True ) - job = Job(msg={"content": content}, socket_id=socket_id) # pyright: ignore[reportCallIssue] - execute_mode = async_kernel.Kernel.get_execute_mode(job) - assert execute_mode is expected - assert job["msg"]["content"]["execute_mode"] is expected + header = MsgHeader(msg_id="", session="", username="", date="", msg_type=MsgType.execute_request, version="1") + msg = Message(header=header, parent_header=header, metadata={}, buffers=[], content=content) + socket = cast("zmq.Socket[Any]", None) # pyright: ignore[reportInvalidCast] + job = Job(msg=msg, socket_id=socket_id, ident=[b""], socket=socket, received_time=0.0) + mode = async_kernel.Kernel.get_run_mode(job) + assert mode is expected + assert job.get("run_mode") is expected + diff --git a/tests/test_kernelspec.py b/tests/test_kernelspec.py index ebe28a9d6..0c83e56f4 100644 --- a/tests/test_kernelspec.py +++ b/tests/test_kernelspec.py @@ -2,22 +2,17 @@ # Distributed under the terms of the Modified BSD License. import json -import pathlib import shutil import pytest from jupyter_client.kernelspec import KernelSpec -from async_kernel.kernelspec import RESOURCES, KernelName, write_kernel_spec +from async_kernel.kernelspec import KernelName, write_kernel_spec @pytest.mark.parametrize("kernel_name", list(KernelName)) -def test_write_kernel_spec(kernel_name: KernelName): - path = write_kernel_spec(kernel_name=kernel_name) - if RESOURCES.exists(): - for fname in RESOURCES.iterdir(): - dst = path.joinpath(fname) - assert pathlib.Path(dst).exists() +def test_write_kernel_spec(kernel_name: KernelName, tmp_path): + path = write_kernel_spec(tmp_path, kernel_name=kernel_name) kernel_json = path.joinpath("kernel.json") assert kernel_json.exists() with kernel_json.open("r") as f: diff --git a/tests/utils.py b/tests/utils.py index 268388cde..7c1917dbf 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,7 +13,7 @@ import async_kernel.utils from async_kernel import Caller, Kernel from async_kernel.asyncshell import AsyncInteractiveShell -from async_kernel.typing import ExecuteContent +from async_kernel.typing import ExecuteContent, MsgType from tests.references import RMessage, references if TYPE_CHECKING: @@ -102,7 +102,6 @@ async def execute(client: AsyncKernelClient, /, code="", clear_pub=True, metadat user_expressions={}, allow_stdin=False, stop_on_error=True, - execute_mode=None, ) | kwargs, ) @@ -157,17 +156,23 @@ async def clear_iopub(client, *, timeout=0.01): await assemble_output(client, timeout=timeout) -async def send_shell_message(client: AsyncKernelClient, msg_type: str, content: Mapping[str, Any] | None = None): +async def send_shell_message( + client: AsyncKernelClient, msg_type: MsgType, content: Mapping[str, Any] | None = None, reply=True +): msg = client.session.msg(msg_type, content=dict(content) if content is not None else None) client.shell_channel.send(msg) + if not reply: + return {} return await get_reply(client, msg["header"]["msg_id"], channel="shell") async def send_control_message( - client: AsyncKernelClient, msg_type: str, content: Mapping[str, Any] | None = None, clear_pub=True + client: AsyncKernelClient, msg_type: MsgType, content: Mapping[str, Any] | None = None, clear_pub=True, reply=True ): msg = client.session.msg(msg_type, content=dict(content) if content is not None else None) client.control_channel.send(msg) + if not reply: + return {} return await get_reply(client, msg["header"]["msg_id"], channel="control", clear_pub=clear_pub) From 562557c40dc8738383d301f7647badd94735ca4b Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 18 Aug 2025 10:12:04 +1000 Subject: [PATCH 09/18] Add RunMode to the message handlers and rename get_run_mode to get_handler_and_run_mode. Job.run_mode is now required. async def do_complete(self, code, cursor_pos) -> dict[str, Any]: do_history, do_inspect, do_is_complete, are all merged into the actual request handlers. --- .../{execute_mode.ipynb => run_mode.ipynb} | 0 mkdocs.yml | 2 +- src/async_kernel/asyncshell.py | 11 +- src/async_kernel/kernel.py | 322 +++++++++--------- src/async_kernel/typing.py | 16 +- tests/test_kernel.py | 8 +- 6 files changed, 184 insertions(+), 175 deletions(-) rename docs/notebooks/{execute_mode.ipynb => run_mode.ipynb} (100%) diff --git a/docs/notebooks/execute_mode.ipynb b/docs/notebooks/run_mode.ipynb similarity index 100% rename from docs/notebooks/execute_mode.ipynb rename to docs/notebooks/run_mode.ipynb diff --git a/mkdocs.yml b/mkdocs.yml index 5e8217c2b..34a44d765 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -155,7 +155,7 @@ nav: - Notebooks: - Overview: notebooks/overview.md - Simple example: notebooks/simple_example.ipynb - - Execute mode: notebooks/execute_mode.ipynb + - Execute mode: notebooks/run_mode.ipynb - Caller notebook: notebooks/caller.ipynb - Command line: command_line.md - Reference: diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index cf77b5732..e45ec8d67 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -74,7 +74,6 @@ def finish_displayhook(self) -> None: class AsyncDisplayPublisher(DisplayPublisher): """A display publisher that publishes data using a ZeroMQ PUB socket.""" - kernel: Instance[Kernel] = Instance("async_kernel.Kernel", ()) topic: ClassVar = b"display_data" @override @@ -98,7 +97,7 @@ def publish( # pyright: ignore[reportIncompatibleMethodOverride] [Reference](https://jupyter-client.readthedocs.io/en/stable/messaging.html#update-display-data) """ - self.kernel.iopub_send( + async_kernel.Kernel().iopub_send( msg_or_type="update_display_data" if update else "display_data", content={"data": data, "metadata": metadata or {}, "transient": transient or {}} | kwargs, ident=self.topic, @@ -113,7 +112,7 @@ def clear_output(self, wait=False) -> None: instead waiting for the next display before clearing. This reduces bounce during repeated clear & display loops. """ - self.kernel.iopub_send(msg_or_type="clear_output", content={"wait": wait}, ident=self.topic) + async_kernel.Kernel().iopub_send(msg_or_type="clear_output", content={"wait": wait}, ident=self.topic) class AsyncInteractiveShell(InteractiveShell): @@ -123,7 +122,6 @@ class AsyncInteractiveShell(InteractiveShell): display_pub_class = Type(AsyncDisplayPublisher) displayhook: Instance[AsyncDisplayHook] display_pub: Instance[AsyncDisplayPublisher] - kernel: Instance[Kernel] = Instance("async_kernel.Kernel", ()) compiler_class = Type(XCachingCompiler) compile: Instance[XCachingCompiler] user_ns_hidden = Dict() @@ -146,6 +144,11 @@ def _default_banner1(self) -> str: # will print a warning in the absence of readline. autoindent = CBool(False) + @property + def kernel(self) -> Kernel: + "The current kernel." + return async_kernel.Kernel() + @property def execute_request_timeout(self) -> float | None: """A timeout in context of the [run_cell_async][async_kernel.Kernel.AsyncInteractiveShell]. diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index b19e8af79..2ffb046d7 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -69,7 +69,7 @@ __all__ = ["Kernel", "KernelInterruptError"] -def error_to_dict(error: BaseException) -> dict[str, str | list[str]]: +def error_to_dict(error: BaseException, /) -> dict[str, str | list[str]]: """Convert the error to a dict. ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply @@ -142,11 +142,18 @@ def _try_bind_socket(port: int): msg = f"Failed to bind {socket} for {transport=}" + (f" to {port=}!" if port else "!") raise RuntimeError(msg) from e + @functools.cache -def wrap_handler(run_handler:Callable[[HandlerType, Job]], handler:HandlerType) -> HandlerType: +def wrap_handler(run_handler: Callable[[HandlerType, Job]], handler: HandlerType) -> HandlerType: + """Wraps the handler with run_handler (cached). + + This is used by [get_handler_and_run_mode][async_kernel.Kernel.get_handler_and_run_mode] to wrap + the calling of message handlers ([message types][async_kernel.typing.MsgType]) with the [run handler][async_kernel.Kernel._run_handler]. + """ async def queued_handler(job: Job) -> None: await run_handler(handler, job) + return queued_handler @@ -202,7 +209,6 @@ class Kernel(ConnectionFileMixin): _stop_on_error_time: float = 0 _interrupts: Container[set[Callable[[], object]]] = Set() _sockets: Dict[SocketID, zmq.Socket] = Dict() - message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, HandlerType]] = Dict() _execution_count = Int(0) anyio_backend = UseEnum(Backend) help_links = Tuple() @@ -215,6 +221,10 @@ class Kernel(ConnectionFileMixin): transport: CaselessStrEnum[str] = CaselessStrEnum( ["tcp", "ipc"] if sys.platform == "linux" else ["tcp"], default_value="tcp", config=True ) + message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, tuple[HandlerType, RunMode]]] = ( + Dict(read_only=True) + ) + "The message handlers for message requests (see also [async_kernel.Kernel.get_handler_and_run_mode][])." cell_execute_timeout = Float(None, allow_none=True) "A default timeout to apply to use for non-silent [execute requests][async_kernel.Kernel.execute_request]." @@ -229,21 +239,21 @@ def __init__(self, **kwargs) -> None: return # Only initialize once super().__init__(**kwargs) self.message_handlers[SocketID.shell] = { - MsgType.execute_request: self.execute_request, - MsgType.kernel_info_request: self.kernel_info_request, - MsgType.comm_info_request: self.comm_info_request, - MsgType.interrupt_request: self.interrupt_request, - MsgType.complete_request: self.complete_request, - MsgType.is_complete_request: self.is_complete_request, - MsgType.inspect_request: self.inspect_request, - MsgType.history_request: self.history_request, - MsgType.comm_open: self.comm_open, - MsgType.comm_msg: self.comm_msg, - MsgType.comm_close: self.comm_close, + MsgType.kernel_info_request: (self.kernel_info_request, RunMode.wait), + MsgType.comm_info_request: (self.comm_info_request, RunMode.wait), + MsgType.execute_request: (self.execute_request, RunMode.queue), + MsgType.interrupt_request: (self.interrupt_request, RunMode.wait), + MsgType.complete_request: (self.complete_request, RunMode.thread), + MsgType.is_complete_request: (self.is_complete_request, RunMode.thread), + MsgType.inspect_request: (self.inspect_request, RunMode.thread), + MsgType.history_request: (self.history_request, RunMode.thread), + MsgType.comm_open: (self.comm_open, RunMode.wait), + MsgType.comm_msg: (self.comm_msg, RunMode.task), + MsgType.comm_close: (self.comm_close, RunMode.wait), } self.message_handlers[SocketID.control] = self.message_handlers[SocketID.shell] | { - MsgType.shutdown_request: self.control_shutdown_request, - MsgType.debug_request: self.debug_request, + MsgType.shutdown_request: (self.shutdown_request, RunMode.wait), + MsgType.debug_request: (self.debug_request, RunMode.task), } sys.excepthook = self.excepthook sys.unraisablehook = self.unraisablehook @@ -332,7 +342,11 @@ def _default_shell(self) -> AsyncInteractiveShell: @classmethod def stop(cls) -> None: - """Stop the kernel.""" + """Stop the kernel. + + Once a kernel is stopped; that instance of the kernel cannot be restarted. + Instead, a new kernel must be started. + """ if instance := cls._instance: cls._instance = None instance._stop_event.set() @@ -523,6 +537,7 @@ async def _receive_msg_loop( ident=ident, msg=msg, # pyright: ignore[reportArgumentType] received_time=time.monotonic(), + run_mode=None, # pyright: ignore[reportArgumentType]. This value is set by `get_handler_and_run_mode`. ) ) except Exception as e: @@ -536,13 +551,13 @@ async def _receive_msg_loop( async def _run_handler(self, handler: HandlerType, job: Job) -> None: self._job_var.set(job) try: - self._publish_status("busy", job) + self._publish_status(job, "busy") await handler(job) except Exception as e: - self._send_reply(job, content=error_to_dict(e)) + self._send_reply(job, error_to_dict(e)) self.log.exception("Exception in message handler:", exc_info=e) finally: - self._publish_status("idle", job) + self._publish_status(job, "idle") @contextlib.contextmanager def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket) -> Generator[None, Any, None]: @@ -601,7 +616,7 @@ def iopub_send( buffers=buffers, ) - def _publish_status(self, status: Literal["busy", "idle"], job: Job) -> None: + def _publish_status(self, job: Job, status: Literal["busy", "idle"], /) -> None: """send status (busy/idle) on IOPub""" self.iopub_send( msg_or_type="status", @@ -610,7 +625,7 @@ def _publish_status(self, status: Literal["busy", "idle"], job: Job) -> None: ident=self.topic("status"), ) - def _send_reply(self, job: Job, content: dict | None = None) -> None: + def _send_reply(self, job: Job, content: dict | None = None, /) -> None: """Send a reply to the job with the specified content.""" content = content or {} if "status" not in content: @@ -623,8 +638,7 @@ def _send_reply(self, job: Job, content: dict | None = None) -> None: ident=job["ident"], ) if msg: - self.log.debug("*** _send_reply %s*** %s", job['socket_id'], msg) - + self.log.debug("*** _send_reply %s*** %s", job["socket_id"], msg) def _input_request(self, prompt: str, *, password=False) -> Any: job = self.job @@ -650,39 +664,57 @@ def _input_request(self, prompt: str, *, password=False) -> Any: raise KernelInterruptError return self.session.recv(socket)[1]["content"]["value"] # pyright: ignore[reportOptionalSubscript] + def topic(self, topic) -> bytes: + """prefixed topic for IOPub messages""" + return (f"kernel.{topic}").encode() + async def handle_message_request(self, job: Job, /) -> None: """The main handler for all shell and control messages. Args: job: The packed [message][async_kernel.typing.Message] for handling. """ - msg_type: MsgType = job["msg"]["header"]["msg_type"] - if not (handler := self.message_handlers[job["socket_id"]].get(msg_type)): - self.log.error("Unknown message type: %r", job["msg"]["header"]) + try: + handler, mode = await self.get_handler_and_run_mode(job) + except ValueError: return - func = wrap_handler(self._run_handler, handler) - match self.get_run_mode(job): + match mode: case RunMode.queue: - await Caller().queue_call(func, job) + await Caller().queue_call(handler, job) case RunMode.thread: - Caller.to_thread(func, job) + Caller.to_thread(handler, job) case RunMode.task: - Caller().call_soon(func, job) + Caller().call_soon(handler, job) case RunMode.wait: - await func(job) + await handler(job) + + async def get_handler_and_run_mode(self, job: Job) -> tuple[HandlerType, RunMode]: + """Determine the appropriate handler and run mode for a given job. + This method retrieves the handler associated with the message type of the job, + and determines the run mode based on metadata, header information, and content + of the message. It also sets the 'run_mode' attribute of the job. + Args: + job: The job dictionary containing message details and socket ID. + + Returns: + A tuple containing the wrapped handler function and the determined run mode. - @staticmethod - def get_run_mode(job: Job[Any]) -> RunMode: - """Determine `RunMode` from the job.""" - if m := job.get("run_mode"): - # Respect an existing mode - return RunMode(m) - msg_type = job["msg"]["header"]["msg_type"] - mode = RunMode.wait - content = job["msg"].get("content") - if msg_type == MsgType.execute_request: + Raises: + ValueError: If a handler does not exist for the message type. + """ + msg_type: MsgType = job["msg"]["header"]["msg_type"] + if not (handler_mode := self.message_handlers[job["socket_id"]].get(msg_type)): + msg = f"A handler does not exist for {msg_type=}!" + raise ValueError(msg) + handler, mode = handler_mode + if mode_from_metadata := job["msg"]["metadata"].get("run_mode"): + mode = mode_from_metadata + if mode_from_header := job["msg"]["header"].get("run_mode"): + mode = mode_from_header + elif job["msg"]["header"]["msg_type"] == MsgType.execute_request: + content = job["msg"].get("content", {}) if mode_ := CODE_MODE_MAPPINGS.get(content.get("code", "").strip().split("\n")[0].strip()): mode = mode_ elif content.get("silent", True) or (job["socket_id"] is SocketID.control): @@ -690,11 +722,7 @@ def get_run_mode(job: Job[Any]) -> RunMode: else: mode = RunMode.queue job["run_mode"] = mode - return mode - - def topic(self, topic) -> bytes: - """prefixed topic for IOPub messages""" - return (f"kernel.{topic}").encode() + return wrap_handler(self._run_handler, handler), mode async def kernel_info_request(self, job: Job) -> None: """Handle a kernel info request.""" @@ -702,8 +730,8 @@ async def kernel_info_request(self, job: Job) -> None: async def comm_info_request(self, job: Job) -> None: """Handle a comm info request.""" - content = job["msg"]["content"] - target_name = content.get("target_name", None) + c = job["msg"]["content"] + target_name = c.get("target_name", None) comms = { k: {"target_name": v.target_name} for (k, v) in tuple(self.comm_manager.comms.items()) @@ -713,32 +741,32 @@ async def comm_info_request(self, job: Job) -> None: async def execute_request(self, job: Job[ExecuteContent]) -> None: """Process the execute request.""" - content = job["msg"]["content"] - if (job["received_time"] < self._stop_on_error_time) and not content.get("silent", False): + c = job["msg"]["content"] + if (job["received_time"] < self._stop_on_error_time) and not c.get("silent", False): self.log.info("Aborting execute_request: %s", job) c_ = error_to_dict(RuntimeError("Aborting due to prior exception")) c_ |= {"execution_count": self.execution_count} - self._publish_status("busy", job) - self._send_reply(job, content=c_) - self._publish_status("idle", job) + self._publish_status(job, "busy") + self._send_reply(job, c_) + self._publish_status(job, "idle") return metadata = job["msg"].get("metadata") or {} - if not (silent := content["silent"]): + if not (silent := c["silent"]): self._execution_count += 1 self._execution_count_var.set(self._execution_count) self.shell.execute_request_timeout = metadata.get(MetadataKeys.timeout) or self.cell_execute_timeout self.iopub_send( msg_or_type="execute_input", - content={"code": content["code"], "execution_count": self.execution_count}, + content={"code": c["code"], "execution_count": self.execution_count}, parent=job["msg"], ident=self.topic("execute_input"), ) - fut = (Caller.to_thread if self.get_run_mode(job) is RunMode.thread else Caller().call_soon)( + fut = (Caller.to_thread if job["run_mode"] is RunMode.thread else Caller().call_soon)( self.shell.run_cell_async, - raw_cell=content["code"], - store_history=content.get("store_history", False), + raw_cell=c["code"], + store_history=c.get("store_history", False), silent=silent, - transformed_cell=self.shell.transform_cell(content["code"]), + transformed_cell=self.shell.transform_cell(c["code"]), shell_futures=True, cell_id=metadata.get("cellId"), ) @@ -755,80 +783,23 @@ async def execute_request(self, job: Job[ExecuteContent]) -> None: # 1. tag # 2. timeout err = None - reply_content = { + err_content = { "status": "error" if err else "ok", "execution_count": self.execution_count, - "user_expressions": self.shell.user_expressions(content.get("user_expressions", {})), + "user_expressions": self.shell.user_expressions(c.get("user_expressions", {})), } if err: - reply_content |= error_to_dict(error=err) - if not silent and content.get("stop_on_error"): + err_content |= error_to_dict(err) + if not silent and c.get("stop_on_error"): self._stop_on_error_time = time.monotonic() self.log.info("An error occurred in a non-silent execution request") - self._send_reply(job, content=reply_content) - - async def interrupt_request(self, job: Job) -> None: - """Handle an interrupt request.""" - self._interrupt_requested = True - if sys.platform == "win32": - signal.raise_signal(signal.SIGINT) - time.sleep(0) - else: - os.kill(os.getpid(), signal.SIGINT) - for interrupter in tuple(self._interrupts): - interrupter() - self._send_reply(job) - - async def complete_request(self, job: Job) -> None: - """Handle a completion request.""" - parent = job["msg"] - matches = await self.do_complete(parent["content"]["code"], parent["content"]["cursor_pos"]) - self._send_reply(job, matches) + self._send_reply(job, err_content) - async def is_complete_request(self, job: Job) -> None: - """Handle an is_complete request.""" - reply_content = await self.do_is_complete(job["msg"]["content"]["code"]) - self._send_reply(job, reply_content) - - async def inspect_request(self, job: Job) -> None: - """Handle an inspect request.""" - content = job["msg"]["content"] - reply_content = await self.do_inspect( - content["code"], - content["cursor_pos"], - int(content.get("detail_level", 0)), - set(content.get("omit_sections", [])), - ) - self._send_reply(job, reply_content) - - async def history_request(self, job: Job) -> None: - """Handle a history request.""" - reply_content = await self.do_history(**job["msg"]["content"]) - self._send_reply(job, reply_content) - - async def comm_open(self, job: Job) -> None: - self.comm_manager.comm_open(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - - async def comm_msg(self, job: Job) -> None: - self.comm_manager.comm_msg(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - - async def comm_close(self, job: Job) -> None: - self.comm_manager.comm_close(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - - async def control_shutdown_request(self, job: Job) -> None: - """Handle a shutdown request.""" - await self.debugger.disconnect() - self._send_reply(job, {"status": "ok", "restart": job["msg"]["content"].get("restart", False)}) - self.stop() - - async def debug_request(self, job: Job) -> None: - """Handle a debug request.""" - content = await self.debugger.process_request(job["msg"]["content"]) - self._send_reply(job=job, content=content) - - async def do_complete(self, code, cursor_pos) -> dict[str, Any]: - """Completions from IPython, using Jedi.""" - cursor_pos = cursor_pos if cursor_pos is not None else len(code) + async def complete_request(self, job: Job[dict[str, Any]]) -> None: + """Handle a [completion request][async_kernel.typing.complete_request].""" + c = job["msg"]["content"] + code: str = c["code"] + cursor_pos = c.get("cursor_pos") or len(code) with _provisionalcompleter(): completions = list(_rectify_completions(code, self.shell.Completer.completions(code, cursor_pos))) comps = [ @@ -843,24 +814,28 @@ async def do_complete(self, code, cursor_pos) -> dict[str, Any]: ] s, e = completions[0].start, completions[0].end if completions else (cursor_pos, cursor_pos) matches = [c.text for c in completions] - return { + matches = { "matches": matches, "cursor_end": e, "cursor_start": s, "metadata": {"_jupyter_types_experimental": comps}, } + self._send_reply(job, matches) - async def do_is_complete(self, code) -> dict[str, Any]: - """Handle an is_complete request.""" - status, indent_spaces = self.shell.input_transformer_manager.check_complete(code) - r = {"status": status} + async def is_complete_request(self, job: Job) -> None: + """Handle an [is_complete request][async_kernel.typing.is_complete_request].""" + status, indent_spaces = self.shell.input_transformer_manager.check_complete(job["msg"]["content"]["code"]) + reply_content = {"status": status} if status == "incomplete": - r["indent"] = " " * indent_spaces - return r + reply_content["indent"] = " " * indent_spaces + self._send_reply(job, reply_content) - async def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=()) -> dict[str, Any]: - """Handle code inspection.""" - name = token_at_cursor(code, cursor_pos) + async def inspect_request(self, job: Job[dict[str, Any]]) -> None: + """Handle an [inspect request][async_kernel.typing.inspect_request].""" + c = job["msg"]["content"] + detail_level = int(c.get("detail_level", 0)) + omit_sections = set(c.get("omit_sections", [])) + name = token_at_cursor(c["code"], c["cursor_pos"]) reply_content: dict[str, Any] = {"status": "ok"} reply_content["data"] = {} reply_content["metadata"] = {} @@ -872,32 +847,61 @@ async def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=()) - reply_content["found"] = True except KeyError: reply_content["found"] = False - return reply_content + self._send_reply(job, reply_content) - async def do_history( - self, - hist_access_type, - output, - raw, - session=0, - start=0, - stop=None, - n=None, - pattern=None, - unique=False, - ) -> dict[str, list[Any]]: - """Handle code history.""" + async def history_request(self, job: Job[dict[str, Any]]) -> None: + """Handle a [history request][async_kernel.typing.history_request].""" + c = job["msg"]["content"] history_manager = self.shell.history_manager assert history_manager - if hist_access_type == "tail": - hist = history_manager.get_tail(n, raw=raw, output=output, include_latest=True) - elif hist_access_type == "range": - hist = history_manager.get_range(session, start, stop, raw=raw, output=output) - elif hist_access_type == "search": - hist = history_manager.search(pattern, raw=raw, output=output, n=n, unique=unique) + if c.get("hist_access_type") == "tail": + hist = history_manager.get_tail(c["n"], raw=c.get("raw"), output=c.get("output"), include_latest=True) + elif c.get("hist_access_type") == "range": + hist = history_manager.get_range( + c.get("session"), c.get("start"), c.get("stop"), raw=c.get("raw"), output=c.get("output") + ) + elif c.get("hist_access_type") == "search": + hist = history_manager.search( + c.get("pattern"), raw=c.get("raw"), output=c.get("output"), n=c.get("n"), unique=c.get("unique") + ) else: hist = [] - return {"history": list(hist)} + self._send_reply(job, {"history": list(hist)}) + + async def comm_open(self, job: Job) -> None: + """Handle a [comm open request][async_kernel.typing.comm_open].""" + self.comm_manager.comm_open(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] + + async def comm_msg(self, job: Job) -> None: + """Handle a [comm msg request][async_kernel.typing.comm_msg].""" + self.comm_manager.comm_msg(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] + + async def comm_close(self, job: Job) -> None: + """Handle a [comm close request][async_kernel.typing.comm_close].""" + self.comm_manager.comm_close(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] + + async def interrupt_request(self, job: Job) -> None: + """Handle a [interrupt request][async_kernel.typing.interrupt_request].""" + self._interrupt_requested = True + if sys.platform == "win32": + signal.raise_signal(signal.SIGINT) + time.sleep(0) + else: + os.kill(os.getpid(), signal.SIGINT) + for interrupter in tuple(self._interrupts): + interrupter() + self._send_reply(job) + + async def shutdown_request(self, job: Job) -> None: + """Handle a [shutdown request][async_kernel.typing.shutdown_request].""" + await self.debugger.disconnect() + self._send_reply(job, {"status": "ok", "restart": job["msg"]["content"].get("restart", False)}) + self.stop() + + async def debug_request(self, job: Job) -> None: + """Handle a [debug request][async_kernel.typing.debug_request].""" + content = await self.debugger.process_request(job["msg"]["content"]) + self._send_reply(job, content) def excepthook(self, etype, evalue, tb) -> None: """Handle an exception.""" diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index ed6ed77dd..030c3d4c0 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -6,7 +6,7 @@ import enum from collections.abc import Callable from types import CoroutineType -from typing import TYPE_CHECKING, Any, Final, Generic, Literal, NotRequired, ParamSpec, TypedDict, TypeVar, TypeVarTuple +from typing import TYPE_CHECKING, Any, Final, Generic, Literal, ParamSpec, TypedDict, TypeVar, TypeVarTuple from typing_extensions import Sentinel @@ -65,6 +65,8 @@ class RunMode(enum.StrEnum): "Execute as a task in the MainThread." thread = "thread" "Execute in a caller worker thread." + thread_ = "thread" + "Execute in a caller worker thread." wait = "wait" """Wait for the message to execute. @@ -191,7 +193,7 @@ class Job(TypedDict, Generic[T]): "" received_time: float "The time the message was received." - run_mode: NotRequired[RunMode] + run_mode: RunMode """The run mode.""" @@ -201,15 +203,15 @@ class ExecuteContent(TypedDict): code: str "The code to execute." silent: bool - "Modifies how code is executed. See also [get_run_mode][async_kernel.kernel.get_run_mode]." + "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." store_history: bool - "See ref." + "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." user_expressions: dict[str, str] - "See ref." + "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." allow_stdin: bool - "See ref." + "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." stop_on_error: bool - "See ref." + "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." DebugMessage = dict[str, Any] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 1ebc8150d..ed198639e 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -509,7 +509,7 @@ async def test_invalid_message(client, channel): f = utils.send_control_message if channel == "control" else utils.send_shell_message response = None with anyio.move_on_after(0.1): - response = await f(client, "test_invalid_message") + response = await f(client, "test_invalid_message") # pyright: ignore[reportArgumentType] assert response is None await utils.clear_iopub(client) @@ -527,15 +527,15 @@ async def test_invalid_message(client, channel): (f"{RUN_MODE_PREFIX}Task", False, SocketID.shell, RunMode.queue), ], ) -def test_get_run_mode_execute_request(code: str, silent: bool, socket_id, expected: RunMode): +async def test_get_run_mode_execute_request(kernel:Kernel, code: str, silent: bool, socket_id, expected: RunMode): content = ExecuteContent( code=code, silent=silent, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True ) header = MsgHeader(msg_id="", session="", username="", date="", msg_type=MsgType.execute_request, version="1") msg = Message(header=header, parent_header=header, metadata={}, buffers=[], content=content) socket = cast("zmq.Socket[Any]", None) # pyright: ignore[reportInvalidCast] - job = Job(msg=msg, socket_id=socket_id, ident=[b""], socket=socket, received_time=0.0) - mode = async_kernel.Kernel.get_run_mode(job) + job = Job(msg=msg, socket_id=socket_id, ident=[b""], socket=socket, received_time=0.0, run_mode=None) # pyright: ignore[reportArgumentType] + _, mode = await kernel.get_handler_and_run_mode(job) assert mode is expected assert job.get("run_mode") is expected From 4d75edd2101e3efe9b12862943a5eb1786c2ed80 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 18 Aug 2025 21:12:54 +1000 Subject: [PATCH 10/18] Improve RunMode Added Content (type) run_handler is now responsible for sending a reply. More docs. --- README.md | 8 +- docs/{command_line.md => commands.md} | 0 docs/notebooks/caller.ipynb | 202 +---------------- docs/notebooks/overview.md | 9 +- docs/notebooks/run_mode.ipynb | 159 +++++++++++-- docs/notebooks/simple_example.ipynb | 279 +++++++---------------- docs/reference/kernel.md | 12 - mkdocs.yml | 41 ++-- src/async_kernel/__init__.py | 1 + src/async_kernel/__main__.py | 2 +- src/async_kernel/_version.py | 2 +- src/async_kernel/asyncshell.py | 56 +++-- src/async_kernel/caller.py | 94 +++++--- src/async_kernel/kernel.py | 312 ++++++++++++++------------ src/async_kernel/typing.py | 112 +++++---- tests/test_caller.py | 10 +- tests/test_comm.py | 2 +- tests/test_kernel.py | 82 +++---- tests/test_typing.py | 26 +++ tests/utils.py | 15 +- 20 files changed, 676 insertions(+), 748 deletions(-) rename docs/{command_line.md => commands.md} (100%) create mode 100644 tests/test_typing.py diff --git a/README.md b/README.md index 1356a431b..092a6dd95 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,14 @@ Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages) that runs in an [anyio](https://pypi.org/project/anyio/) event loop. -Async kernel is designed to run [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) outside the shell message loop to prevent dead locks when waiting for a response via the shell message loop. ## Highlights - +- Asynchronous - Comms is not blocked during cell execution[^non-blocking-execution] -- Concurrent cell execution in tasks or cells [^run-concurrent] +- Concurrent code execution [^run-concurrent] - [Debugger client](https://jupyterlab.readthedocs.io/en/latest/user/debugger.html#debugger) - Configurable backend - "asyncio" (default) or "trio backend" [^config-backend] -- [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, etc +- [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, and history - No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets @@ -32,7 +31,6 @@ To add a kernel spec for `trio`[^config-backend]. ```shell pip install trio -async-kernel add async-trio ``` ```shell diff --git a/docs/command_line.md b/docs/commands.md similarity index 100% rename from docs/command_line.md rename to docs/commands.md diff --git a/docs/notebooks/caller.ipynb b/docs/notebooks/caller.ipynb index 81433c757..0d2815fb5 100644 --- a/docs/notebooks/caller.ipynb +++ b/docs/notebooks/caller.ipynb @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "4", "metadata": { "editable": true, @@ -93,181 +93,10 @@ "slide_type": "" }, "tags": [ - "do-not-publish-error" + "suppress-error" ] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4b616ff79a844d4c9594f843d063a5a2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "818ff86d5ad84ebc87ca924c47f165bd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4ed7cb4d4c0749e0b749ca12d41eb4a3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "59559c85fb434147bcf74d5ee5ed62bc", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "95b350f0fcdf4b169cb45c67ac81660d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4037458da04041c0946fc0b302c353bf", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a1fd3d61b01f496abf16cc74688d5ff4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "21625909a2f64ca48872f8ef2d918431", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Finished: 2\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e4bf1c1c649f42caa4f4de371f11d3a5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "86cea41153254bfd864976eef3adb4a5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value='', description='Caller', style=HTMLStyle(description_width='220px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Finished: 1055\r" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Exception in message handler:\n", - "Traceback (most recent call last):\n", - " File \"C:\\code\\ipykernel\\src\\async_kernel\\kernel.py\", line 531, in _run_handler\n", - " await handler(job)\n", - " File \"C:\\code\\ipykernel\\src\\async_kernel\\kernel.py\", line 710, in _execute_request_handler\n", - " result: ExecutionResult = await fut\n", - " ^^^^^^^^^\n", - " File \"C:\\code\\ipykernel\\src\\async_kernel\\caller.py\", line 95, in result\n", - " raise self._exception\n", - "async_kernel.caller.FutureCancelledError\n" - ] - } - ], + "outputs": [], "source": [ "import random\n", "import time\n", @@ -305,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "5", "metadata": { "editable": true, @@ -314,28 +143,7 @@ }, "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t← current thread\n", - " ✓\t 🔐\t\tControlThread\t\n", - " ✓\t\t\tThread-3 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-4 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-5 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-6 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-7 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-8 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-9 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-10 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-11 (anyio_run_caller)\t\n", - " ✓\t\t\tThread-12 (anyio_run_caller)\t\n" - ] - } - ], + "outputs": [], "source": [ "%callers" ] diff --git a/docs/notebooks/overview.md b/docs/notebooks/overview.md index 8e046c594..b7e1a2d16 100644 --- a/docs/notebooks/overview.md +++ b/docs/notebooks/overview.md @@ -4,12 +4,7 @@ Notebooks in this documentation show the result of each cell after executing for You can download the notebook with the button at the top right of the page for the notebook. -### Issues -#### Inter-notebook links -Unfortunately, links between notebooks in the documentation don't work: see issue [#157:](https://github.com/danielfrg/mkdocs-jupyter/issues/157). +!!! note -#### Widgets - -Unfortunately, widgets don't render correctly. They wouldn't be functional even if the did render, so no big deal. See issue:[#180](https://github.com/danielfrg/mkdocs-jupyter/issues/180). - \ No newline at end of file + [`suppress-error`][async_kernel.typing.Tags.suppress_error] error tags are used with generating documentation. diff --git a/docs/notebooks/run_mode.ipynb b/docs/notebooks/run_mode.ipynb index 46537539f..989807094 100644 --- a/docs/notebooks/run_mode.ipynb +++ b/docs/notebooks/run_mode.ipynb @@ -11,13 +11,67 @@ "tags": [] }, "source": [ - "# Execute mode\n", + "# Run mode\n", "\n", - "You can execute code concurrently by adding either `##thread` or `##task` at top of a code cell.\n", + "RunMode is an enumeration of run modes supported by the Kernel. \n", "\n", - "Executing multiple cells concurrently is possible if the frontend supports it. Jupyterlab does and VScode does not.\n", + "The kernel is configured for different run modes depending on the execute request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from async_kernel import Kernel\n", "\n", - "## Example\n", + "for ch in [\"shell\", \"control\"]:\n", + " print(f\"**{ch}**\")\n", + " print(\"-- Run mode ------ MsgType ---------\")\n", + " handlers = Kernel().message_handlers[ch]\n", + " for k in handlers:\n", + " print(f\"{handlers[k][1]} \\t----\", k)" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The user can also override the run mode for a cell in a number of ways:\n", + "\n", + "- Metadata\n", + "- [Directly in code](#code-directive)\n", + "- Message header (in custom messages)" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Code directive\n", + "### Code for example\n", "\n", "- **This example requires ipywidgets**\n", "- **Ensure you are running an async kernel**\n", @@ -28,7 +82,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1", + "id": "4", "metadata": { "editable": true, "slideshow": { @@ -44,7 +98,9 @@ " import anyio\n", " from ipywidgets import Button\n", "\n", - " from async_kernel import Caller\n", + " from async_kernel import Caller, Kernel\n", + "\n", + " print(\"Run mode:\", Kernel().job[\"run_mode\"])\n", "\n", " print(f\"Thread name: '{threading.current_thread().name}'\")\n", " button = Button(description=\"Finish\")\n", @@ -61,8 +117,14 @@ }, { "cell_type": "markdown", - "id": "2", - "metadata": {}, + "id": "5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "Lets run it normally (queue)" ] @@ -70,14 +132,14 @@ { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "6", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [ - "do-not-publish-error" + "suppress-error" ] }, "outputs": [], @@ -88,25 +150,26 @@ { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "7", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [ - "do-not-publish-error" + "suppress-error" ] }, "outputs": [], "source": [ + "##queue\n", "# Tip: try running this cell while the previous cell is still busy.\n", "await demo()" ] }, { "cell_type": "markdown", - "id": "5", + "id": "8", "metadata": { "editable": true, "slideshow": { @@ -115,7 +178,7 @@ "tags": [] }, "source": [ - "### Execute mode: task\n", + "### Run mode: task\n", "``` python\n", "##task\n", "...\n", @@ -129,14 +192,14 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "9", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [ - "do-not-publish-error" + "suppress-error" ] }, "outputs": [], @@ -147,7 +210,7 @@ }, { "cell_type": "markdown", - "id": "7", + "id": "10", "metadata": { "editable": true, "slideshow": { @@ -156,7 +219,7 @@ "tags": [] }, "source": [ - "### Execute mode: thread\n", + "### Run mode: thread\n", "``` python\n", "##thread\n", "...\n", @@ -166,14 +229,14 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "11", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [ - "do-not-publish-error" + "suppress-error" ] }, "outputs": [], @@ -185,7 +248,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "12", "metadata": { "editable": true, "slideshow": { @@ -198,6 +261,58 @@ "##thread\n", "%callers # magic provided by async kernel" ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "### Direct use symbol\n", + "\n", + "Using the `RunMode` vaules directly is also possible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from async_kernel.typing import RunMode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "suppress-error" + ] + }, + "outputs": [], + "source": [ + "RunMode.task\n", + "\n", + "await demo()" + ] } ], "metadata": { @@ -216,7 +331,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.11.13.final.0" } }, "nbformat": 4, diff --git a/docs/notebooks/simple_example.ipynb b/docs/notebooks/simple_example.ipynb index 7f86b4b4e..6b8d98295 100644 --- a/docs/notebooks/simple_example.ipynb +++ b/docs/notebooks/simple_example.ipynb @@ -2,8 +2,14 @@ "cells": [ { "cell_type": "markdown", - "id": "10ab52e6", - "metadata": {}, + "id": "0", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "### Overview\n", "\n", @@ -21,9 +27,15 @@ }, { "cell_type": "code", - "execution_count": 4, - "id": "0", - "metadata": {}, + "execution_count": null, + "id": "1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "import anyio\n", @@ -44,91 +56,42 @@ " print(f\"Waiting {i}\", end=\"\\r\")\n", " await event.wait()\n", " b.close()\n", - " print(\"\\nDone!\")" + " print(\"\\nDone!\")\n" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t← current thread\n", - " ✓\t 🔐\t\tControlThread\t\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2b0c5b62fe5f41d197e30681c6b663ff", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Button(description='Continue', style=ButtonStyle())" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "id": "2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting 3\n", - "Done!\n" - ] - } - ], + "tags": [ + "suppress-error" + ] + }, + "outputs": [], "source": [ + "print(utils.get_tags())\n", "await demo()" ] }, { "cell_type": "code", - "execution_count": 6, - "id": "2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t← current thread\n", - " ✓\t 🔐\t\tControlThread\t\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ec8d324b96f14ca8824299d785143970", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Button(description='Continue', style=ButtonStyle())" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "id": "3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting 3\n", - "Done!\n" - ] - } - ], + "tags": [ + "suppress-error" + ] + }, + "outputs": [], "source": [ "##task\n", "await demo()" @@ -136,44 +99,18 @@ }, { "cell_type": "code", - "execution_count": 7, - "id": "3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t\n", - " ✓\t 🔐\t\tControlThread\t\n", - " ✓\t\t\tThread-3 (anyio_run_caller)\t← current thread\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2c586e969b3f43b2b99cc208bbcee54c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Button(description='Continue', style=ButtonStyle())" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "id": "4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting 3\n", - "Done!\n" - ] - } - ], + "tags": [ + "suppress-error" + ] + }, + "outputs": [], "source": [ "##thread\n", "await demo()" @@ -181,16 +118,28 @@ }, { "cell_type": "markdown", - "id": "76720a7e-8c90-49f1-bfe9-18ce893b481a", - "metadata": {}, + "id": "5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "[test](callers.ipynb)" ] }, { "cell_type": "markdown", - "id": "18abc375", - "metadata": {}, + "id": "6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "## Caller.as_completed\n", "\n", @@ -199,85 +148,20 @@ }, { "cell_type": "code", - "execution_count": 28, - "id": "58d08376", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t← current thread\n", - " ✓\t 🔐\t\tControlThread\t\n", - " ✓\t\t\tThread-3 (anyio_run_caller)\t\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e4d853b05e5b405ba74a7cd3f54e74b9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Button(description='Continue', style=ButtonStyle())" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active\tProtected\t\t\tName\n", - "──────────────────────────────────────────────────────────────────────\n", - " ✓\t 🔐\t\tMainThread\t← current thread\n", - " ✓\t 🔐\t\tControlThread\t\n", - " ✓\t\t\tThread-3 (anyio_run_caller)\t\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c127b6a7796e45ee905124174b89f953", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Button(description='Continue', style=ButtonStyle())" - ] - }, - "metadata": {}, - "output_type": "display_data" + "execution_count": null, + "id": "7", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Waiting 3\n", - "Done!\n", - "\n", - "Done!\n" - ] - } - ], + "tags": [] + }, + "outputs": [], "source": [ - "# \n", - "\n", - "async for _ in Caller.as_completed( Caller().call_soon(demo) for _ in range(2)):\n", + "async for _ in Caller.as_completed(Caller().call_soon(demo) for _ in range(2)):\n", " pass" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ad04634d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -297,13 +181,6 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.13.final.0" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } } }, "nbformat": 4, diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index c6cdfbb7f..f054c9683 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -1,15 +1,3 @@ # Kernel module ::: async_kernel.kernel - -# Kernel messaging - -`shell` and `control` messages are processed with the [_receive_msg_loop][async_kernel.Kernel._receive_msg_loop] running running in event loops in separate threads. The `shell` thread the "MainThread" and `control`thread is provided by a *protected* [Caller][async_kernel.Caller] thread named "CallerThread". - -The - -::: async_kernel.Kernel._receive_msg_loop - -The - -::: async_kernel.Kernel._shell_execute_request_queue diff --git a/mkdocs.yml b/mkdocs.yml index 34a44d765..200e6d905 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,7 +74,7 @@ plugins: include: ["*.ipynb"] - git-revision-date-localized: type: timeago - enabled: !ENV [CI, false] + enabled: !ENV [CI, false] enable_creation_date: true # - privacy: @@ -92,6 +92,7 @@ plugins: paths: [src, docs/snippets] inventories: - https://docs.python.org/3/objects.inv + - https://ipython.readthedocs.io/en/stable/objects.inv options: docstring_options: ignore_init_summary: true @@ -151,27 +152,27 @@ markdown_extensions: nav: - Home: index.md - Usage: - - usage.md - - Notebooks: - - Overview: notebooks/overview.md - - Simple example: notebooks/simple_example.ipynb - - Execute mode: notebooks/run_mode.ipynb - - Caller notebook: notebooks/caller.ipynb - - Command line: command_line.md + - usage.md + - Notebooks: + - Overview: notebooks/overview.md + - Simple example: notebooks/simple_example.ipynb + - Execute mode: notebooks/run_mode.ipynb + - Caller notebook: notebooks/caller.ipynb + - Commands: commands.md - Reference: - reference/overview.md - - Caller & Future: - - reference/caller.md - - Command prompt: - - reference/main.md - - Kernel: - - reference/kernel.md - - Async shell: - - reference/asyncshell.md - - Types: - - reference/typing.md - - Utils: - - reference/utils.md + - Caller & Future: + - reference/caller.md + - Command prompt: + - reference/main.md + - Kernel: + - reference/kernel.md + - Async shell: + - reference/asyncshell.md + - Types: + - reference/typing.md + - Utils: + - reference/utils.md - About: - Contributing: contributing.md - Changelog: changelog.md diff --git a/src/async_kernel/__init__.py b/src/async_kernel/__init__.py index 3f98c6b6d..4d8a6b774 100644 --- a/src/async_kernel/__init__.py +++ b/src/async_kernel/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. + from async_kernel import utils from async_kernel._version import __version__, kernel_protocol_version, kernel_protocol_version_info from async_kernel.caller import Caller, Future diff --git a/src/async_kernel/__main__.py b/src/async_kernel/__main__.py index 1a864c61a..6b56d1d33 100644 --- a/src/async_kernel/__main__.py +++ b/src/async_kernel/__main__.py @@ -107,7 +107,7 @@ async def _start() -> None: else: sys.exit(0) finally: - print("\nKernel stopped: ", kernel.connection_file) + print("Kernel stopped: ", kernel.connection_file) if __name__ == "__main__": diff --git a/src/async_kernel/_version.py b/src/async_kernel/_version.py index d5360f9c0..75589f8ea 100644 --- a/src/async_kernel/_version.py +++ b/src/async_kernel/_version.py @@ -6,7 +6,7 @@ import sys # Version string must appear intact for hatch versioning -__version__ = "0.1b6" +__version__ = "0.1rc0" kernel_protocol_version_info = (5, 4) kernel_protocol_version = "{}.{}".format(*kernel_protocol_version_info) diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index e45ec8d67..73d9d660e 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -24,7 +24,7 @@ import async_kernel from async_kernel.caller import Caller from async_kernel.compiler import XCachingCompiler -from async_kernel.typing import Tags +from async_kernel.typing import Content, Tags if TYPE_CHECKING: from async_kernel.kernel import Kernel @@ -79,11 +79,11 @@ class AsyncDisplayPublisher(DisplayPublisher): @override def publish( # pyright: ignore[reportIncompatibleMethodOverride] self, - data, - metadata=None, + data: Content, + metadata: dict | None = None, *, - transient=None, - update=False, + transient: dict | None = None, + update: bool = False, **kwargs, ) -> None: """Publish a display-data message. @@ -104,7 +104,7 @@ def publish( # pyright: ignore[reportIncompatibleMethodOverride] ) @override - def clear_output(self, wait=False) -> None: + def clear_output(self, wait: bool = False) -> None: """Clear output associated with the current execution (cell). Args: @@ -116,7 +116,17 @@ def clear_output(self, wait=False) -> None: class AsyncInteractiveShell(InteractiveShell): - """A modified IPython [InteractiveShell][IPython.core.interactiveshell.InteractiveShell] to work with [Async kernel][async_kernel.Kernel].""" + """An [IPython InteractiveShell][IPython.core.interactiveshell.InteractiveShell] modified to work with [Async kernel][async_kernel.Kernel]. + + !!! note "Notable differences" + + - All [execute requests][async_kernel.Kernel.execute_request] are run asynchronously. + - Supports a soft timeout with the metadata {"timeout":}[^1]. + + [^1]: When the execution time exceeds the timeout value, the code execution will "move on". + - Not all features are support (see "not-supported" featues listed below). + + """ displayhook_class = Type(AsyncDisplayHook) display_pub_class = Type(AsyncDisplayPublisher) @@ -127,6 +137,19 @@ class AsyncInteractiveShell(InteractiveShell): user_ns_hidden = Dict() _main_mod_cache = Dict() _execute_request_timeout: ContextVar[float | None] = ContextVar("execute_request_timeout", default=None) + run_cell = None # pyright: ignore[reportAssignmentType] + "**not-supported**" + should_run_async = None # pyright: ignore[reportAssignmentType] + loop_runner_map = None + "**not-supported**" + loop_runner = None + "**not-supported**" + debug = None + "**not-supported**" + readline_use = False + "**not-supported**" + autoindent = False + "**not-supported**" @default("banner1") def _default_banner1(self) -> str: @@ -136,13 +159,6 @@ def _default_banner1(self) -> str: f"IPython shell {IPython.core.release.version}\n" ) - # Override the traitlet in the parent class, because there's no point using - # readline for the kernel. Can be removed when the readline code is moved - # to the terminal frontend. - readline_use = CBool(False) - # autoindent has no meaning in a zmqshell, and attempting to enable it - # will print a warning in the absence of readline. - autoindent = CBool(False) @property def kernel(self) -> Kernel: @@ -151,7 +167,7 @@ def kernel(self) -> Kernel: @property def execute_request_timeout(self) -> float | None: - """A timeout in context of the [run_cell_async][async_kernel.Kernel.AsyncInteractiveShell]. + """A timeout in context of the [run_cell_async][async_kernel.asyncshell.AsyncInteractiveShell]. See also: @@ -252,7 +268,7 @@ async def run_cell_async( @override def _showtraceback(self, etype, evalue, stb) -> None: - if Tags.do_not_publish_error in async_kernel.utils.get_tags(): + if Tags.suppress_error in async_kernel.utils.get_tags(): return if self.execute_request_timeout is not None and etype is self.kernel.CancelledError: etype, evalue, stb = TimeoutError, "Cell execute timeout", [] @@ -301,10 +317,10 @@ def connect_info(self, _) -> None: @line_magic def callers(self, _) -> None: - "Print a table of [Callers][async_kernel.Callers], indicating if it is acttive, protect and on the current thread." - lines = ["\t".join(["Active", "Protected", "\t", "Name"]), "─" * 70] - for caller in Caller.all_callers(active_only=False): - symbol = " ✓" if caller.active else " ✗" + "Print a table of [Callers][async_kernel.Caller], indicating its status including: -running - protected - on the current thread." + lines = ["\t".join(["Running", "Protected", "\t", "Name"]), "─" * 70] + for caller in Caller.all_callers(running_only=False): + symbol = " ✓" if caller.running else " ✗" current_thread: Literal["← current thread", ""] = "← current thread" if caller is Caller() else "" protected = " 🔐" if caller.protected else "" lines.append("\t".join([symbol, protected, "", caller.thread.name, current_thread])) diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index 0bbe34665..9a61b61b2 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -20,6 +20,7 @@ from typing_extensions import override from zmq import Context, Socket, SocketType +from async_kernel.kernelspec import Backend from async_kernel.typing import NoValue, PosArgsT, T from async_kernel.utils import wait_thread_event @@ -133,7 +134,7 @@ def set_value(): if threading.current_thread() is not self.thread: try: - Caller(self.thread).call_no_context(func=set_value) + Caller(thread=self.thread).call_no_context(set_value) except RuntimeError: msg = f"The current thread is not {self.thread.name} and a `Caller` does not exist for that thread either." raise RuntimeError(msg) from None @@ -170,7 +171,7 @@ def cancel(self) -> bool: if threading.current_thread() is self.thread: scope.cancel() else: - Caller(self.thread).call_no_context(self.cancel) + Caller(thread=self.thread).call_no_context(self.cancel) return self.cancelled() def cancelled(self) -> bool: @@ -209,7 +210,7 @@ def set_cancel_scope(self, scope: anyio.CancelScope) -> None: def get_caller(self) -> Caller: "The the Caller the Future's thread corresponds." - return Caller(self.thread) + return Caller(thread=self.thread) class Caller: @@ -226,40 +227,69 @@ class Caller: provides methods to start, stop, and query the status of the caller. """ + MAX_IDLE_POOL_INSTANCES = 10 + "The number of `pool` instances to leave idle (See also[to_thread][async_kernel.Caller.to_thread])." + MAX_BUFFER_SIZE = 1000 + "The default maximum_buffer_size used in [queue_call][async_kernel.Caller.queue_call]." _instances: ClassVar[dict[threading.Thread, Self]] = {} - thread: threading.Thread - backend = "" - log: logging.LoggerAdapter[Any] __stack = None _outstanding = 0 _to_thread_pool: ClassVar[deque[Self]] = deque() _pool_instances: ClassVar[weakref.WeakSet[Self]] = weakref.WeakSet() _executor_queue: dict - MAX_IDLE_POOL_INSTANCES = 10 - MAX_BUFFER_SIZE = 1000 _taskgroup: TaskGroup | None = None _jobs: deque[tuple[contextvars.Context, tuple[Future, float, float, Callable, tuple, dict]] | Callable[[], Any]] _jobs_added: threading.Event _stopped = False _protected = False - active = False + _running = False + thread: threading.Thread + "The thread in which the caller will run." + backend: Backend + "The `anyio` backend the caller is running in." + log: logging.LoggerAdapter[Any] + "" iopub_sockets: ClassVar[weakref.WeakKeyDictionary[threading.Thread, Socket]] = weakref.WeakKeyDictionary() iopub_url: ClassVar = "inproc://iopub" def __new__( cls, - thread: threading.Thread | None = None, *, + thread: threading.Thread | None = None, log: logging.LoggerAdapter | None = None, create=False, protected=False, ) -> Self: + """Create the `Caller` instance for the current thread or retrieve an existing instance + by passing the thread. + + The caller provides a way to execute synchronous code in a separate + thread, and to call asynchronous code from synchronous code. + + Args: + thread: + log: Logger to use for logging messages. + create: Whether to create a new instance if one does not exist for the current thread. + protected : Whether the caller is protected from having its event loop closed. + + Returns + ------- + Caller + The `Caller` instance for the current thread. + + Raises + ------ + RuntimeError + If `create` is False and a `Caller` instance does not exist. + """ + thread = thread or threading.current_thread() if not (inst := cls._instances.get(thread)): if not create: msg = f"A caller is not provided for {thread=}" raise RuntimeError(msg) inst = super().__new__(cls) + inst.backend = Backend(sniffio.current_async_library()) inst.thread = thread inst.log = log or logging.LoggerAdapter(logging.getLogger()) inst._jobs = deque() @@ -267,7 +297,6 @@ def __new__( inst._protected = protected inst._executor_queue = {} cls._instances[thread] = inst - return inst @override @@ -277,7 +306,7 @@ def __repr__(self) -> str: async def __aenter__(self) -> Self: self._cancelled_exception_class = anyio.get_cancelled_exc_class() async with contextlib.AsyncExitStack() as stack: - self.active = True + self._running = True self._taskgroup = tg = await stack.enter_async_context(anyio.create_task_group()) await tg.start(self._server_loop, tg) self.__stack = stack.pop_all() @@ -309,7 +338,7 @@ async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> No self._jobs_added.clear() await wait_thread_event(self._jobs_added) finally: - self.active = False + self._running = False for job in self._jobs: if not callable(job): job[1][0].set_exception(FutureCancelledError()) @@ -367,15 +396,21 @@ def _check_in_thread(self): @property def protected(self) -> bool: - "Returns `True` when the instance is protected from stopping." + "Returns `True` if the caller is protected from stopping." return self._protected + @property + def running(self): + "Returns `True` when the caller is available to run requests." + return self._running + @property def stopped(self) -> bool: + "Returns `True` if the caller is stopped." return self._stopped def stop(self, *, force=False) -> None: - """Stop the caller cancelling all pending tasks and close the thread. + """Stop the caller, cancelling all pending tasks and close the thread. If the instance is protected, this is no-op unless force is used. """ @@ -409,7 +444,7 @@ def call_later( self._outstanding += 1 return fut - def call_soon(self, func: Callable[P, T | Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> Future[T]: + def call_soon(self, func: Callable[P, T | Awaitable[T]], /, *args: P.args, **kwargs: P.kwargs) -> Future[T]: """Schedule func to be called in this instances event loop using the current contextvars context. Args: @@ -419,7 +454,7 @@ def call_soon(self, func: Callable[P, T | Awaitable[T]], *args: P.args, **kwargs """ return self.call_later(func, 0.0, *args, **kwargs) - def call_no_context(self, func: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: + def call_no_context(self, func: Callable[P, Any], /, *args: P.args, **kwargs: P.kwargs) -> None: """Call func in the thread event loop. Args: @@ -437,10 +472,11 @@ def has_execution_queue(self, func: Callable) -> bool: async def queue_call( self, func: Callable[[*PosArgsT], Awaitable[Any]], + /, *args: *PosArgsT, max_buffer_size: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] ) -> None: - """Queues the execution of func with the given arguments (not thread-safe). + """Queue the execution of func in queue specific to the function (not thread-safe). The args are added to a queue associated with the provided `func`. If queue does not already exist for func, a new queue is created with a specified maximum buffer size. The arguments are then sent to the queue, @@ -451,8 +487,6 @@ async def queue_call( func: The asynchronous function to execute. *args: The arguments to pass to the function. max_buffer_size: The maximum buffer size for the queue. If NoValue, defaults to [async_kernel.Caller.MAX_BUFFER_SIZE]. - - For usage see [handle_message_request][async_kernel.Kernel.handle_message_request]. """ self._check_in_thread() if not self.has_execution_queue(func): @@ -474,7 +508,7 @@ async def execute_loop(): self._executor_queue[func] = {"queue": sender, "future": self.call_soon(execute_loop)} await self._executor_queue[func]["queue"].send(args) - async def queue_close(self, func: Callable, *, force=False) -> bool: + async def queue_close(self, func: Callable, *, force: bool = False) -> bool: """Close the execution queue associated with func (not thread-safe). Args: @@ -496,8 +530,12 @@ async def queue_close(self, func: Callable, *, force=False) -> bool: return False @classmethod - def stop_all(cls, *, _stop_protected=False) -> None: - "A classmethod to stop all un-protected instances." + def stop_all(cls, *, _stop_protected: bool = False) -> None: + """A classmethod to stop all un-protected callers. + + Args: + _stop_protected: A private argument to shutdown protected instances. + """ for caller in tuple(reversed(cls._instances.values())): caller.stop(force=_stop_protected) @@ -531,8 +569,8 @@ def to_thread_by_name( Args: name: The name of the `Caller`. A new `Caller` is created if an instance corresponding to name [^notes]. - [^notes]: 'MainThread' is special name that applies to the main thread and - will raise a runtime error if a Caller does not exist for the main thread. + [^notes]: 'MainThread' is special name corresponding to the main thread. + A `RuntimeError` will be raised if a Caller does not exist for the main thread. func: The function to call. If it returns an awaitable, the awaitable will be awaited. Passing a coroutine as `func` discourage, but will be awaited. @@ -668,10 +706,10 @@ async def iter_items(task_status: TaskStatus[None]): fut.cancel() @classmethod - def all_callers(cls, active_only=True) -> list[Caller]: + def all_callers(cls, running_only: bool = True) -> list[Caller]: """A classmethod to get a list of the callers. Args: - active_only: Restrict the list to callers that are active (running in an async context). + running_only: Restrict the list to callers that are active (running in an async context). """ - return [caller for caller in Caller._instances.values() if caller.active or not active_only] + return [caller for caller in Caller._instances.values() if caller._running or not running_only] diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 2ffb046d7..9aaf9db76 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -44,7 +44,7 @@ from async_kernel.iostream import OutStream from async_kernel.kernelspec import Backend, KernelName from async_kernel.typing import ( - CODE_MODE_MAPPINGS, + Content, ExecuteContent, HandlerType, Job, @@ -69,7 +69,7 @@ __all__ = ["Kernel", "KernelInterruptError"] -def error_to_dict(error: BaseException, /) -> dict[str, str | list[str]]: +def error_to_content(error: BaseException, /) -> Content: """Convert the error to a dict. ref: https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply @@ -145,10 +145,16 @@ def _try_bind_socket(port: int): @functools.cache def wrap_handler(run_handler: Callable[[HandlerType, Job]], handler: HandlerType) -> HandlerType: - """Wraps the handler with run_handler (cached). - - This is used by [get_handler_and_run_mode][async_kernel.Kernel.get_handler_and_run_mode] to wrap - the calling of message handlers ([message types][async_kernel.typing.MsgType]) with the [run handler][async_kernel.Kernel._run_handler]. + """Wraps a handler function to be executed by a runner function. + + This function takes a runner function and a handler function as input and returns a new handler function. + The new handler function, when called, will execute the original handler function using the provided runner function. + This allows for customization of how handlers are executed, such as running them in a separate thread or process. + Args: + run_handler: A callable that takes a handler and a job as input and executes the handler with the job. + handler: The handler function to be wrapped. + Returns: + A new handler function that will execute the original handler function using the provided runner function. """ async def queued_handler(job: Job) -> None: @@ -165,38 +171,48 @@ class KernelInterruptError(InterruptedError): class Kernel(ConnectionFileMixin): - """An asynchronous kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. + """An asynchronous kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. + + Only one instance will be created/run at a time. The instance can be obtained with `Kernel()`. + + To start the kernel: + + + === "Shell" - To start the kernel. + At the command prompt. - === Shell + ``` shell + async-kernel -f . + ``` - At the command prompt. + See also: - ``` shell - async-kernel -f . - ``` + - - === Normal + === "Normal" ``` python from async_kernel.__main__ import main main() ``` - === Direct + === "start (`classmethod`)" ``` python Kernel.start() ``` - === Asynchronously inside anyio event loop. + === "Asynchronously inside anyio event loop" ``` python kernel = Kernel() async with kernel.start_in_context(): await anyio.sleep_forever() - ``` + ``` + ???+ tip + + This is a convenient way to start a kernel for debugging. """ @@ -239,20 +255,20 @@ def __init__(self, **kwargs) -> None: return # Only initialize once super().__init__(**kwargs) self.message_handlers[SocketID.shell] = { - MsgType.kernel_info_request: (self.kernel_info_request, RunMode.wait), - MsgType.comm_info_request: (self.comm_info_request, RunMode.wait), + MsgType.kernel_info_request: (self.kernel_info_request, RunMode.direct), + MsgType.comm_info_request: (self.comm_info_request, RunMode.direct), MsgType.execute_request: (self.execute_request, RunMode.queue), - MsgType.interrupt_request: (self.interrupt_request, RunMode.wait), + MsgType.interrupt_request: (self.interrupt_request, RunMode.direct), MsgType.complete_request: (self.complete_request, RunMode.thread), MsgType.is_complete_request: (self.is_complete_request, RunMode.thread), MsgType.inspect_request: (self.inspect_request, RunMode.thread), MsgType.history_request: (self.history_request, RunMode.thread), - MsgType.comm_open: (self.comm_open, RunMode.wait), + MsgType.comm_open: (self.comm_open, RunMode.direct), MsgType.comm_msg: (self.comm_msg, RunMode.task), - MsgType.comm_close: (self.comm_close, RunMode.wait), + MsgType.comm_close: (self.comm_close, RunMode.direct), } self.message_handlers[SocketID.control] = self.message_handlers[SocketID.shell] | { - MsgType.shutdown_request: (self.shutdown_request, RunMode.wait), + MsgType.shutdown_request: (self.shutdown_request, RunMode.direct), MsgType.debug_request: (self.debug_request, RunMode.task), } sys.excepthook = self.excepthook @@ -386,6 +402,7 @@ async def start_in_context(self) -> AsyncGenerator[Self, Any]: finally: self.stop() finally: + AsyncInteractiveShell.clear_instance() Context.instance().term() def _signal_handler(self, signum, frame: FrameType | None) -> None: @@ -503,62 +520,6 @@ async def _wait_stopped(self, task_status: TaskStatus[None]) -> None: pass Caller.stop_all(_stop_protected=True) - async def _receive_msg_loop( - self, socket_id: Literal[SocketID.control, SocketID.shell], *, task_status: TaskStatus[None] - ) -> None: - """Receive shell and control messages over zmq sockets.""" - if ( - sys.platform == "win32" - and sniffio.current_async_library() == "asyncio" - and (policy := asyncio.get_event_loop_policy()) - and policy.__class__.__name__ == "WindowsProactorEventLoopPolicy" - ): - from anyio._core._asyncio_selector_thread import get_selector # noqa: PLC0415 - - utils.mark_thread_pydev_do_not_trace(get_selector()._thread) # pyright: ignore[reportPrivateUsage] - socket: Socket[Literal[SocketType.ROUTER]] = Context.instance().socket(SocketType.ROUTER) - with self._bind_socket(socket_id, socket): - try: - task_status.started() - while True: - while socket.get(SocketOption.EVENTS) & PollEvent.POLLIN: # pyright: ignore[reportOperatorIssue] - try: - ident, msg = self.session.recv(socket, copy=False) - assert ident - assert msg - if socket_id == SocketID.shell: - # Reset the frame to show the main thread is not blocked. - self._last_interrupt_frame = None - self.log.debug("*** _receive_msg_loop %s*** %s", socket_id, msg) - await self.handle_message_request( - Job( - socket_id=socket_id, - socket=socket, - ident=ident, - msg=msg, # pyright: ignore[reportArgumentType] - received_time=time.monotonic(), - run_mode=None, # pyright: ignore[reportArgumentType]. This value is set by `get_handler_and_run_mode`. - ) - ) - except Exception as e: - self.log.debug("Bad message on %s: %s", socket_id, e) - continue - await anyio.sleep(0) - await anyio.wait_readable(socket) - except (zmq.ContextTerminated, self.CancelledError): - return - - async def _run_handler(self, handler: HandlerType, job: Job) -> None: - self._job_var.set(job) - try: - self._publish_status(job, "busy") - await handler(job) - except Exception as e: - self._send_reply(job, error_to_dict(e)) - self.log.exception("Exception in message handler:", exc_info=e) - finally: - self._publish_status(job, "idle") - @contextlib.contextmanager def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket) -> Generator[None, Any, None]: """Bind a zmq.Socket storing a reference to the socket and the port @@ -584,7 +545,7 @@ def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket) -> Generator[Non def iopub_send( self, msg_or_type: dict[str, Any] | str, - content: dict[str, Any] | None = None, + content: Content | None = None, metadata: dict[str, Any] | None = None, parent: dict[str, Any] | None | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] ident: bytes | list[bytes] | None = None, @@ -668,6 +629,51 @@ def topic(self, topic) -> bytes: """prefixed topic for IOPub messages""" return (f"kernel.{topic}").encode() + async def _receive_msg_loop( + self, socket_id: Literal[SocketID.control, SocketID.shell], *, task_status: TaskStatus[None] + ) -> None: + """Receive shell and control messages over zmq sockets.""" + if ( + sys.platform == "win32" + and sniffio.current_async_library() == "asyncio" + and (policy := asyncio.get_event_loop_policy()) + and policy.__class__.__name__ == "WindowsProactorEventLoopPolicy" + ): + from anyio._core._asyncio_selector_thread import get_selector # noqa: PLC0415 + + utils.mark_thread_pydev_do_not_trace(get_selector()._thread) # pyright: ignore[reportPrivateUsage] + socket: Socket[Literal[SocketType.ROUTER]] = Context.instance().socket(SocketType.ROUTER) + with self._bind_socket(socket_id, socket): + try: + task_status.started() + while True: + while socket.get(SocketOption.EVENTS) & PollEvent.POLLIN: # pyright: ignore[reportOperatorIssue] + try: + ident, msg = self.session.recv(socket, copy=False) + assert ident + assert msg + if socket_id == SocketID.shell: + # Reset the frame to show the main thread is not blocked. + self._last_interrupt_frame = None + self.log.debug("*** _receive_msg_loop %s*** %s", socket_id, msg) + await self.handle_message_request( + Job( + socket_id=socket_id, + socket=socket, + ident=ident, + msg=msg, # pyright: ignore[reportArgumentType] + received_time=time.monotonic(), + run_mode=None, # pyright: ignore[reportArgumentType]. This value is set by `get_handler_and_run_mode`. + ) + ) + except Exception as e: + self.log.debug("Bad message on %s: %s", socket_id, e) + continue + await anyio.sleep(0) + await anyio.wait_readable(socket) + except (zmq.ContextTerminated, self.CancelledError): + return + async def handle_message_request(self, job: Job, /) -> None: """The main handler for all shell and control messages. @@ -685,7 +691,7 @@ async def handle_message_request(self, job: Job, /) -> None: Caller.to_thread(handler, job) case RunMode.task: Caller().call_soon(handler, job) - case RunMode.wait: + case RunMode.direct: await handler(job) async def get_handler_and_run_mode(self, job: Job) -> tuple[HandlerType, RunMode]: @@ -715,21 +721,38 @@ async def get_handler_and_run_mode(self, job: Job) -> tuple[HandlerType, RunMode mode = mode_from_header elif job["msg"]["header"]["msg_type"] == MsgType.execute_request: content = job["msg"].get("content", {}) - if mode_ := CODE_MODE_MAPPINGS.get(content.get("code", "").strip().split("\n")[0].strip()): + if mode_ := RunMode.get_mode(content.get("code", "")): mode = mode_ elif content.get("silent", True) or (job["socket_id"] is SocketID.control): mode = RunMode.task else: mode = RunMode.queue job["run_mode"] = mode - return wrap_handler(self._run_handler, handler), mode + self.log.debug("%s %s run mode %s for %s", job["socket_id"], mode, msg_type, handler) + return wrap_handler(self.run_handler, handler), mode - async def kernel_info_request(self, job: Job) -> None: - """Handle a kernel info request.""" - self._send_reply(job, self.kernel_info) + async def run_handler(self, handler: HandlerType, job: Job) -> None: + """Runs the handler in the context of the job/message sending the reply content if it is provided. - async def comm_info_request(self, job: Job) -> None: - """Handle a comm info request.""" + This method gets called for every valid request with the relevent handler. + """ + self._job_var.set(job) + try: + self._publish_status(job, "busy") + if (content := await handler(job)) is not None: + self._send_reply(job, content) + except Exception as e: + self._send_reply(job, error_to_content(e)) + self.log.exception("Exception in message handler:", exc_info=e) + finally: + self._publish_status(job, "idle") + + async def kernel_info_request(self, job: Job[Content]) -> Content: + """Handle a ke[rnel info request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info).""" + return self.kernel_info + + async def comm_info_request(self, job: Job[Content]) -> Content: + """Handle a [comm info request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info).""" c = job["msg"]["content"] target_name = c.get("target_name", None) comms = { @@ -737,19 +760,20 @@ async def comm_info_request(self, job: Job) -> None: for (k, v) in tuple(self.comm_manager.comms.items()) if v.target_name == target_name or target_name is None } - self._send_reply(job, {"comms": comms}) + return {"comms": comms} - async def execute_request(self, job: Job[ExecuteContent]) -> None: - """Process the execute request.""" + async def execute_request(self, job: Job[ExecuteContent]) -> Content: + """Handle a [execute request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute).""" c = job["msg"]["content"] - if (job["received_time"] < self._stop_on_error_time) and not c.get("silent", False): + if ( + job["run_mode"] is RunMode.queue + and (job["received_time"] < self._stop_on_error_time) + and not c.get("silent", False) + ): self.log.info("Aborting execute_request: %s", job) - c_ = error_to_dict(RuntimeError("Aborting due to prior exception")) - c_ |= {"execution_count": self.execution_count} - self._publish_status(job, "busy") - self._send_reply(job, c_) - self._publish_status(job, "idle") - return + return error_to_content(RuntimeError("Aborting due to prior exception")) | { + "execution_count": self.execution_count + } metadata = job["msg"].get("metadata") or {} if not (silent := c["silent"]): self._execution_count += 1 @@ -773,8 +797,12 @@ async def execute_request(self, job: Job[ExecuteContent]) -> None: if not silent: self._interrupts.add(fut.cancel) fut.add_done_callback(lambda fut: self._interrupts.discard(fut.cancel)) - result: ExecutionResult = await fut - err = result.error_before_exec or result.error_in_exec if result else KernelInterruptError() + try: + result: ExecutionResult = await fut + err = result.error_before_exec or result.error_in_exec if result else KernelInterruptError() + except Exception as e: + # A safeguard to catch exceptions not caught by the shell. + err = e if (err) and ( (Tags.suppress_error in metadata.get("tags", ())) # 1. or (isinstance(err, self.CancelledError) and (self.shell.execute_request_timeout is not None)) # 2. @@ -783,20 +811,20 @@ async def execute_request(self, job: Job[ExecuteContent]) -> None: # 1. tag # 2. timeout err = None - err_content = { + content = { "status": "error" if err else "ok", "execution_count": self.execution_count, "user_expressions": self.shell.user_expressions(c.get("user_expressions", {})), } if err: - err_content |= error_to_dict(err) - if not silent and c.get("stop_on_error"): + content |= error_to_content(err) + if (not silent) and c.get("stop_on_error"): self._stop_on_error_time = time.monotonic() self.log.info("An error occurred in a non-silent execution request") - self._send_reply(job, err_content) + return content - async def complete_request(self, job: Job[dict[str, Any]]) -> None: - """Handle a [completion request][async_kernel.typing.complete_request].""" + async def complete_request(self, job: Job[Content]) -> Content: + """Handle a [completion request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion).""" c = job["msg"]["content"] code: str = c["code"] cursor_pos = c.get("cursor_pos") or len(code) @@ -814,43 +842,42 @@ async def complete_request(self, job: Job[dict[str, Any]]) -> None: ] s, e = completions[0].start, completions[0].end if completions else (cursor_pos, cursor_pos) matches = [c.text for c in completions] - matches = { + return { "matches": matches, "cursor_end": e, "cursor_start": s, "metadata": {"_jupyter_types_experimental": comps}, } - self._send_reply(job, matches) - async def is_complete_request(self, job: Job) -> None: - """Handle an [is_complete request][async_kernel.typing.is_complete_request].""" + async def is_complete_request(self, job: Job[Content]) -> Content: + """Handle a [is_complete request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness).""" status, indent_spaces = self.shell.input_transformer_manager.check_complete(job["msg"]["content"]["code"]) - reply_content = {"status": status} + content = {"status": status} if status == "incomplete": - reply_content["indent"] = " " * indent_spaces - self._send_reply(job, reply_content) + content["indent"] = " " * indent_spaces + return content - async def inspect_request(self, job: Job[dict[str, Any]]) -> None: - """Handle an [inspect request][async_kernel.typing.inspect_request].""" + async def inspect_request(self, job: Job[Content]) -> Content: + """Handle a [inspect request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introspection).""" c = job["msg"]["content"] detail_level = int(c.get("detail_level", 0)) omit_sections = set(c.get("omit_sections", [])) name = token_at_cursor(c["code"], c["cursor_pos"]) - reply_content: dict[str, Any] = {"status": "ok"} - reply_content["data"] = {} - reply_content["metadata"] = {} + content: dict[str, Any] = {"status": "ok"} + content["data"] = {} + content["metadata"] = {} try: bundle = self.shell.object_inspect_mime(name, detail_level=detail_level, omit_sections=omit_sections) - reply_content["data"].update(bundle) + content["data"].update(bundle) if not self.shell.enable_html_pager: - reply_content["data"].pop("text/html") - reply_content["found"] = True + content["data"].pop("text/html") + content["found"] = True except KeyError: - reply_content["found"] = False - self._send_reply(job, reply_content) + content["found"] = False + return content - async def history_request(self, job: Job[dict[str, Any]]) -> None: - """Handle a [history request][async_kernel.typing.history_request].""" + async def history_request(self, job: Job[Content]) -> Content: + """Handle a [history request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#history).""" c = job["msg"]["content"] history_manager = self.shell.history_manager assert history_manager @@ -866,22 +893,22 @@ async def history_request(self, job: Job[dict[str, Any]]) -> None: ) else: hist = [] - self._send_reply(job, {"history": list(hist)}) + return {"history": list(hist)} - async def comm_open(self, job: Job) -> None: - """Handle a [comm open request][async_kernel.typing.comm_open].""" + async def comm_open(self, job: Job[Content]) -> None: + """Handle a [comm open request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#opening-a-comm).""" self.comm_manager.comm_open(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_msg(self, job: Job) -> None: - """Handle a [comm msg request][async_kernel.typing.comm_msg].""" + async def comm_msg(self, job: Job[Content]) -> None: + """Handle a [comm msg request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-messages).""" self.comm_manager.comm_msg(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_close(self, job: Job) -> None: - """Handle a [comm close request][async_kernel.typing.comm_close].""" + async def comm_close(self, job: Job[Content]) -> None: + """Handle a [comm close request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#tearing-down-comms).""" self.comm_manager.comm_close(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def interrupt_request(self, job: Job) -> None: - """Handle a [interrupt request][async_kernel.typing.interrupt_request].""" + async def interrupt_request(self, job: Job[Content]) -> Content: + """Handle a [interrupt request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt) (control only).""" self._interrupt_requested = True if sys.platform == "win32": signal.raise_signal(signal.SIGINT) @@ -890,18 +917,17 @@ async def interrupt_request(self, job: Job) -> None: os.kill(os.getpid(), signal.SIGINT) for interrupter in tuple(self._interrupts): interrupter() - self._send_reply(job) + return {} - async def shutdown_request(self, job: Job) -> None: - """Handle a [shutdown request][async_kernel.typing.shutdown_request].""" + async def shutdown_request(self, job: Job[Content]) -> Content: + """Handle a [shutdown request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-shutdown) (control only).""" await self.debugger.disconnect() - self._send_reply(job, {"status": "ok", "restart": job["msg"]["content"].get("restart", False)}) - self.stop() + Caller().call_no_context(self.stop) + return {"status": "ok", "restart": job["msg"]["content"].get("restart", False)} - async def debug_request(self, job: Job) -> None: - """Handle a [debug request][async_kernel.typing.debug_request].""" - content = await self.debugger.process_request(job["msg"]["content"]) - self._send_reply(job, content) + async def debug_request(self, job: Job[Content]) -> Content: + """Handle a [debug request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request) (control only).""" + return await self.debugger.process_request(job["msg"]["content"]) def excepthook(self, etype, evalue, tb) -> None: """Handle an exception.""" diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 030c3d4c0..8148cc639 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -4,11 +4,10 @@ from __future__ import annotations import enum -from collections.abc import Callable -from types import CoroutineType -from typing import TYPE_CHECKING, Any, Final, Generic, Literal, ParamSpec, TypedDict, TypeVar, TypeVarTuple +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any, Generic, Literal, ParamSpec, TypedDict, TypeVar, TypeVarTuple -from typing_extensions import Sentinel +from typing_extensions import Sentinel, override if TYPE_CHECKING: from collections.abc import Mapping @@ -16,8 +15,6 @@ import zmq __all__ = [ - "CODE_MODE_MAPPINGS", - "RUN_MODE_PREFIX", "DebugMessage", "Job", "Message", @@ -53,27 +50,60 @@ class SocketID(enum.StrEnum): "" -RUN_MODE_PREFIX: Final = "##" # "The Prefix used for [RunMode][async_kernel.typing.RunMode] identifiers." +class RunMode(enum.StrEnum): + """An Enum of the [kernel run modes][async_kernel.Kernel.handle_message_request] available for + altering how message requests are run. + !!! note "Prefix '##'" -class RunMode(enum.StrEnum): - "An Enum of the Run modes available for altering how jobs are(https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute) are handled." + Run mode is matched for both mode without the prefix ("##"). - queue = "queue" - "Add to the execute_request queue." - task = "task" - "Execute as a task in the MainThread." - thread = "thread" - "Execute in a caller worker thread." - thread_ = "thread" - "Execute in a caller worker thread." - wait = "wait" - """Wait for the message to execute. + !!! note "special usage" - This blocks the message loop""" + Run mode can be used in [execute requests](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute). + Add it at the top line (or use the string equivalent "##") of a code cell. + """ + "The prefix for each run mode." -CODE_MODE_MAPPINGS: Final[dict[str, RunMode]] = {f"{RUN_MODE_PREFIX}{mode}": mode for mode in RunMode} + queue = "queue" + "The message for the [handler][async_kernel.typing.MsgType] is run sequentially with other messages that are queued." + task = "task" + "The message for the [handler][async_kernel.typing.MsgType] are run concurrently in task (starting immediately)." + thread = "thread" + "Messages for the [handler][async_kernel.typing.MsgType] are run concurrently in a thread (starting immediately)." + direct = "direct" + """Run the handler directly as soon as it is received. + + !!! warning + + **This mode blocks the message loop.** + + Use this for short running messages that should be processed as soon as it is received. + """ + + @override + def __str__(self): + return f"##{self.name}" + + @override + def __eq__(self, value: object, /) -> bool: + return str(value) in (self.name, str(self), repr(self)) + + @override + def __hash__(self) -> int: + return hash(self.name) + + @classmethod + def get_mode(cls, code: str) -> RunMode | None: + "Get a RunMode from the code if it is found." + try: + if (code := code.strip().split("\n")[0].strip()).startswith("##"): + return RunMode(code.removeprefix("##")) + if code.startswith("RunMode."): + return RunMode(code.removeprefix("RunMode.")) + except ValueError: + return None class MsgType(enum.StrEnum): @@ -85,32 +115,32 @@ class MsgType(enum.StrEnum): """ kernel_info_request = "kernel_info_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info)" + "[async_kernel.Kernel.kernel_info_request][]" comm_info_request = "comm_info_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info)" + "[async_kernel.Kernel.comm_info_request][]" execute_request = "execute_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)" + "[async_kernel.Kernel.execute_request][]" complete_request = "complete_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion)" + "[async_kernel.Kernel.complete_request][]" is_complete_request = "is_complete_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness)" + "[async_kernel.Kernel.is_complete_request][]" inspect_request = "inspect_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introspection)" + "[async_kernel.Kernel.inspect_request][]" history_request = "history_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#history)" + "[async_kernel.Kernel.history_request][]" comm_open = "comm_open" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#opening-a-comm)" + "[async_kernel.Kernel.comm_open][]" comm_msg = "comm_msg" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-messages)" + "[async_kernel.Kernel.comm_msg][]" comm_close = "comm_close" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#tearing-down-comms)" + "[async_kernel.Kernel.comm_close][]" # Control interrupt_request = "interrupt_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt) (control only)" + "[async_kernel.Kernel.interrupt_request][]" shutdown_request = "shutdown_request" - "[ref](shutdown_request) (control only)" + "[async_kernel.Kernel.shutdown_request][]" debug_request = "debug_request" - "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request) (control only)" + "[async_kernel.Kernel.debug_request][]" class MetadataKeys(enum.StrEnum): @@ -138,9 +168,12 @@ class Tags(enum.StrEnum): """Tags recognised by the kernel""" suppress_error = "suppress-error" - """Ignore`stop_on_error` in context of the `execute request`.""" - do_not_publish_error = "do-not-publish-error" - """Prevent the shell from publishing error messages in context of the `execute request`.""" + """Suppress exceptions for the code code cell. + + !!! note "Warning" + + The code block will return as 'ok' and there will be no message recorded. + """ class MsgHeader(TypedDict): @@ -169,7 +202,7 @@ class Message(TypedDict, Generic[T]): "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#parent-header)" metadata: Mapping[MetadataKeys | str, Any] "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata)" - content: T + content: T | Content """[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata) See also: @@ -215,4 +248,5 @@ class ExecuteContent(TypedDict): DebugMessage = dict[str, Any] -HandlerType = Callable[[Job], CoroutineType] +Content = dict[str, Any] +HandlerType = Callable[[Job], Awaitable[Content | None]] diff --git a/tests/test_caller.py b/tests/test_caller.py index 32922d12e..e751957a5 100644 --- a/tests/test_caller.py +++ b/tests/test_caller.py @@ -138,16 +138,20 @@ async def test_sync(self): caller.call_later(is_called.set) await is_called.wait() - def test_caller_no_thread(self): + def test_no_thread(self): with pytest.raises(RuntimeError): Caller() - def test_caller_protected(self): + async def test_protected(self, anyio_backend): caller = Caller(create=True, protected=True) caller.stop() assert not caller.stopped caller.stop(force=True) + def test_no_backend_error(self, anyio_backend): + with pytest.raises(RuntimeError): + Caller(create=True) + @pytest.mark.parametrize("args_kwargs", [((), {}), ((1, 2, 3), {"a": 10})]) async def test_async(self, args_kwargs: tuple[tuple, dict]): val = None @@ -168,7 +172,7 @@ async def my_func(is_called: anyio.Event, *args, **kwargs): async def test_anyio_to_thread(self): # Test the call works from an anyio thread async with Caller(create=True) as caller: - assert caller.active + assert caller.running assert caller in Caller.all_callers() def _in_thread(): diff --git a/tests/test_comm.py b/tests/test_comm.py index 9952534f3..e1016f56b 100644 --- a/tests/test_comm.py +++ b/tests/test_comm.py @@ -103,4 +103,4 @@ def on_msg(msg): manager.comm_close(None, None, msg) assert len(msgs) == 3 - assert comm._closed + assert comm._closed # pyright: ignore[reportPrivateUsage] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index ed198639e..d287162b5 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -18,17 +18,7 @@ import async_kernel.utils from async_kernel.caller import Caller from async_kernel.comm import Comm -from async_kernel.typing import ( - RUN_MODE_PREFIX, - ExecuteContent, - Job, - Message, - MsgHeader, - MsgType, - RunMode, - SocketID, - Tags, -) +from async_kernel.typing import ExecuteContent, Job, Message, MsgHeader, MsgType, RunMode, SocketID, Tags from tests import utils if TYPE_CHECKING: @@ -94,13 +84,11 @@ async def test_simple_print(kernel, client, quiet: bool): await utils.clear_iopub(client) -@pytest.mark.parametrize("mode", ["kernel_timeout", "metadata", "metadata-do-not-publish-error"]) +@pytest.mark.parametrize("mode", ["kernel_timeout", "metadata"]) async def test_execute_kernel_timeout(client, kernel: Kernel, mode: str): kernel.cell_execute_timeout = 0.1 if "kernel" in mode else None last_stop_time = kernel._stop_on_error_time # pyright: ignore[reportPrivateUsage] metadata: dict[str, float | list] = {"timeout": 0.1} - if "do-not-publish-error" in mode: - metadata["tags"] = ["do-not-publish-error"] try: code = "\n".join(["import anyio", "await anyio.sleep_forever()"]) msg_id, content = await utils.execute(client, code=code, metadata=metadata, clear_pub=False) @@ -108,9 +96,8 @@ async def test_execute_kernel_timeout(client, kernel: Kernel, mode: str): assert content["status"] == "ok" await utils.check_pub_message(client, msg_id, execution_state="busy") await utils.check_pub_message(client, msg_id, msg_type="execute_input") - if "do-not-publish-error" not in mode: - expected = {"traceback": [], "ename": "TimeoutError", "evalue": "Cell execute timeout"} - await utils.check_pub_message(client, msg_id, msg_type="error", **expected) + expected = {"traceback": [], "ename": "TimeoutError", "evalue": "Cell execute timeout"} + await utils.check_pub_message(client, msg_id, msg_type="error", **expected) await utils.check_pub_message(client, msg_id, execution_state="idle") finally: kernel.cell_execute_timeout = None @@ -227,33 +214,46 @@ async def test_message_order(client): reply = await client.get_shell_msg() assert reply["content"]["execution_count"] == i assert reply["parent_header"]["msg_id"] == msg_id - await utils.clear_iopub(client) + await utils.clear_iopub(client, timeout=0.2) async def test_execute_request_error_tag_ignore_error(client): metadata = {"tags": [Tags.suppress_error]} - _, content = await utils.execute(client, "ignore error", metadata=metadata) + msg_id, content = await utils.execute( + client, "This error should be suppressed...", metadata=metadata, clear_pub=False + ) + await utils.check_pub_message(client, msg_id, execution_state="busy") + await utils.check_pub_message(client, msg_id, msg_type="execute_input") + await utils.check_pub_message(client, msg_id, execution_state="idle") assert content["status"] == "ok" - await utils.clear_iopub(client) -async def test_execute_request_error(client): - reply = await utils.send_shell_message( - client, MsgType.execute_request, {"code": "some invalid code", "silent": False} - ) +@pytest.mark.parametrize("run_mode", RunMode) +@pytest.mark.parametrize( + "code", + [ + "some invalid code", + "\n".join( + [ + "from async_kernel.caller import FutureCancelledError", + "async def fail():", + " raise FutureCancelledError", + "await fail()", + ] + ), + ], +) +async def test_execute_request_error(client, code: str, run_mode: RunMode): + reply = await utils.send_shell_message(client, MsgType.execute_request, {"code": code, "silent": False}) assert reply["header"]["msg_type"] == "execute_reply" assert reply["content"]["status"] == "error" await utils.clear_iopub(client) -async def test_execute_request_stop_on_error(client, kernel): - kernel._stop_on_error_time = time.monotonic() + 10 - reply = await utils.send_shell_message( - client, MsgType.execute_request, {"code": "some invalid code", "silent": False} - ) - assert reply["header"]["msg_type"] == "execute_reply" - assert reply["content"]["status"] == "error" - kernel._stop_on_error_time = 0 +async def test_execute_request_stop_on_error(client): + client.execute("import anyio;anyio.sleep(0.1);stop-here") + _, content = await utils.execute(client) + assert content["evalue"] == "Aborting due to prior exception" async def test_complete_request(client): @@ -355,7 +355,7 @@ async def test_interrupt_request_blocking_exec_request(subprocess_kernels_client async def test_interrupt_request_blocking_task(subprocess_kernels_client): code = f""" - {RUN_MODE_PREFIX}{RunMode.task} + {RunMode.task} time.sleep(100) """ client = subprocess_kernels_client @@ -476,7 +476,7 @@ async def test_shell_can_set_namespace(kernel): @pytest.mark.parametrize("mode", RunMode) async def test_header_mode(client, mode: RunMode): code = f""" -{RUN_MODE_PREFIX}{mode} +{mode} import time time.sleep(0.1) print("{mode.name}") @@ -517,17 +517,18 @@ async def test_invalid_message(client, channel): @pytest.mark.parametrize( ("code", "silent", "socket_id", "expected"), [ - (f"{RUN_MODE_PREFIX}{RunMode.task}", False, SocketID.shell, RunMode.task), - (f" {RUN_MODE_PREFIX}{RunMode.task}", False, SocketID.shell, RunMode.task), + (f"{RunMode.task}", False, SocketID.shell, RunMode.task), + (f" {RunMode.task}", False, SocketID.shell, RunMode.task), ("print(1)", False, SocketID.shell, RunMode.queue), ("", True, SocketID.shell, RunMode.task), - (f"{RUN_MODE_PREFIX}{RunMode.thread}\nprint('hello')", False, SocketID.shell, RunMode.thread), + (f"{RunMode.thread}\nprint('hello')", False, SocketID.shell, RunMode.thread), ("", False, SocketID.control, RunMode.task), - (f"{RUN_MODE_PREFIX}threads", False, SocketID.shell, RunMode.queue), - (f"{RUN_MODE_PREFIX}Task", False, SocketID.shell, RunMode.queue), + ("threads", False, SocketID.shell, RunMode.queue), + ("Task", False, SocketID.shell, RunMode.queue), + ("RunMode.direct", False, SocketID.shell, RunMode.direct), ], ) -async def test_get_run_mode_execute_request(kernel:Kernel, code: str, silent: bool, socket_id, expected: RunMode): +async def test_get_run_mode_execute_request(kernel: Kernel, code: str, silent: bool, socket_id, expected: RunMode): content = ExecuteContent( code=code, silent=silent, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True ) @@ -538,4 +539,3 @@ async def test_get_run_mode_execute_request(kernel:Kernel, code: str, silent: bo _, mode = await kernel.get_handler_and_run_mode(job) assert mode is expected assert job.get("run_mode") is expected - diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 000000000..7dd720309 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,26 @@ +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + + +from async_kernel.typing import RunMode + + +class TestRunMode: + def test_str(self): + assert str(RunMode.task) == RunMode.task + + def test_repr(self): + assert repr(RunMode.task) == RunMode.task + + def test_hash(self): + assert hash(RunMode.task) == hash(RunMode.task) + + def test_members(self): + assert list(RunMode) == ["queue", "task", "thread", "direct"] + assert list(RunMode) == ["##queue", "##task", "##thread", "##direct"] + assert list(RunMode) == [ + "", + "", + "", + "", + ] diff --git a/tests/utils.py b/tests/utils.py index 7c1917dbf..a7081c3fa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -114,7 +114,7 @@ async def execute(client: AsyncKernelClient, /, code="", clear_pub=True, metadat return msg_id, reply["content"] -async def assemble_output(client: AsyncKernelClient, timeout=TIMEOUT): +async def assemble_output(client: AsyncKernelClient, timeout=TIMEOUT, exit_at_idle=True): """Assemble stdout/err from an execution""" assert isinstance(client, AsyncKernelClient) stdout = "" @@ -125,11 +125,12 @@ async def assemble_output(client: AsyncKernelClient, timeout=TIMEOUT): msg = await client.get_iopub_msg() msg_type = msg["msg_type"] content = msg["content"] - if not done: - done = bool(msg_type == "status" and content["execution_state"] == "idle") - if done and (stdout or stderr): - # idle message signals end of output - break + if exit_at_idle: + if not done: + done = bool(msg_type == "status" and content["execution_state"] == "idle") + if done and (stdout or stderr): + # idle message signals end of output + break if msg["msg_type"] == "stream": if content["name"] == "stdout": stdout += content["text"] @@ -153,7 +154,7 @@ async def wait_for_idle(client: AsyncKernelClient, *, wait=1.0): async def clear_iopub(client, *, timeout=0.01): "Ensure there are no further iopub messages waiting." - await assemble_output(client, timeout=timeout) + await assemble_output(client, timeout=timeout, exit_at_idle=False) async def send_shell_message( From e5d462be3b561058d0839ce170b6d0c34d622586 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Mon, 18 Aug 2025 21:39:37 +1000 Subject: [PATCH 11/18] Better kernel cleanup, no longer need tests.utils.clear_kernel. --- .vscode/spellright.dict | 1 + README.md | 4 +-- docs/notebooks/overview.md | 3 +-- docs/notebooks/run_mode.ipynb | 40 +++++++++++++++++------------ docs/notebooks/simple_example.ipynb | 3 ++- docs/reference/asyncshell.md | 2 +- docs/reference/caller.md | 2 +- docs/reference/main.md | 2 +- docs/reference/overview.md | 1 - docs/reference/typing.md | 2 +- docs/reference/utils.md | 2 +- docs/usage.md | 11 ++++---- src/async_kernel/asyncshell.py | 7 +++-- src/async_kernel/kernel.py | 8 +++--- tests/conftest.py | 8 ++---- tests/test_main.py | 1 - tests/test_start_in_context.py | 39 ++++++++++------------------ tests/utils.py | 11 -------- 18 files changed, 63 insertions(+), 84 deletions(-) diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index d52233e84..8294c379c 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -7,3 +7,4 @@ zmq IPyKernel anyio's Asyc +Jupyterlab diff --git a/README.md b/README.md index 092a6dd95..42381250a 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels-programming-languages) that runs in an [anyio](https://pypi.org/project/anyio/) event loop. - ## Highlights + - Asynchronous - Comms is not blocked during cell execution[^non-blocking-execution] - Concurrent code execution [^run-concurrent] @@ -18,7 +18,6 @@ Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/pro - [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, and history - No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets - ## Installation ```shell @@ -39,7 +38,6 @@ async-kernel -a async-trio [![Link to demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811)](https://fleming79.github.io/async-kernel/simple_example/ "Show demo notebook.") - [^non-blocking-execution]: Shell messaging runs in a task separate to execute requests in the main thread. This means shell messages (including comms) can pass freely whilst an execute request is busy awaiting a result. [^run-concurrent]: Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. diff --git a/docs/notebooks/overview.md b/docs/notebooks/overview.md index b7e1a2d16..caa18e878 100644 --- a/docs/notebooks/overview.md +++ b/docs/notebooks/overview.md @@ -4,7 +4,6 @@ Notebooks in this documentation show the result of each cell after executing for You can download the notebook with the button at the top right of the page for the notebook. - !!! note - [`suppress-error`][async_kernel.typing.Tags.suppress_error] error tags are used with generating documentation. + \[`suppress-error`\][async_kernel.typing.Tags.suppress_error] error tags are used with generating documentation. diff --git a/docs/notebooks/run_mode.ipynb b/docs/notebooks/run_mode.ipynb index 989807094..367cd5fab 100644 --- a/docs/notebooks/run_mode.ipynb +++ b/docs/notebooks/run_mode.ipynb @@ -42,8 +42,16 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "2", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "3", "metadata": { "editable": true, "slideshow": { @@ -61,7 +69,7 @@ }, { "cell_type": "markdown", - "id": "3", + "id": "4", "metadata": { "editable": true, "slideshow": { @@ -82,7 +90,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "5", "metadata": { "editable": true, "slideshow": { @@ -117,7 +125,7 @@ }, { "cell_type": "markdown", - "id": "5", + "id": "6", "metadata": { "editable": true, "slideshow": { @@ -132,7 +140,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "7", "metadata": { "editable": true, "slideshow": { @@ -150,7 +158,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "8", "metadata": { "editable": true, "slideshow": { @@ -169,7 +177,7 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "9", "metadata": { "editable": true, "slideshow": { @@ -192,7 +200,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "10", "metadata": { "editable": true, "slideshow": { @@ -210,7 +218,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "11", "metadata": { "editable": true, "slideshow": { @@ -229,7 +237,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "12", "metadata": { "editable": true, "slideshow": { @@ -248,7 +256,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "13", "metadata": { "editable": true, "slideshow": { @@ -264,7 +272,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "14", "metadata": { "editable": true, "slideshow": { @@ -275,13 +283,13 @@ "source": [ "### Direct use symbol\n", "\n", - "Using the `RunMode` vaules directly is also possible." + "Using the literal `RunMode` values directly is also possible. Though it may show up as a [Flake8 B018 issue](https://docs.astral.sh/ruff/rules/useless-expression/)." ] }, { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": { "editable": true, "slideshow": { @@ -297,7 +305,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": { "editable": true, "slideshow": { @@ -309,7 +317,7 @@ }, "outputs": [], "source": [ - "RunMode.task\n", + "RunMode.task # noqa: B018\n", "\n", "await demo()" ] diff --git a/docs/notebooks/simple_example.ipynb b/docs/notebooks/simple_example.ipynb index 6b8d98295..b4e81efd0 100644 --- a/docs/notebooks/simple_example.ipynb +++ b/docs/notebooks/simple_example.ipynb @@ -40,6 +40,7 @@ "source": [ "import anyio\n", "import ipywidgets as ipw\n", + "import utils\n", "\n", "from async_kernel import Caller\n", "\n", @@ -56,7 +57,7 @@ " print(f\"Waiting {i}\", end=\"\\r\")\n", " await event.wait()\n", " b.close()\n", - " print(\"\\nDone!\")\n" + " print(\"\\nDone!\")" ] }, { diff --git a/docs/reference/asyncshell.md b/docs/reference/asyncshell.md index 50391f955..1f96cc084 100644 --- a/docs/reference/asyncshell.md +++ b/docs/reference/asyncshell.md @@ -1,3 +1,3 @@ # Shell module -::: async_kernel.asyncshell \ No newline at end of file +::: async_kernel.asyncshell diff --git a/docs/reference/caller.md b/docs/reference/caller.md index 04541fbde..cc6a028ea 100644 --- a/docs/reference/caller.md +++ b/docs/reference/caller.md @@ -1 +1 @@ -::: async_kernel.caller \ No newline at end of file +::: async_kernel.caller diff --git a/docs/reference/main.md b/docs/reference/main.md index 7f48cf7b5..a954507e5 100644 --- a/docs/reference/main.md +++ b/docs/reference/main.md @@ -6,4 +6,4 @@ - Define a new kernel spec - Remove an existing kernel spec -:::async_kernel.__main__.main \ No newline at end of file +:::async_kernel.__main__.main diff --git a/docs/reference/overview.md b/docs/reference/overview.md index 5f5a31d9d..5c98646d4 100644 --- a/docs/reference/overview.md +++ b/docs/reference/overview.md @@ -5,4 +5,3 @@ The reference section provides documentation for each module in async kernel. ## Highlights - [Caller][async_kernel.caller.Caller] -- [command prompt (main)][async_kernel.__main__.main] \ No newline at end of file diff --git a/docs/reference/typing.md b/docs/reference/typing.md index af380911d..bcaeab673 100644 --- a/docs/reference/typing.md +++ b/docs/reference/typing.md @@ -1,2 +1,2 @@ ::: async_kernel.typing - \ No newline at end of file + diff --git a/docs/reference/utils.md b/docs/reference/utils.md index 4e7a87776..6187d369e 100644 --- a/docs/reference/utils.md +++ b/docs/reference/utils.md @@ -1 +1 @@ -::: async_kernel.utils \ No newline at end of file +::: async_kernel.utils diff --git a/docs/usage.md b/docs/usage.md index d44720be4..e68af76a8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,12 +9,13 @@ Async kernel is asynchronous by design, shell messaging runs in a loop in the ma - Use [async_kernel.Caller.call_soon][] or [async_kernel.Caller.call_later][] to run code in tasks to support either backend.(1) - Use [anyio](https://anyio.readthedocs.io) or async functions corresponding to the anyio backend(2) freely in the main thread. - Use [async_kernel.Caller.start_new][] to start a thread with the opposite backend. -- Start a new Caller if there are functions require the opposite asynchronous backend. +- Start a new Caller if there are functions require the opposite asynchronous backend. -1. Caller provides methods for thread safe scheduling and awaiting the result using [`Futures`][async_kernel.caller.Future]) + +1. Caller provides methods for thread safe scheduling and awaiting the result using [Futures][async_kernel.caller.Future] + 1. Use `Caller.get_instance()` to get the `Caller` for the main thread. - 2. Use `Caller()` to get the `Caller` for the current thread. + 1. Use `Caller()` to get the `Caller` for the current thread. -2. Async-kernel runs in anyio. -3. \ No newline at end of file +1. Async-kernel runs in anyio. diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index 73d9d660e..22d4a67e5 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -18,7 +18,7 @@ from IPython.core.magic import Magics, line_magic, magics_class from jupyter_client.jsonutil import json_default from jupyter_core.paths import jupyter_runtime_dir -from traitlets import CBool, Dict, Instance, Type, default, observe +from traitlets import Dict, Instance, Type, default, observe from typing_extensions import override import async_kernel @@ -124,8 +124,8 @@ class AsyncInteractiveShell(InteractiveShell): - Supports a soft timeout with the metadata {"timeout":}[^1]. [^1]: When the execution time exceeds the timeout value, the code execution will "move on". - - Not all features are support (see "not-supported" featues listed below). - + - Not all features are support (see "not-supported" features listed below). + """ displayhook_class = Type(AsyncDisplayHook) @@ -159,7 +159,6 @@ def _default_banner1(self) -> str: f"IPython shell {IPython.core.release.version}\n" ) - @property def kernel(self) -> Kernel: "The current kernel." diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 9aaf9db76..fe29d3751 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -171,10 +171,10 @@ class KernelInterruptError(InterruptedError): class Kernel(ConnectionFileMixin): - """An asynchronous kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. + """An asynchronous kernel with an anyio backend providing an IPython AsyncInteractiveShell with zmq sockets. Only one instance will be created/run at a time. The instance can be obtained with `Kernel()`. - + To start the kernel: @@ -188,7 +188,7 @@ class Kernel(ConnectionFileMixin): See also: - - + - === "Normal" ``` python @@ -734,7 +734,7 @@ async def get_handler_and_run_mode(self, job: Job) -> tuple[HandlerType, RunMode async def run_handler(self, handler: HandlerType, job: Job) -> None: """Runs the handler in the context of the job/message sending the reply content if it is provided. - This method gets called for every valid request with the relevent handler. + This method gets called for every valid request with the relevant handler. """ self._job_var.set(job) try: diff --git a/tests/conftest.py b/tests/conftest.py index 454a2d304..c12254752 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,17 +47,13 @@ def transport(): @pytest.fixture(scope="module") async def kernel(anyio_backend, transport: str, tmp_path_factory): # Set a blank connection_file - utils.clear_kernel() connection_file = tmp_path_factory.mktemp("async_kernel") / "temp_connection.json" os.environ["IPYTHONDIR"] = str(tmp_path_factory.mktemp("ipython_config")) kernel = Kernel() kernel.connection_file = str(connection_file.resolve()) kernel.transport = transport - try: - async with kernel.start_in_context(): - yield kernel - finally: - utils.clear_kernel() + async with kernel.start_in_context(): + yield kernel @pytest.fixture(scope="module") diff --git a/tests/test_main.py b/tests/test_main.py index 6f293793d..70b115a20 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -105,7 +105,6 @@ async def wait_exit(): out = capsys.readouterr().out assert "Starting kernel" in out assert "Kernel stopped" in out - utils.clear_kernel() def test_start_kernel_failure(monkeypatch, capsys, mocker): diff --git a/tests/test_start_in_context.py b/tests/test_start_in_context.py index 1a458abff..56d8f36f4 100644 --- a/tests/test_start_in_context.py +++ b/tests/test_start_in_context.py @@ -3,17 +3,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast - import pytest from async_kernel import Kernel from async_kernel.kernelspec import KernelName from async_kernel.typing import SocketID -from tests import utils - -if TYPE_CHECKING: - import zmq @pytest.fixture(scope="module", params=list(KernelName)) @@ -27,24 +21,19 @@ def anyio_backend(kernel_name: KernelName): async def test_start_kernel_in_context(anyio_backend, kernel_name): - utils.clear_kernel() - try: - async with Kernel().start_in_context() as kernel: - assert kernel.kernel_name == kernel_name - connection_file = kernel.connection_file - # Test prohibit nested async context. - with pytest.raises(RuntimeError, match="Already started"): - async with kernel.start_in_context(): - pass - # Test prevents binding socket more than once. - with ( - pytest.raises(RuntimeError, match=".*is already loaded"), - kernel._bind_socket(SocketID.shell, cast("zmq.Socket", None)), - ): + async with Kernel().start_in_context() as kernel: + assert kernel.kernel_name == kernel_name + connection_file = kernel.connection_file + # Test prohibit nested async context. + with pytest.raises(RuntimeError, match="Already started"): + async with kernel.start_in_context(): pass - utils.clear_kernel() - async with Kernel(connection_file=connection_file).start_in_context(): - # Test we can re-enter the kernel. + # Test prevents binding socket more than once. + with ( + pytest.raises(RuntimeError, match=".*is already loaded"), + kernel._bind_socket(SocketID.shell, None), # pyright: ignore[reportArgumentType, reportPrivateUsage] + ): pass - finally: - utils.clear_kernel() + async with Kernel(connection_file=connection_file).start_in_context(): + # Test we can re-enter the kernel. + pass diff --git a/tests/utils.py b/tests/utils.py index a7081c3fa..673453296 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,8 +11,6 @@ from jupyter_client.asynchronous.client import AsyncKernelClient import async_kernel.utils -from async_kernel import Caller, Kernel -from async_kernel.asyncshell import AsyncInteractiveShell from async_kernel.typing import ExecuteContent, MsgType from tests.references import RMessage, references @@ -33,15 +31,6 @@ class ExecuteContentType(TypedDict): stop_on_error: NotRequired[bool] -def clear_kernel(): - "Clear the kernel so it can be started fresh." - if kernel := Kernel._instance: # pyright: ignore[reportPrivateUsage] - kernel.stop() - Kernel._instance = None # pyright: ignore[reportPrivateUsage] - AsyncInteractiveShell.clear_instance() - Caller.stop_all() - - async def get_reply( client: AsyncKernelClient, msg_id: str, From 00f407b4fd2e41e0059917134dbb9eb25e76f63e Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Tue, 19 Aug 2025 13:21:45 +1000 Subject: [PATCH 12/18] Shift contextvars into utils notably, removing kernel.job. Add Kernel.concurrency_mode replacing get_handler_and_run_mode with get_run_mode and get_handler. Add more methods to utils. --- docs/notebooks/simple_example.ipynb | 9 +- src/async_kernel/asyncshell.py | 33 +-- src/async_kernel/caller.py | 28 +-- src/async_kernel/kernel.py | 314 +++++++++++++--------------- src/async_kernel/typing.py | 6 + src/async_kernel/utils.py | 57 ++++- tests/test_kernel.py | 7 +- 7 files changed, 236 insertions(+), 218 deletions(-) diff --git a/docs/notebooks/simple_example.ipynb b/docs/notebooks/simple_example.ipynb index b4e81efd0..eb7041864 100644 --- a/docs/notebooks/simple_example.ipynb +++ b/docs/notebooks/simple_example.ipynb @@ -40,9 +40,8 @@ "source": [ "import anyio\n", "import ipywidgets as ipw\n", - "import utils\n", "\n", - "from async_kernel import Caller\n", + "from async_kernel import Caller, utils\n", "\n", "\n", "async def demo():\n", @@ -53,7 +52,7 @@ " for i in range(1, 4):\n", " b.description = f\"Continue {i}\"\n", " event = anyio.Event()\n", - " b.on_click(lambda _: caller.call_soon(event.set)) # noqa: B023\n", + " b.on_click(lambda _: caller.call_soon(event.set)) # noqa: B023 # pyright: ignore[reportUnknownLambdaType]\n", " print(f\"Waiting {i}\", end=\"\\r\")\n", " await event.wait()\n", " b.close()\n", @@ -156,7 +155,9 @@ "slideshow": { "slide_type": "" }, - "tags": [] + "tags": [ + "suppress-error" + ] }, "outputs": [], "source": [ diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index 22d4a67e5..802fd5bb6 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -7,7 +7,6 @@ import json import pathlib import sys -from contextvars import ContextVar from typing import TYPE_CHECKING, Any, ClassVar, Literal import anyio @@ -22,6 +21,7 @@ from typing_extensions import override import async_kernel +from async_kernel import utils from async_kernel.caller import Caller from async_kernel.compiler import XCachingCompiler from async_kernel.typing import Content, Tags @@ -97,7 +97,7 @@ def publish( # pyright: ignore[reportIncompatibleMethodOverride] [Reference](https://jupyter-client.readthedocs.io/en/stable/messaging.html#update-display-data) """ - async_kernel.Kernel().iopub_send( + utils.get_kernel().iopub_send( msg_or_type="update_display_data" if update else "display_data", content={"data": data, "metadata": metadata or {}, "transient": transient or {}} | kwargs, ident=self.topic, @@ -112,7 +112,7 @@ def clear_output(self, wait: bool = False) -> None: instead waiting for the next display before clearing. This reduces bounce during repeated clear & display loops. """ - async_kernel.Kernel().iopub_send(msg_or_type="clear_output", content={"wait": wait}, ident=self.topic) + utils.get_kernel().iopub_send(msg_or_type="clear_output", content={"wait": wait}, ident=self.topic) class AsyncInteractiveShell(InteractiveShell): @@ -136,7 +136,7 @@ class AsyncInteractiveShell(InteractiveShell): compile: Instance[XCachingCompiler] user_ns_hidden = Dict() _main_mod_cache = Dict() - _execute_request_timeout: ContextVar[float | None] = ContextVar("execute_request_timeout", default=None) + run_cell = None # pyright: ignore[reportAssignmentType] "**not-supported**" should_run_async = None # pyright: ignore[reportAssignmentType] @@ -162,21 +162,7 @@ def _default_banner1(self) -> str: @property def kernel(self) -> Kernel: "The current kernel." - return async_kernel.Kernel() - - @property - def execute_request_timeout(self) -> float | None: - """A timeout in context of the [run_cell_async][async_kernel.asyncshell.AsyncInteractiveShell]. - - See also: - - - [async_kernel.typing.MetadataKeys.timeout][]. - """ - return self._execute_request_timeout.get() - - @execute_request_timeout.setter - def execute_request_timeout(self, value: float | None) -> None: - self._execute_request_timeout.set(value) + return utils.get_kernel() @observe("exit_now") def _update_exit_now(self, _) -> None: @@ -250,7 +236,7 @@ async def run_cell_async( This function runs [execute requests][async_kernel.Kernel.execute_request] for the kernel wrapping [InteractiveShell][IPython.core.interactiveshell.InteractiveShell.run_cell_async]. """ - with anyio.fail_after(delay=self.execute_request_timeout): + with anyio.fail_after(delay=utils.get_execute_request_timeout()): result: ExecutionResult = await super().run_cell_async( raw_cell=raw_cell, store_history=store_history, @@ -269,7 +255,7 @@ async def run_cell_async( def _showtraceback(self, etype, evalue, stb) -> None: if Tags.suppress_error in async_kernel.utils.get_tags(): return - if self.execute_request_timeout is not None and etype is self.kernel.CancelledError: + if utils.get_execute_request_timeout() is not None and etype is self.kernel.CancelledError: etype, evalue, stb = TimeoutError, "Cell execute timeout", [] self.kernel.iopub_send( msg_or_type="error", @@ -294,14 +280,11 @@ class KernelMagics(Magics): @line_magic def connect_info(self, _) -> None: """Print information for connecting other clients to this kernel.""" - - kernel = async_kernel.Kernel() + kernel = utils.get_kernel() connection_file = pathlib.Path(kernel.connection_file) - # if it's in the default dir, truncate to basename if jupyter_runtime_dir() == str(connection_file.parent): connection_file = connection_file.name - info = kernel.get_connection_info() print( json.dumps(info, indent=2, default=json_default), diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index 9a61b61b2..e0b54c3b7 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -238,8 +238,8 @@ class Caller: _pool_instances: ClassVar[weakref.WeakSet[Self]] = weakref.WeakSet() _executor_queue: dict _taskgroup: TaskGroup | None = None - _jobs: deque[tuple[contextvars.Context, tuple[Future, float, float, Callable, tuple, dict]] | Callable[[], Any]] - _jobs_added: threading.Event + _callers: deque[tuple[contextvars.Context, tuple[Future, float, float, Callable, tuple, dict]] | Callable[[], Any]] + _callers_added: threading.Event _stopped = False _protected = False _running = False @@ -292,8 +292,8 @@ def __new__( inst.backend = Backend(sniffio.current_async_library()) inst.thread = thread inst.log = log or logging.LoggerAdapter(logging.getLogger()) - inst._jobs = deque() - inst._jobs_added = threading.Event() + inst._callers = deque() + inst._callers_added = threading.Event() inst._protected = protected inst._executor_queue = {} cls._instances[thread] = inst @@ -325,8 +325,8 @@ async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> No self.iopub_sockets[self.thread] = socket task_status.started() while not self._stopped: - while len(self._jobs): - job = self._jobs.popleft() + while len(self._callers): + job = self._callers.popleft() if isinstance(job, Callable): try: job() @@ -335,11 +335,11 @@ async def _server_loop(self, tg: TaskGroup, task_status: TaskStatus[None]) -> No else: context, args = job context.run(tg.start_soon, self._wrap_call, *args) - self._jobs_added.clear() - await wait_thread_event(self._jobs_added) + self._callers_added.clear() + await wait_thread_event(self._callers_added) finally: self._running = False - for job in self._jobs: + for job in self._callers: if not callable(job): job[1][0].set_exception(FutureCancelledError()) socket.close() @@ -417,7 +417,7 @@ def stop(self, *, force=False) -> None: if self._protected and not force: return self._stopped = True - self._jobs_added.set() + self._callers_added.set() self._instances.pop(self.thread, None) if self in self._to_thread_pool: self._to_thread_pool.remove(self) @@ -439,8 +439,8 @@ def call_later( if threading.current_thread() is self.thread and (tg := self._taskgroup): tg.start_soon(self._wrap_call, fut, time.monotonic(), delay, func, args, kwargs) else: - self._jobs.append((contextvars.copy_context(), (fut, time.monotonic(), delay, func, args, kwargs))) - self._jobs_added.set() + self._callers.append((contextvars.copy_context(), (fut, time.monotonic(), delay, func, args, kwargs))) + self._callers_added.set() self._outstanding += 1 return fut @@ -462,8 +462,8 @@ def call_no_context(self, func: Callable[P, Any], /, *args: P.args, **kwargs: P. *args: Arguments to use with func. **kwargs: Keyword arguments to use with func. """ - self._jobs.append(functools.partial(func, *args, **kwargs)) - self._jobs_added.set() + self._callers.append(functools.partial(func, *args, **kwargs)) + self._callers_added.set() def has_execution_queue(self, func: Callable) -> bool: "Returns True if an execution queue exists for `func`." diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index fe29d3751..5ef56df8a 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -7,7 +7,6 @@ import atexit import builtins import contextlib -import contextvars import errno import functools import getpass @@ -20,10 +19,12 @@ import time import traceback import uuid +from collections.abc import Callable from contextlib import asynccontextmanager from logging import Logger, LoggerAdapter from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self +from types import CoroutineType +from typing import TYPE_CHECKING, Any, Literal, Self import anyio import sniffio @@ -48,7 +49,7 @@ ExecuteContent, HandlerType, Job, - MetadataKeys, + KernelConcurrencyMode, MsgType, NoValue, RunMode, @@ -58,7 +59,7 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Generator - from types import FrameType + from types import CoroutineType, FrameType from anyio.abc import TaskStatus from IPython.core.interactiveshell import ExecutionResult @@ -144,23 +145,22 @@ def _try_bind_socket(port: int): @functools.cache -def wrap_handler(run_handler: Callable[[HandlerType, Job]], handler: HandlerType) -> HandlerType: - """Wraps a handler function to be executed by a runner function. - - This function takes a runner function and a handler function as input and returns a new handler function. - The new handler function, when called, will execute the original handler function using the provided runner function. - This allows for customization of how handlers are executed, such as running them in a separate thread or process. - Args: - run_handler: A callable that takes a handler and a job as input and executes the handler with the job. - handler: The handler function to be wrapped. - Returns: - A new handler function that will execute the original handler function using the provided runner function. - """ +def _wrap_handler( + runner: Callable[[HandlerType, Job]], handler: HandlerType +) -> Callable[[Job], CoroutineType[Any, Any, None]]: + async def wrap_handler(job: Job) -> None: + """A cache of run handlers. + + Args: + runner: The function that calls and awaits the handler - async def queued_handler(job: Job) -> None: - await run_handler(handler, job) + Required by: - return queued_handler + - call[async_kernel.Caller.queue_call][] : The queue is on per-function (hash) basis. + """ + await runner(handler, job) + + return wrap_handler class KernelInterruptError(InterruptedError): @@ -216,9 +216,8 @@ class Kernel(ConnectionFileMixin): """ - _job_var: ClassVar[contextvars.ContextVar[Job]] = contextvars.ContextVar("job") - _execution_count_var: ClassVar[contextvars.ContextVar[int]] = contextvars.ContextVar("execution_count") _instance: Self | None = None + _initialised = False _interrupt_requested = False _last_interrupt_frame = None _stop_event = Instance(threading.Event, ()) @@ -227,6 +226,7 @@ class Kernel(ConnectionFileMixin): _sockets: Dict[SocketID, zmq.Socket] = Dict() _execution_count = Int(0) anyio_backend = UseEnum(Backend) + concurrency_mode = UseEnum(KernelConcurrencyMode) help_links = Tuple() quiet = CBool(True, help="Only send stdout/stderr to output stream") shell = Instance(AsyncInteractiveShell) @@ -251,44 +251,20 @@ def __new__(cls, **kwargs) -> Self: # noqa: ARG004 return instance def __init__(self, **kwargs) -> None: - if self.message_handlers: + if self._initialised: return # Only initialize once + self._initialised = True super().__init__(**kwargs) - self.message_handlers[SocketID.shell] = { - MsgType.kernel_info_request: (self.kernel_info_request, RunMode.direct), - MsgType.comm_info_request: (self.comm_info_request, RunMode.direct), - MsgType.execute_request: (self.execute_request, RunMode.queue), - MsgType.interrupt_request: (self.interrupt_request, RunMode.direct), - MsgType.complete_request: (self.complete_request, RunMode.thread), - MsgType.is_complete_request: (self.is_complete_request, RunMode.thread), - MsgType.inspect_request: (self.inspect_request, RunMode.thread), - MsgType.history_request: (self.history_request, RunMode.thread), - MsgType.comm_open: (self.comm_open, RunMode.direct), - MsgType.comm_msg: (self.comm_msg, RunMode.task), - MsgType.comm_close: (self.comm_close, RunMode.direct), - } - self.message_handlers[SocketID.control] = self.message_handlers[SocketID.shell] | { - MsgType.shutdown_request: (self.shutdown_request, RunMode.direct), - MsgType.debug_request: (self.debug_request, RunMode.task), - } sys.excepthook = self.excepthook sys.unraisablehook = self.unraisablehook signal.signal(signal.SIGINT, self._signal_handler) if not os.environ.get("MPLBACKEND"): os.environ["MPLBACKEND"] = "module://matplotlib_inline.backend_inline" - @property - def job(self) -> Job | dict: - "The job in context of the current coroutine." - try: - return self._job_var.get() - except LookupError: - return {} - @property def execution_count(self) -> int: "The execution count in context of the current coroutine, else the current value if there isn't one in context." - return self._execution_count_var.get(self._execution_count) + return utils.get_execution_count() @property def kernel_info(self) -> dict[str, str | dict[str, str | dict[str, str | int]] | Any | tuple[Any, ...] | bool]: @@ -354,7 +330,7 @@ def _default_session(self) -> Any: @default("shell") def _default_shell(self) -> AsyncInteractiveShell: - return AsyncInteractiveShell.instance(parent=self, kernel=self) + return AsyncInteractiveShell.instance(parent=self) @classmethod def stop(cls) -> None: @@ -542,67 +518,8 @@ def _bind_socket(self, socket_id: SocketID, socket: zmq.Socket) -> Generator[Non socket.close(linger=500) self._sockets.pop(socket_id) - def iopub_send( - self, - msg_or_type: dict[str, Any] | str, - content: Content | None = None, - metadata: dict[str, Any] | None = None, - parent: dict[str, Any] | None | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] - ident: bytes | list[bytes] | None = None, - buffers: list[bytes] | None = None, - ) -> None: - """Send a message on the zmq iopub socket.""" - if socket := Caller.iopub_sockets.get(thread := threading.current_thread()): - msg = self.session.send( - stream=socket, - msg_or_type=msg_or_type, - content=content, - metadata=metadata, - parent=parent if parent is not NoValue else self.job.get("msg"), # pyright: ignore[reportArgumentType] - ident=ident, - buffers=buffers, - ) - if msg: - self.log.debug( - "iopub_send: (thread=%s) msg_type:'%s', content: %s", thread.name, msg["msg_type"], msg["content"] - ) - else: - self.control_thread_caller.call_no_context( - self.iopub_send, - msg_or_type=msg_or_type, - content=content, - metadata=metadata, - parent=parent if parent is not NoValue else None, - ident=ident, - buffers=buffers, - ) - - def _publish_status(self, job: Job, status: Literal["busy", "idle"], /) -> None: - """send status (busy/idle) on IOPub""" - self.iopub_send( - msg_or_type="status", - content={"execution_state": status}, - parent=job["msg"], # type: ignore[call-arg] - ident=self.topic("status"), - ) - - def _send_reply(self, job: Job, content: dict | None = None, /) -> None: - """Send a reply to the job with the specified content.""" - content = content or {} - if "status" not in content: - content["status"] = "ok" - msg = self.session.send( - stream=job["socket"], - msg_or_type=job["msg"]["header"]["msg_type"].replace("request", "reply"), - content=content, - parent=job["msg"]["header"], # pyright: ignore[reportArgumentType] - ident=job["ident"], - ) - if msg: - self.log.debug("*** _send_reply %s*** %s", job["socket_id"], msg) - def _input_request(self, prompt: str, *, password=False) -> Any: - job = self.job + job = utils.get_job() if not job["msg"].get("content", {}).get("allow_stdin", False): msg = "Stdin is not allowed in this context!" raise StdinNotImplementedError(msg) @@ -625,10 +542,6 @@ def _input_request(self, prompt: str, *, password=False) -> Any: raise KernelInterruptError return self.session.recv(socket)[1]["content"]["value"] # pyright: ignore[reportOptionalSubscript] - def topic(self, topic) -> bytes: - """prefixed topic for IOPub messages""" - return (f"kernel.{topic}").encode() - async def _receive_msg_loop( self, socket_id: Literal[SocketID.control, SocketID.shell], *, task_status: TaskStatus[None] ) -> None: @@ -681,71 +594,141 @@ async def handle_message_request(self, job: Job, /) -> None: job: The packed [message][async_kernel.typing.Message] for handling. """ try: - handler, mode = await self.get_handler_and_run_mode(job) - except ValueError: + msg_type = MsgType(job["msg"]["header"]["msg_type"]) + socket_id = job["socket_id"] + handler = self.get_handler(socket_id, msg_type) + except (ValueError, TypeError): return - match mode: + run_mode = self.get_run_mode(socket_id, msg_type, self.concurrency_mode, job=job) + self.log.debug("%s %s run mode %s handler: %s", socket_id, msg_type, run_mode, handler) + job["run_mode"] = run_mode + runner = _wrap_handler(self.run_handler, handler) + match run_mode: case RunMode.queue: - await Caller().queue_call(handler, job) + await Caller().queue_call(runner, job) case RunMode.thread: - Caller.to_thread(handler, job) + Caller.to_thread(runner, job) case RunMode.task: - Caller().call_soon(handler, job) + Caller().call_soon(runner, job) case RunMode.direct: - await handler(job) + await runner(job) - async def get_handler_and_run_mode(self, job: Job) -> tuple[HandlerType, RunMode]: - """Determine the appropriate handler and run mode for a given job. - - This method retrieves the handler associated with the message type of the job, - and determines the run mode based on metadata, header information, and content - of the message. It also sets the 'run_mode' attribute of the job. - - Args: - job: The job dictionary containing message details and socket ID. - - Returns: - A tuple containing the wrapped handler function and the determined run mode. - - Raises: - ValueError: If a handler does not exist for the message type. - """ - msg_type: MsgType = job["msg"]["header"]["msg_type"] - if not (handler_mode := self.message_handlers[job["socket_id"]].get(msg_type)): - msg = f"A handler does not exist for {msg_type=}!" - raise ValueError(msg) - handler, mode = handler_mode - if mode_from_metadata := job["msg"]["metadata"].get("run_mode"): - mode = mode_from_metadata - if mode_from_header := job["msg"]["header"].get("run_mode"): - mode = mode_from_header - elif job["msg"]["header"]["msg_type"] == MsgType.execute_request: - content = job["msg"].get("content", {}) - if mode_ := RunMode.get_mode(content.get("code", "")): - mode = mode_ - elif content.get("silent", True) or (job["socket_id"] is SocketID.control): - mode = RunMode.task - else: - mode = RunMode.queue - job["run_mode"] = mode - self.log.debug("%s %s run mode %s for %s", job["socket_id"], mode, msg_type, handler) - return wrap_handler(self.run_handler, handler), mode - - async def run_handler(self, handler: HandlerType, job: Job) -> None: + def get_run_mode( + self, + socket_id: SocketID, + msg_type: MsgType, + concurrency_mode: KernelConcurrencyMode = KernelConcurrencyMode.default, + *, + job: Job | None = None, + ) -> RunMode: + # TODO: Are any of these options worth including? + # if mode_from_metadata := job["msg"]["metadata"].get("run_mode"): + # return RunMode( mode_from_metadata) + # if mode_from_header := job["msg"]["header"].get("run_mode"): + # return RunMode( mode_from_header) + match (concurrency_mode, socket_id, msg_type): + case KernelConcurrencyMode.direct, _, _: + return RunMode.direct + # Default + case _, SocketID.control, MsgType.execute_request: + return RunMode.task + case _, _, MsgType.execute_request: + if job: + if content := job["msg"].get("content", {}): + if (code := content.get("code")) and (mode_ := RunMode.get_mode(code)): + return mode_ + if content.get("silent"): + return RunMode.task + if mode_ := set(utils.get_tags(job)).intersection(RunMode): + return RunMode(next(iter(mode_))) + return RunMode.queue + case _, SocketID.shell, MsgType.shutdown_request | MsgType.debug_request: + msg = f"{msg_type=} not allowed on shell!" + raise ValueError(msg) + case _, SocketID.control, MsgType.debug_request: + return RunMode.task + case _, _, MsgType.inspect_request | MsgType.complete_request | MsgType.is_complete_request: + return RunMode.thread + case _, _, MsgType.history_request: + return RunMode.thread + case _: + return RunMode.direct + + def get_handler(self, socket_id: SocketID, msg_type: MsgType) -> HandlerType: + if not callable(f := getattr(self, msg_type, None)): + msg = "A handler was not found for " + raise TypeError(msg) + return f # pyright: ignore[reportReturnType] + + async def run_handler(self, handler: HandlerType, job: Job[dict]) -> None: """Runs the handler in the context of the job/message sending the reply content if it is provided. This method gets called for every valid request with the relevant handler. """ - self._job_var.set(job) + + def _send_reply(content: dict, /) -> None: + """Send a reply to the job with the specified content.""" + if "status" not in content: + content["status"] = "ok" + msg = self.session.send( + stream=job["socket"], + msg_or_type=job["msg"]["header"]["msg_type"].replace("request", "reply"), + content=content, + parent=job["msg"]["header"], # pyright: ignore[reportArgumentType] + ident=job["ident"], + ) + if msg: + self.log.debug("*** _send_reply %s*** %s", job["socket_id"], msg) + + utils._job_var.set(job) # pyright: ignore[reportPrivateUsage] try: - self._publish_status(job, "busy") + self.iopub_send(msg_or_type="status", content={"execution_state": "busy"}, ident=self.topic("status")) if (content := await handler(job)) is not None: - self._send_reply(job, content) + _send_reply(content) except Exception as e: - self._send_reply(job, error_to_content(e)) + _send_reply(error_to_content(e)) self.log.exception("Exception in message handler:", exc_info=e) finally: - self._publish_status(job, "idle") + self.iopub_send(msg_or_type="status", content={"execution_state": "idle"}, ident=self.topic("status")) + + def iopub_send( + self, + msg_or_type: dict[str, Any] | str, + content: Content | None = None, + metadata: dict[str, Any] | None = None, + parent: dict[str, Any] | None | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] + ident: bytes | list[bytes] | None = None, + buffers: list[bytes] | None = None, + ) -> None: + """Send a message on the zmq iopub socket.""" + if socket := Caller.iopub_sockets.get(thread := threading.current_thread()): + msg = self.session.send( + stream=socket, + msg_or_type=msg_or_type, + content=content, + metadata=metadata, + parent=parent if parent is not NoValue else utils.get_parent(), # pyright: ignore[reportArgumentType] + ident=ident, + buffers=buffers, + ) + if msg: + self.log.debug( + "iopub_send: (thread=%s) msg_type:'%s', content: %s", thread.name, msg["msg_type"], msg["content"] + ) + else: + self.control_thread_caller.call_no_context( + self.iopub_send, + msg_or_type=msg_or_type, + content=content, + metadata=metadata, + parent=parent if parent is not NoValue else None, + ident=ident, + buffers=buffers, + ) + + def topic(self, topic) -> bytes: + """prefixed topic for IOPub messages""" + return (f"kernel.{topic}").encode() async def kernel_info_request(self, job: Job[Content]) -> Content: """Handle a ke[rnel info request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info).""" @@ -777,8 +760,7 @@ async def execute_request(self, job: Job[ExecuteContent]) -> Content: metadata = job["msg"].get("metadata") or {} if not (silent := c["silent"]): self._execution_count += 1 - self._execution_count_var.set(self._execution_count) - self.shell.execute_request_timeout = metadata.get(MetadataKeys.timeout) or self.cell_execute_timeout + utils._execution_count_var.set(self._execution_count) # pyright: ignore[reportPrivateUsage] self.iopub_send( msg_or_type="execute_input", content={"code": c["code"], "execution_count": self.execution_count}, @@ -805,7 +787,7 @@ async def execute_request(self, job: Job[ExecuteContent]) -> Content: err = e if (err) and ( (Tags.suppress_error in metadata.get("tags", ())) # 1. - or (isinstance(err, self.CancelledError) and (self.shell.execute_request_timeout is not None)) # 2. + or (isinstance(err, self.CancelledError) and (utils.get_execute_request_timeout() is not None)) # 2. ): # Suppress the error due to either: # 1. tag diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 8148cc639..12c0b586f 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -17,6 +17,7 @@ __all__ = [ "DebugMessage", "Job", + "KernelConcurrencyMode", "Message", "MetadataKeys", "MsgHeader", @@ -106,6 +107,11 @@ def get_mode(cls, code: str) -> RunMode | None: return None +class KernelConcurrencyMode(enum.StrEnum): + default = "default" + direct = "direct" + + class MsgType(enum.StrEnum): """An enumeration of Message `msg_type` for [shell and control messages]( https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel). diff --git a/src/async_kernel/utils.py b/src/async_kernel/utils.py index c9023ce21..12646307d 100644 --- a/src/async_kernel/utils.py +++ b/src/async_kernel/utils.py @@ -6,19 +6,28 @@ import contextlib import sys import threading +from contextvars import ContextVar from typing import TYPE_CHECKING, Any import anyio import anyio.to_thread import async_kernel +from async_kernel.typing import Message, MetadataKeys if TYPE_CHECKING: from collections.abc import Mapping + from async_kernel.kernel import Kernel + from async_kernel.typing import Job + __all__ = [ "do_not_debug_this_thread", + "get_execute_request_timeout", + "get_execution_count", + "get_job", "get_metadata", + "get_parent", "get_tags", "mark_thread_pydev_do_not_trace", "wait_thread_event", @@ -26,6 +35,10 @@ LAUNCHED_BY_DEBUGPY = "debugpy" in sys.modules +_job_var = ContextVar("job") +_execution_count_var: ContextVar[int] = ContextVar("execution_count") +_execute_request_timeout: ContextVar[float | None] = ContextVar("execute_request_timeout", default=None) + def mark_thread_pydev_do_not_trace(thread: threading.Thread, name="", *, remove=False): """Modifies the given thread's attributes to hide or unhide it from the debugger (e.g., debugpy).""" @@ -62,11 +75,45 @@ def _in_thread_call(): event.set() -def get_metadata() -> Mapping[str, Any]: - "Gets metadata from current [`Job`][async_kernel.typing.Job] context if there is one." - return (async_kernel.Kernel().job.get("msg") or {}).get("metadata") or {} +def get_kernel() -> Kernel: + "Get the current kernel." + return async_kernel.Kernel() + + +def get_job() -> Job[dict] | dict: + "Get the job for the current context." + try: + return _job_var.get() + except Exception: + return {} + + +def get_parent(job: Job | None = None, /) -> Message[dict[str, Any]] | None: + "Get the [parent message]() for the current context." + return (job or get_job()).get("msg") + +def get_metadata(job: Job | None = None, /) -> Mapping[str, Any]: + "Gets [metadata]() for the current context." + return (job or get_job().get("msg") or {}).get("metadata") or {} -def get_tags() -> list[str]: - "Gets the list of tags from current [`Job`][async_kernel.typing.Job] context if there is one." + +def get_tags(job: Job | None = None, /) -> list[str]: + "Gets the [tags]() for the current context." return get_metadata().get("tags") or [] + + +def get_execute_request_timeout(job: Job | None = None, /) -> float | None: + "Gets the execute_request_timeout for the current context." + try: + if timeout := get_metadata(job).get(MetadataKeys.timeout): + return float(timeout) + return get_kernel().cell_execute_timeout + except Exception: + return None + + +def get_execution_count() -> int: + "Gets the execution count for the current context, defaults to the current kernel count." + + return _execution_count_var.get(None) or async_kernel.Kernel()._execution_count # pyright: ignore[reportPrivateUsage] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index d287162b5..bd9b97bcc 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -251,7 +251,7 @@ async def test_execute_request_error(client, code: str, run_mode: RunMode): async def test_execute_request_stop_on_error(client): - client.execute("import anyio;anyio.sleep(0.1);stop-here") + client.execute("import anyio;await anyio.sleep(0.1);stop-here") _, content = await utils.execute(client) assert content["evalue"] == "Aborting due to prior exception" @@ -528,7 +528,7 @@ async def test_invalid_message(client, channel): ("RunMode.direct", False, SocketID.shell, RunMode.direct), ], ) -async def test_get_run_mode_execute_request(kernel: Kernel, code: str, silent: bool, socket_id, expected: RunMode): +async def test_get_run_mode(kernel: Kernel, code: str, silent: bool, socket_id, expected: RunMode): content = ExecuteContent( code=code, silent=silent, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True ) @@ -536,6 +536,5 @@ async def test_get_run_mode_execute_request(kernel: Kernel, code: str, silent: b msg = Message(header=header, parent_header=header, metadata={}, buffers=[], content=content) socket = cast("zmq.Socket[Any]", None) # pyright: ignore[reportInvalidCast] job = Job(msg=msg, socket_id=socket_id, ident=[b""], socket=socket, received_time=0.0, run_mode=None) # pyright: ignore[reportArgumentType] - _, mode = await kernel.get_handler_and_run_mode(job) + mode = kernel.get_run_mode(socket_id, MsgType.execute_request, job=job) assert mode is expected - assert job.get("run_mode") is expected From 3e33b682deaa867a7654d472a57fa5af20317e9d Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 10:37:24 +1000 Subject: [PATCH 13/18] Added all_concurrency_run_modes to Kernel. Rename 'direct' to 'blocking' and improve some documentation. Print some message when suppressing the error; added a metatakey 'suppress_error_message'. Added a sleep(0) to _stop_on_error_time to clear any waiting messages. --- .vscode/settings.json | 4 +- CONTRIBUTING.md | 8 +- README.md | 14 +- docs/about/index.md | 6 + docs/index.md | 6 + docs/notebooks/index.md | 18 ++ docs/notebooks/overview.md | 9 - docs/notebooks/run_mode.ipynb | 200 +++++++++++++---------- docs/notebooks/simple_example.ipynb | 2 +- docs/reference/asyncshell.md | 2 - docs/reference/{overview.md => index.md} | 9 +- docs/reference/kernel.md | 2 - docs/reference/typing.md | 1 - docs/stylesheets/extra.css | 2 +- docs/usage.md | 21 --- mkdocs.yml | 39 ++--- pyproject.toml | 3 +- src/async_kernel/asyncshell.py | 7 +- src/async_kernel/caller.py | 9 +- src/async_kernel/kernel.py | 81 +++++++-- src/async_kernel/typing.py | 119 ++++++++++---- src/async_kernel/utils.py | 4 +- tests/conftest.py | 12 ++ tests/test_kernel.py | 62 ++++--- tests/test_typing.py | 29 +++- tests/test_utils.py | 57 +++++++ 26 files changed, 475 insertions(+), 251 deletions(-) create mode 100644 docs/about/index.md create mode 100644 docs/notebooks/index.md delete mode 100644 docs/notebooks/overview.md rename docs/reference/{overview.md => index.md} (51%) delete mode 100644 docs/usage.md create mode 100644 tests/test_utils.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 8062834ae..d4171cbc7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,7 @@ "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" - ] + ], + "spellright.language": ["en"], + "spellright.documentTypes": ["markdown", "latex", "plaintext", "python"] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 203750acf..c984248ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,16 +30,16 @@ pytest -vv --cov ## Code Styling -`Async kernel` uses ruff for code formatting. -the pre-commit hook should take care of how it should look. -To install `pre-commit`, run the following:: +`Async kernel` uses ruff for code formatting. The pre-commit hook should take care of how it should look. + +To install `pre-commit`, run the following: ```shell pip install pre-commit pre-commit install ``` -You can invoke the pre-commit hook by hand at any time with:: +You can invoke the pre-commit hook by hand at any time with: ```shell pre-commit run diff --git a/README.md b/README.md index 42381250a..6f3309efe 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,10 @@ Async kernel is a Python [Jupyter kernel](https://docs.jupyter.org/en/latest/pro ## Highlights -- Asynchronous -- Comms is not blocked during cell execution[^non-blocking-execution] -- Concurrent code execution [^run-concurrent] +- Concurrent message handling - [Debugger client](https://jupyterlab.readthedocs.io/en/latest/user/debugger.html#debugger) - Configurable backend - "asyncio" (default) or "trio backend" [^config-backend] - [IPython](https://pypi.org/project/ipython/) shell for magic, code completions, and history -- No tornado - instead using anyio's [`wait_readable`](https://anyio.readthedocs.io/en/stable/api.html#anyio.wait_readable) to wait for incoming messages on zmq sockets ## Installation @@ -38,13 +35,4 @@ async-kernel -a async-trio [![Link to demo](https://github.com/user-attachments/assets/9a4935ba-6af8-4c9f-bc67-b256be368811)](https://fleming79.github.io/async-kernel/simple_example/ "Show demo notebook.") -[^non-blocking-execution]: Shell messaging runs in a task separate to execute requests in the main thread. This means shell messages (including comms) can pass freely whilst an execute request is busy awaiting a result. - -[^run-concurrent]: Code can also be scheduled for concurrent execution by adding `##task` or `##thread` at the top of the cell. - - Works with concurrent cell execution - - - [x] Jupyterlab - - [ ] VS code - runs one cell at a time - [^config-backend]: The default backend is 'asyncio'. To add a 'trio' backend, define a KernelSpec with a kernel name that includes trio in it. diff --git a/docs/about/index.md b/docs/about/index.md new file mode 100644 index 000000000..ee55d0aa8 --- /dev/null +++ b/docs/about/index.md @@ -0,0 +1,6 @@ +--- +title: About +description: Notes about contributing, changelog and development. +icon: +# subtitle: A sub title +--- diff --git a/docs/index.md b/docs/index.md index 612c7a5e0..9e2674d63 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,7 @@ +--- +title: Reference +description: Readme. +icon: material/home +--- + --8<-- "README.md" diff --git a/docs/notebooks/index.md b/docs/notebooks/index.md new file mode 100644 index 000000000..eb8aa900f --- /dev/null +++ b/docs/notebooks/index.md @@ -0,0 +1,18 @@ +--- +title: Notebooks +description: Example notebooks for async kernel. +icon: material/note-text +# subtitle: A sub title +--- + +# Notebooks + +Notebooks in this documentation show the result of each cell after executing for a short duration (~100ms). + +You can download the notebook with the button at the top right of the page for the notebook. + +!!! note + + The [suppress-error][async_kernel.typing.Tags.suppress_error] tag is inserted in code cells to enable + with generating documentation. The symbol '⚠' is an indicator that the error was suppressed. Normally + this is due to the timeout but there is no distinction on the type of error. diff --git a/docs/notebooks/overview.md b/docs/notebooks/overview.md deleted file mode 100644 index caa18e878..000000000 --- a/docs/notebooks/overview.md +++ /dev/null @@ -1,9 +0,0 @@ -# Overview - -Notebooks in this documentation show the result of each cell after executing for a short duration (~100ms). - -You can download the notebook with the button at the top right of the page for the notebook. - -!!! note - - \[`suppress-error`\][async_kernel.typing.Tags.suppress_error] error tags are used with generating documentation. diff --git a/docs/notebooks/run_mode.ipynb b/docs/notebooks/run_mode.ipynb index 367cd5fab..8d4a561c0 100644 --- a/docs/notebooks/run_mode.ipynb +++ b/docs/notebooks/run_mode.ipynb @@ -11,11 +11,32 @@ "tags": [] }, "source": [ - "# Run mode\n", + "# Concurrency\n", "\n", - "RunMode is an enumeration of run modes supported by the Kernel. \n", + "Async kernel enables concurrent execution through the `Caller` class. \n", + "It provides anyio compatible methods and classmethods for code execution \n", + "in threads and asynchronous event loops. \n", "\n", - "The kernel is configured for different run modes depending on the execute request." + "\n", + "## Concurrency mode\n", + "\n", + "Every message request received by the kernel is run with a message handler using one of the run modes [listed below](#run-mode).\n", + "The run mode is decided by the kernel method `get_run_mode`.\n", + "\n", + "## Run mode\n", + "\n", + "- blocking: Run directly blocking any messages\n", + "- queue: Run in a queue\n", + "- task: Run in the task\n", + "- thread: Run in a Caller thread\n", + "\n", + "\n", + "## `Kernel.get_run_mode`\n", + "\n", + "The kernel method `get_run_mode` takes channel, message type and `concurrency_mode` as arguments and returns run mode.\n", + "\n", + "`concurrency_mode` is an option argument, defaulting to the kernel.concurrency_mode. In this way it is possible to obtain\n", + "the current run mode for a message." ] }, { @@ -31,27 +52,51 @@ }, "outputs": [], "source": [ - "from async_kernel import Kernel\n", + "from async_kernel import utils\n", + "from async_kernel.typing import KernelConcurrencyMode, MsgType, RunMode, SocketID\n", "\n", - "for ch in [\"shell\", \"control\"]:\n", - " print(f\"**{ch}**\")\n", - " print(\"-- Run mode ------ MsgType ---------\")\n", - " handlers = Kernel().message_handlers[ch]\n", - " for k in handlers:\n", - " print(f\"{handlers[k][1]} \\t----\", k)" + "kernel = utils.get_kernel()\n", + "\n", + "kernel.get_run_mode(SocketID.shell, MsgType.comm_msg)" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "If the `kernel.concurrency_mode` is changed, the run mode for a particluar `msg_type` might change." ] }, { "cell_type": "code", "execution_count": null, - "id": "2", - "metadata": {}, + "id": "3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], - "source": [] + "source": [ + "kernel.concurrency_mode = KernelConcurrencyMode.blocking\n", + "print(kernel.concurrency_mode, kernel.get_run_mode(SocketID.shell, MsgType.comm_msg))\n", + "\n", + "kernel.concurrency_mode = KernelConcurrencyMode.default\n", + "print(kernel.concurrency_mode, kernel.get_run_mode(SocketID.shell, MsgType.comm_msg))" + ] }, { "cell_type": "markdown", - "id": "3", + "id": "4", "metadata": { "editable": true, "slideshow": { @@ -60,16 +105,41 @@ "tags": [] }, "source": [ - "The user can also override the run mode for a cell in a number of ways:\n", + "Below is a list of the run modes for the currently available concurrency modes.\n", "\n", - "- Metadata\n", - "- [Directly in code](#code-directive)\n", - "- Message header (in custom messages)" + "!!! note\n", + "\n", + " `blocking` mode is equivalent to how IpyKernel < 7.0 operates. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "data = kernel.all_concurrency_run_modes()\n", + "try:\n", + " import pandas as pd\n", + "except ImportError:\n", + " print(data)\n", + "else:\n", + " data = pd.DataFrame(data)\n", + " data[\"RunMode\"] = data.RunMode.str.replace(\"##\", \"\")\n", + " data = data.pivot(index=\"MsgType\", columns=[\"KernelConcurrencyMode\", \"SocketID\"], values=\"RunMode\") # noqa: PD010\n", + " display(data)" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": { "editable": true, "slideshow": { @@ -78,7 +148,18 @@ "tags": [] }, "source": [ - "## Code directive\n", + "## Execute request run mode\n", + "\n", + "There are a few options to modify how code cells are run.\n", + "\n", + "- Metadata\n", + "- [Directly in code]\n", + "- tag\n", + "- Message header (in custom messages)\n", + "!!! Warning\n", + "\n", + " Only Jupyter lab is known to allow concurrent execution of cells.\n", + "\n", "### Code for example\n", "\n", "- **This example requires ipywidgets**\n", @@ -90,7 +171,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": { "editable": true, "slideshow": { @@ -106,9 +187,9 @@ " import anyio\n", " from ipywidgets import Button\n", "\n", - " from async_kernel import Caller, Kernel\n", + " from async_kernel import Caller, utils\n", "\n", - " print(\"Run mode:\", Kernel().job[\"run_mode\"])\n", + " print(\"Run mode:\", utils.get_job()[\"run_mode\"])\n", "\n", " print(f\"Thread name: '{threading.current_thread().name}'\")\n", " button = Button(description=\"Finish\")\n", @@ -125,7 +206,7 @@ }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": { "editable": true, "slideshow": { @@ -140,7 +221,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": { "editable": true, "slideshow": { @@ -158,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "10", "metadata": { "editable": true, "slideshow": { @@ -177,7 +258,7 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "11", "metadata": { "editable": true, "slideshow": { @@ -200,7 +281,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "12", "metadata": { "editable": true, "slideshow": { @@ -212,13 +293,13 @@ }, "outputs": [], "source": [ - "##task\n", + "RunMode.task # noqa: B018 # Using the literal `RunMode` values directly is also possible. Though it may show up as a [Flake8 B018 issue](https://docs.astral.sh/ruff/rules/useless-expression/)\n", "await demo()" ] }, { "cell_type": "markdown", - "id": "11", + "id": "13", "metadata": { "editable": true, "slideshow": { @@ -237,26 +318,27 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", "metadata": { "editable": true, "slideshow": { "slide_type": "" }, "tags": [ + "thread", "suppress-error" ] }, "outputs": [], "source": [ - "##thread\n", + "# This time we'll use the tag to run the cell in a Thread\n", "await demo()" ] }, { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": { "editable": true, "slideshow": { @@ -269,58 +351,6 @@ "##thread\n", "%callers # magic provided by async kernel" ] - }, - { - "cell_type": "markdown", - "id": "14", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "### Direct use symbol\n", - "\n", - "Using the literal `RunMode` values directly is also possible. Though it may show up as a [Flake8 B018 issue](https://docs.astral.sh/ruff/rules/useless-expression/)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "from async_kernel.typing import RunMode" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [ - "suppress-error" - ] - }, - "outputs": [], - "source": [ - "RunMode.task # noqa: B018\n", - "\n", - "await demo()" - ] } ], "metadata": { diff --git a/docs/notebooks/simple_example.ipynb b/docs/notebooks/simple_example.ipynb index eb7041864..59469f240 100644 --- a/docs/notebooks/simple_example.ipynb +++ b/docs/notebooks/simple_example.ipynb @@ -182,7 +182,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13.final.0" + "version": "3.11.13" } }, "nbformat": 4, diff --git a/docs/reference/asyncshell.md b/docs/reference/asyncshell.md index 1f96cc084..96f9ba40d 100644 --- a/docs/reference/asyncshell.md +++ b/docs/reference/asyncshell.md @@ -1,3 +1 @@ -# Shell module - ::: async_kernel.asyncshell diff --git a/docs/reference/overview.md b/docs/reference/index.md similarity index 51% rename from docs/reference/overview.md rename to docs/reference/index.md index 5c98646d4..00cc8cbbc 100644 --- a/docs/reference/overview.md +++ b/docs/reference/index.md @@ -1,4 +1,11 @@ -# Overview +--- +title: Reference +description: API reference for async kernel. +# icon: material/ +# subtitle: A sub title +--- + +# Reference The reference section provides documentation for each module in async kernel. diff --git a/docs/reference/kernel.md b/docs/reference/kernel.md index f054c9683..d706ef4d2 100644 --- a/docs/reference/kernel.md +++ b/docs/reference/kernel.md @@ -1,3 +1 @@ -# Kernel module - ::: async_kernel.kernel diff --git a/docs/reference/typing.md b/docs/reference/typing.md index bcaeab673..2d32fd935 100644 --- a/docs/reference/typing.md +++ b/docs/reference/typing.md @@ -1,2 +1 @@ ::: async_kernel.typing - diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index fcb71554a..ca42bcab2 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,3 +1,3 @@ .md-grid { - max-width: 1200px; + max-width: 1440px; } diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index e68af76a8..000000000 --- a/docs/usage.md +++ /dev/null @@ -1,21 +0,0 @@ -# Usage - -Async kernel is asynchronous by design, shell messaging runs in a loop in the main thread - -## Tips - -
- -- Use [async_kernel.Caller.call_soon][] or [async_kernel.Caller.call_later][] to run code in tasks to support either backend.(1) -- Use [anyio](https://anyio.readthedocs.io) or async functions corresponding to the anyio backend(2) freely in the main thread. -- Use [async_kernel.Caller.start_new][] to start a thread with the opposite backend. -- Start a new Caller if there are functions require the opposite asynchronous backend. - -
- -1. Caller provides methods for thread safe scheduling and awaiting the result using [Futures][async_kernel.caller.Future] - - 1. Use `Caller.get_instance()` to get the `Caller` for the main thread. - 1. Use `Caller()` to get the `Caller` for the current thread. - -1. Async-kernel runs in anyio. diff --git a/mkdocs.yml b/mkdocs.yml index 200e6d905..d4a889181 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,19 +29,18 @@ theme: - content.action.edit - content.action.view - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#anchor-following - - toc.integrate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#navigation-integration + # - toc.integrate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#navigation-integration - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#back-to-top-button - header.autohide # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/#automatic-hiding - navigation.footer # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-footer/#navigation - search.suggest # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-suggestions - search.highlight # https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/?h=search#search-highlighting - navigation.instant # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#instant-loading + - navigation.indexes # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#section-index-pages icon: annotation: material/information-outline - font: - text: Noto Sans Cham - code: Noto Sans Mono + font: false palette: - media: "(prefers-color-scheme)" toggle: @@ -101,7 +100,7 @@ plugins: filters: public heading_level: 2 inherited_members: true - line_length: 120 + line_length: 90 merge_init_into_class: true parameter_headings: true # preload_modules: [mkdocstrings] @@ -152,27 +151,21 @@ markdown_extensions: nav: - Home: index.md - Usage: - - usage.md - Notebooks: - - Overview: notebooks/overview.md - - Simple example: notebooks/simple_example.ipynb - - Execute mode: notebooks/run_mode.ipynb - - Caller notebook: notebooks/caller.ipynb + - notebooks/index.md + - notebooks/simple_example.ipynb + - notebooks/run_mode.ipynb + - notebooks/caller.ipynb - Commands: commands.md - Reference: - - reference/overview.md - - Caller & Future: - - reference/caller.md - - Command prompt: - - reference/main.md - - Kernel: - - reference/kernel.md - - Async shell: - - reference/asyncshell.md - - Types: - - reference/typing.md - - Utils: - - reference/utils.md + - reference/index.md + - reference/caller.md + - reference/main.md + - reference/kernel.md + - reference/asyncshell.md + - reference/typing.md + - reference/utils.md + - About: - Contributing: contributing.md - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index 1829de5c3..15bccf691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ docs = ["mkdocs-material", "mkdocs-open-in-new-tab", "mkdocs-git-revision-date-localized-plugin", "jupyterlab", - "ipywidgets" + "ipywidgets", + "pandas" ] dev = [ "debugpy", diff --git a/src/async_kernel/asyncshell.py b/src/async_kernel/asyncshell.py index 802fd5bb6..a97b1c1fc 100644 --- a/src/async_kernel/asyncshell.py +++ b/src/async_kernel/asyncshell.py @@ -20,11 +20,10 @@ from traitlets import Dict, Instance, Type, default, observe from typing_extensions import override -import async_kernel from async_kernel import utils from async_kernel.caller import Caller from async_kernel.compiler import XCachingCompiler -from async_kernel.typing import Content, Tags +from async_kernel.typing import Content, MetadataKeys, Tags if TYPE_CHECKING: from async_kernel.kernel import Kernel @@ -253,7 +252,9 @@ async def run_cell_async( @override def _showtraceback(self, etype, evalue, stb) -> None: - if Tags.suppress_error in async_kernel.utils.get_tags(): + if Tags.suppress_error in utils.get_tags(): + if msg := utils.get_metadata().get(MetadataKeys.suppress_error_message, "⚠"): + print(msg) return if utils.get_execute_request_timeout() is not None and etype is self.kernel.CancelledError: etype, evalue, stb = TimeoutError, "Cell execute timeout", [] diff --git a/src/async_kernel/caller.py b/src/async_kernel/caller.py index e0b54c3b7..e5a7583b2 100644 --- a/src/async_kernel/caller.py +++ b/src/async_kernel/caller.py @@ -257,8 +257,8 @@ def __new__( *, thread: threading.Thread | None = None, log: logging.LoggerAdapter | None = None, - create=False, - protected=False, + create: bool = False, + protected: bool = False, ) -> Self: """Create the `Caller` instance for the current thread or retrieve an existing instance by passing the thread. @@ -638,7 +638,7 @@ async def as_completed( *, max_concurrent: NoValue | int = NoValue, # pyright: ignore[reportInvalidTypeForm] ) -> AsyncGenerator[Future[T], Any]: - """An iterator to get Futures as they complete. + """An iterator to get [Futures][async_kernel.caller.Future] as they complete. Args: items: Either a container with existing futures or generator of Futures. @@ -646,7 +646,8 @@ async def as_completed( This is useful when `items` is a generator utilising Caller.to_thread. By default this will limit to `Caller.MAX_IDLE_POOL_INSTANCES`. - !!! Tip: + !!! tip + 1. Pass a generator should you wish to limit the number future jobs when calling to_thread/to_task etc. 2. Pass a set/list/tuple to ensure all get monitored at once. """ diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 5ef56df8a..7973b565a 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -11,6 +11,7 @@ import functools import getpass import logging +import math import os import pathlib import signal @@ -226,21 +227,30 @@ class Kernel(ConnectionFileMixin): _sockets: Dict[SocketID, zmq.Socket] = Dict() _execution_count = Int(0) anyio_backend = UseEnum(Backend) + "" concurrency_mode = UseEnum(KernelConcurrencyMode) + """The mode to use when getting the run mode for running the handler of a message request. + + See also: + - [async_kernel.Kernel.handle_message_request][] + """ help_links = Tuple() + "" quiet = CBool(True, help="Only send stdout/stderr to output stream") + "" shell = Instance(AsyncInteractiveShell) + "" session = Instance(Session) + "" log = Instance(logging.LoggerAdapter) + "" debugger = Instance(Debugger, ()) + "" comm_manager: Instance[CommManager] = Instance("async_kernel.comm.CommManager") + "" transport: CaselessStrEnum[str] = CaselessStrEnum( ["tcp", "ipc"] if sys.platform == "linux" else ["tcp"], default_value="tcp", config=True ) - message_handlers: Dict[Literal[SocketID.shell, SocketID.control], dict[MsgType, tuple[HandlerType, RunMode]]] = ( - Dict(read_only=True) - ) - "The message handlers for message requests (see also [async_kernel.Kernel.get_handler_and_run_mode][])." cell_execute_timeout = Float(None, allow_none=True) "A default timeout to apply to use for non-silent [execute requests][async_kernel.Kernel.execute_request]." @@ -599,7 +609,7 @@ async def handle_message_request(self, job: Job, /) -> None: handler = self.get_handler(socket_id, msg_type) except (ValueError, TypeError): return - run_mode = self.get_run_mode(socket_id, msg_type, self.concurrency_mode, job=job) + run_mode = self.get_run_mode(socket_id, msg_type, job=job) self.log.debug("%s %s run mode %s handler: %s", socket_id, msg_type, run_mode, handler) job["run_mode"] = run_mode runner = _wrap_handler(self.run_handler, handler) @@ -610,26 +620,43 @@ async def handle_message_request(self, job: Job, /) -> None: Caller.to_thread(runner, job) case RunMode.task: Caller().call_soon(runner, job) - case RunMode.direct: + case RunMode.blocking: await runner(job) def get_run_mode( self, socket_id: SocketID, msg_type: MsgType, - concurrency_mode: KernelConcurrencyMode = KernelConcurrencyMode.default, *, + concurrency_mode: KernelConcurrencyMode | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] job: Job | None = None, ) -> RunMode: + """Determine the run mode for a given channel, message type and concurrency mode. + + The run mode determines how the kernel will execute the message. + + Args: + socket_id: The socket ID the message was received on. + msg_type: The type of the message. + concurrency_mode: The concurrency mode of the kernel. Defaults to [kernel.concurrency_mode][async_kernel.Kernel.concurrency_mode] + job: The job associated with the message, if any. + + Returns: + The run mode for the message. + + Raises: + ValueError: If a shutdown or debug request is received on the shell socket. + """ + + concurrency_mode = self.concurrency_mode if concurrency_mode is NoValue else concurrency_mode # TODO: Are any of these options worth including? # if mode_from_metadata := job["msg"]["metadata"].get("run_mode"): # return RunMode( mode_from_metadata) # if mode_from_header := job["msg"]["header"].get("run_mode"): # return RunMode( mode_from_header) match (concurrency_mode, socket_id, msg_type): - case KernelConcurrencyMode.direct, _, _: - return RunMode.direct - # Default + case KernelConcurrencyMode.blocking, _, _: + return RunMode.blocking case _, SocketID.control, MsgType.execute_request: return RunMode.task case _, _, MsgType.execute_request: @@ -645,14 +672,34 @@ def get_run_mode( case _, SocketID.shell, MsgType.shutdown_request | MsgType.debug_request: msg = f"{msg_type=} not allowed on shell!" raise ValueError(msg) - case _, SocketID.control, MsgType.debug_request: - return RunMode.task case _, _, MsgType.inspect_request | MsgType.complete_request | MsgType.is_complete_request: return RunMode.thread case _, _, MsgType.history_request: return RunMode.thread + case _, _, MsgType.kernel_info_request | MsgType.comm_info_request | MsgType.comm_open | MsgType.comm_close: + return RunMode.blocking case _: - return RunMode.direct + return RunMode.task + + def all_concurrency_run_modes( + self, + ) -> dict[ + Literal["SocketID", "KernelConcurrencyMode", "MsgType", "RunMode"], + tuple[SocketID, KernelConcurrencyMode, MsgType, RunMode | None], + ]: + """Generates a dictionary containing all combinations of SocketID, KernelConcurrencyMode, and MsgType, + along with their corresponding RunMode (if available).""" + data: list[Any] = [] + for socket_id in [SocketID.shell, SocketID.control]: + for concurrency_mode in KernelConcurrencyMode: + for msg_type in MsgType: + try: + mode = self.get_run_mode(socket_id, msg_type, concurrency_mode=concurrency_mode) + except ValueError: + mode = None + data.append((socket_id, concurrency_mode, msg_type, mode)) + data_ = zip(*data, strict=True) + return dict(zip(["SocketID", "KernelConcurrencyMode", "MsgType", "RunMode"], data_, strict=True)) def get_handler(self, socket_id: SocketID, msg_type: MsgType) -> HandlerType: if not callable(f := getattr(self, msg_type, None)): @@ -801,8 +848,12 @@ async def execute_request(self, job: Job[ExecuteContent]) -> Content: if err: content |= error_to_content(err) if (not silent) and c.get("stop_on_error"): - self._stop_on_error_time = time.monotonic() - self.log.info("An error occurred in a non-silent execution request") + try: + self._stop_on_error_time = math.inf + self.log.info("An error occurred in a non-silent execution request") + await anyio.sleep(0) + finally: + self._stop_on_error_time = time.monotonic() return content async def complete_request(self, job: Job[Content]) -> Content: diff --git a/src/async_kernel/typing.py b/src/async_kernel/typing.py index 12c0b586f..8cf9cdc7e 100644 --- a/src/async_kernel/typing.py +++ b/src/async_kernel/typing.py @@ -55,9 +55,13 @@ class RunMode(enum.StrEnum): """An Enum of the [kernel run modes][async_kernel.Kernel.handle_message_request] available for altering how message requests are run. - !!! note "Prefix '##'" + !!! hint "String match options" - Run mode is matched for both mode without the prefix ("##"). + Each of these options will give a match. + + - `` + - `<##value>` + - '`RunMode.`. !!! note "special usage" @@ -67,22 +71,6 @@ class RunMode(enum.StrEnum): "The prefix for each run mode." - queue = "queue" - "The message for the [handler][async_kernel.typing.MsgType] is run sequentially with other messages that are queued." - task = "task" - "The message for the [handler][async_kernel.typing.MsgType] are run concurrently in task (starting immediately)." - thread = "thread" - "Messages for the [handler][async_kernel.typing.MsgType] are run concurrently in a thread (starting immediately)." - direct = "direct" - """Run the handler directly as soon as it is received. - - !!! warning - - **This mode blocks the message loop.** - - Use this for short running messages that should be processed as soon as it is received. - """ - @override def __str__(self): return f"##{self.name}" @@ -106,47 +94,80 @@ def get_mode(cls, code: str) -> RunMode | None: except ValueError: return None + queue = "queue" + "The message for the [handler][async_kernel.typing.MsgType] is run sequentially with other messages that are queued." + + task = "task" + "The message for the [handler][async_kernel.typing.MsgType] are run concurrently in task (starting immediately)." + + thread = "thread" + "Messages for the [handler][async_kernel.typing.MsgType] are run concurrently in a thread (starting immediately)." + + blocking = "blocking" + """Run the handler directly as soon as it is received. + + !!! warning + + **This mode blocks the message loop.** + + Use this for short running messages that should be processed as soon as it is received. + """ + class KernelConcurrencyMode(enum.StrEnum): + "" + default = "default" - direct = "direct" + "The default concurrency mode" + blocking = "blocking" + "All handlers are run with the [blocking][async_kernel.typing.RunMode.blocking]." class MsgType(enum.StrEnum): """An enumeration of Message `msg_type` for [shell and control messages]( https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-shell-router-dealer-channel). - - - [Control channel](https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-control-router-dealer-channel) only + Some message types are on the [control channel](https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-control-router-dealer-channel) only. """ kernel_info_request = "kernel_info_request" "[async_kernel.Kernel.kernel_info_request][]" + comm_info_request = "comm_info_request" "[async_kernel.Kernel.comm_info_request][]" + execute_request = "execute_request" "[async_kernel.Kernel.execute_request][]" + complete_request = "complete_request" "[async_kernel.Kernel.complete_request][]" + is_complete_request = "is_complete_request" "[async_kernel.Kernel.is_complete_request][]" + inspect_request = "inspect_request" "[async_kernel.Kernel.inspect_request][]" + history_request = "history_request" "[async_kernel.Kernel.history_request][]" + comm_open = "comm_open" "[async_kernel.Kernel.comm_open][]" + comm_msg = "comm_msg" "[async_kernel.Kernel.comm_msg][]" + comm_close = "comm_close" "[async_kernel.Kernel.comm_close][]" + # Control interrupt_request = "interrupt_request" - "[async_kernel.Kernel.interrupt_request][]" + "[async_kernel.Kernel.interrupt_request][] (control channel only)" + shutdown_request = "shutdown_request" - "[async_kernel.Kernel.shutdown_request][]" + "[async_kernel.Kernel.shutdown_request][] (control channel only)" + debug_request = "debug_request" - "[async_kernel.Kernel.debug_request][]" + "[async_kernel.Kernel.debug_request][] (control channel only)" class MetadataKeys(enum.StrEnum): @@ -157,6 +178,14 @@ class MetadataKeys(enum.StrEnum): Metadata can be edited in Jupyter lab "Advanced tools" and Tags can be added using "common tools" in the [right side bar](https://jupyterlab.readthedocs.io/en/stable/user/interface.html#left-and-right-sidebar). """ + @override + def __eq__(self, value: object, /) -> bool: + return str(value) in (self.name, str(self)) + + @override + def __hash__(self) -> int: + return hash(self.name) + tags = "tags" """The `tags` metadata key corresponds to is a list of strings. @@ -168,13 +197,35 @@ class MetadataKeys(enum.StrEnum): The value should be a floating point value of the timeout in seconds. """ + suppress_error_message = "suppress-error-message" + """A message to print when the error has been suppressed using [async_kernel.typing.Tags.suppress_error][]. + + ???+ note + + The default message is '⚠'. + """ class Tags(enum.StrEnum): - """Tags recognised by the kernel""" + """Tags recognised by the kernel. + + ??? info + Tags are can be added per cell. + + - Jupyter: via the [right side bar](https://jupyterlab.readthedocs.io/en/stable/user/interface.html#left-and-right-sidebar). + - VScode: via [Jupyter variables explorer](https://code.visualstudio.com/docs/python/jupyter-support-py#_variables-explorer-and-data-viewer)/ + """ + + @override + def __eq__(self, value: object, /) -> bool: + return str(value) in (self.name, str(self)) + + @override + def __hash__(self) -> int: + return hash(self.name) suppress_error = "suppress-error" - """Suppress exceptions for the code code cell. + """Suppress exceptions that occur during execution of the code cell. !!! note "Warning" @@ -204,10 +255,13 @@ class Message(TypedDict, Generic[T]): header: MsgHeader "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header)" + parent_header: MsgHeader "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#parent-header)" + metadata: Mapping[MetadataKeys | str, Any] "[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata)" + content: T | Content """[ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#metadata) @@ -232,6 +286,7 @@ class Job(TypedDict, Generic[T]): "" received_time: float "The time the message was received." + run_mode: RunMode """The run mode.""" @@ -242,15 +297,15 @@ class ExecuteContent(TypedDict): code: str "The code to execute." silent: bool - "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." + "" store_history: bool - "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." + "" user_expressions: dict[str, str] - "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." + "" allow_stdin: bool - "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." + "" stop_on_error: bool - "See [Ref](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute)." + "" DebugMessage = dict[str, Any] diff --git a/src/async_kernel/utils.py b/src/async_kernel/utils.py index 12646307d..a5ec59a30 100644 --- a/src/async_kernel/utils.py +++ b/src/async_kernel/utils.py @@ -95,12 +95,12 @@ def get_parent(job: Job | None = None, /) -> Message[dict[str, Any]] | None: def get_metadata(job: Job | None = None, /) -> Mapping[str, Any]: "Gets [metadata]() for the current context." - return (job or get_job().get("msg") or {}).get("metadata") or {} + return (job or get_job()).get("msg", {}).get("metadata", {}) def get_tags(job: Job | None = None, /) -> list[str]: "Gets the [tags]() for the current context." - return get_metadata().get("tags") or [] + return get_metadata(job).get("tags", []) def get_execute_request_timeout(job: Job | None = None, /) -> float | None: diff --git a/tests/conftest.py b/tests/conftest.py index c12254752..0a38fe0ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ import async_kernel.utils from async_kernel.kernel import Kernel from async_kernel.kernelspec import KernelName, make_argv +from async_kernel.typing import ExecuteContent, Job, Message, MsgHeader, MsgType, SocketID from tests import utils if TYPE_CHECKING: @@ -105,3 +106,14 @@ async def subprocess_kernels_client(anyio_backend, tmp_path_factory, kernel_name process.kill() assert not connection_file.exists(), "cleanup_connection_file not called by atexit ..." + + +@pytest.fixture +def job() -> Job[ExecuteContent]: + "An execute dummy job" + content = ExecuteContent( + code="", silent=True, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True + ) + header = MsgHeader(msg_id="", session="", username="", date="", msg_type=MsgType.execute_request, version="1") + msg = Message(header=header, parent_header=header, metadata={}, buffers=[], content=content) + return Job(msg=msg, socket_id=SocketID.shell, ident=[b""], socket=None, received_time=0.0, run_mode=None) # pyright: ignore[ reportArgumentType] diff --git a/tests/test_kernel.py b/tests/test_kernel.py index bd9b97bcc..2e37cfefa 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -9,7 +9,7 @@ import pathlib import threading import time -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Literal, cast import anyio import pytest @@ -18,7 +18,8 @@ import async_kernel.utils from async_kernel.caller import Caller from async_kernel.comm import Comm -from async_kernel.typing import ExecuteContent, Job, Message, MsgHeader, MsgType, RunMode, SocketID, Tags +from async_kernel.compiler import murmur2_x86 +from async_kernel.typing import ExecuteContent, Job, MsgType, RunMode, SocketID, Tags from tests import utils if TYPE_CHECKING: @@ -219,13 +220,10 @@ async def test_message_order(client): async def test_execute_request_error_tag_ignore_error(client): metadata = {"tags": [Tags.suppress_error]} - msg_id, content = await utils.execute( - client, "This error should be suppressed...", metadata=metadata, clear_pub=False - ) - await utils.check_pub_message(client, msg_id, execution_state="busy") - await utils.check_pub_message(client, msg_id, msg_type="execute_input") - await utils.check_pub_message(client, msg_id, execution_state="idle") - assert content["status"] == "ok" + await utils.execute(client, "stop - suppress me", metadata=metadata, clear_pub=False) + stdout, _ = await utils.assemble_output(client) + assert "⚠" in stdout + await utils.clear_iopub(client, timeout=0.1) @pytest.mark.parametrize("run_mode", RunMode) @@ -233,14 +231,11 @@ async def test_execute_request_error_tag_ignore_error(client): "code", [ "some invalid code", - "\n".join( - [ - "from async_kernel.caller import FutureCancelledError", - "async def fail():", - " raise FutureCancelledError", - "await fail()", - ] - ), + """ + from async_kernel.caller import FutureCancelledError, + async def fail():, + raise FutureCancelledError, + await fail()""", ], ) async def test_execute_request_error(client, code: str, run_mode: RunMode): @@ -525,16 +520,31 @@ async def test_invalid_message(client, channel): ("", False, SocketID.control, RunMode.task), ("threads", False, SocketID.shell, RunMode.queue), ("Task", False, SocketID.shell, RunMode.queue), - ("RunMode.direct", False, SocketID.shell, RunMode.direct), + ("RunMode.blocking", False, SocketID.shell, RunMode.blocking), ], ) -async def test_get_run_mode(kernel: Kernel, code: str, silent: bool, socket_id, expected: RunMode): - content = ExecuteContent( - code=code, silent=silent, store_history=True, user_expressions={}, allow_stdin=False, stop_on_error=True - ) - header = MsgHeader(msg_id="", session="", username="", date="", msg_type=MsgType.execute_request, version="1") - msg = Message(header=header, parent_header=header, metadata={}, buffers=[], content=content) - socket = cast("zmq.Socket[Any]", None) # pyright: ignore[reportInvalidCast] - job = Job(msg=msg, socket_id=socket_id, ident=[b""], socket=socket, received_time=0.0, run_mode=None) # pyright: ignore[reportArgumentType] +async def test_get_run_mode( + kernel: Kernel, code: str, silent: bool, socket_id, expected: RunMode, job: Job[ExecuteContent] +): + job["msg"]["content"]["code"] = code + job["msg"]["content"]["silent"] = silent mode = kernel.get_run_mode(socket_id, MsgType.execute_request, job=job) assert mode is expected + + +async def test_get_run_mode_tag(client): + metadata = {"tags": [RunMode.thread]} + _, content = await utils.execute( + client, + "from async_kernel import utils;run_mode=utils.get_job()['run_mode']", + metadata=metadata, + user_expressions={"run_mode": "run_mode"}, + ) + assert content["status"] == "ok" + assert "thread" in content["user_expressions"]["run_mode"]["data"]["text/plain"] + + +async def test_all_concurrency_run_modes(kernel): + data = kernel.all_concurrency_run_modes() + # Regen the hash as required + assert murmur2_x86(str(data), 1) == 417361055 diff --git a/tests/test_typing.py b/tests/test_typing.py index 7dd720309..bb6434d1a 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -2,7 +2,8 @@ # Distributed under the terms of the Modified BSD License. -from async_kernel.typing import RunMode +from async_kernel import Kernel +from async_kernel.typing import MetadataKeys, MsgType, RunMode, Tags class TestRunMode: @@ -16,11 +17,31 @@ def test_hash(self): assert hash(RunMode.task) == hash(RunMode.task) def test_members(self): - assert list(RunMode) == ["queue", "task", "thread", "direct"] - assert list(RunMode) == ["##queue", "##task", "##thread", "##direct"] + assert list(RunMode) == ["queue", "task", "thread", "blocking"] + assert list(RunMode) == ["##queue", "##task", "##thread", "##blocking"] assert list(RunMode) == [ "", "", "", - "", + "", ] + + +class TestMetadataKeys: + def test_str(self): + assert str(MetadataKeys.suppress_error_message) == MetadataKeys.suppress_error_message + assert MetadataKeys.suppress_error_message == MetadataKeys.suppress_error_message.name + + +class TestMsgType: + def test_all_names(self): + assert set(MsgType).intersection(vars(Kernel)) + + +class TestTags: + def test_equality(self): + assert Tags.suppress_error == str(Tags.suppress_error) + assert Tags.suppress_error == Tags.suppress_error.name + + def test_hash(self): + assert hash(Tags.suppress_error) == hash(Tags.suppress_error) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..06f636945 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,57 @@ +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING + +import pytest + +from async_kernel import utils as ak_utils + +if TYPE_CHECKING: + from async_kernel.typing import ExecuteContent, Job + + +class TestUtils: + def test_do_not_debug_this_thread(self): + with ak_utils.do_not_debug_this_thread("test"): + thread = threading.current_thread() + assert thread.name == "test" + assert getattr(thread, "pydev_do_not_trace", None) + + async def test_get_job(self, anyio_backend, job: Job[ExecuteContent]): + with pytest.raises(LookupError): + ak_utils._job_var.get() # pyright: ignore[reportPrivateUsage] + ak_utils.get_job() + ak_utils._job_var.set(job) # pyright: ignore[reportPrivateUsage] + assert ak_utils.get_job() is job + + async def test_get_execution_count(self, anyio_backend, job: Job[ExecuteContent]): + assert ak_utils.get_execution_count() == 0 + ak_utils._execution_count_var.set(3) # pyright: ignore[reportPrivateUsage] + assert ak_utils.get_execution_count() == 3 + + async def test_get_metadata(self, anyio_backend, job: Job[ExecuteContent]): + assert ak_utils.get_metadata() == {} + assert ak_utils.get_metadata(job) is job["msg"]["metadata"] + ak_utils._job_var.set(job) # pyright: ignore[reportPrivateUsage] + assert ak_utils.get_metadata() is job["msg"]["metadata"] + + async def test_get_parent(self, anyio_backend, job: Job[ExecuteContent]): + assert ak_utils.get_parent() is None + assert ak_utils.get_parent(job) is job["msg"] + ak_utils._job_var.set(job) # pyright: ignore[reportPrivateUsage] + assert ak_utils.get_parent(job) is job["msg"] + + async def test_get_tags(self, anyio_backend, job: Job[ExecuteContent]): + job["msg"]["metadata"]["tags"] = tags = [] # pyright: ignore[reportIndexIssue] + assert ak_utils.get_tags() == [] + assert ak_utils.get_tags(job) is tags + + async def test_get_execute_request_timeout(self, anyio_backend, job: Job[ExecuteContent]): + job["msg"]["metadata"] = {"timeout": 3} + assert ak_utils.get_execute_request_timeout(job) == 3 + ak_utils._job_var.set(job) # pyright: ignore[reportPrivateUsage] + assert ak_utils.get_execute_request_timeout() == 3 From bd8d5880a8fcdb78d06eaad7fb53469f60321ef7 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 11:03:24 +1000 Subject: [PATCH 14/18] Update packages an lock file. --- .pre-commit-config.yaml | 4 +- CONTRIBUTING.md | 8 +- mkdocs.yml | 4 +- uv.lock | 654 +++++++++++++++++++++++++--------------- 4 files changed, 430 insertions(+), 240 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a74885ac..011c69d22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - id: check-json5 - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows @@ -66,7 +66,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.9 hooks: - id: ruff types_or: [python, jupyter] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c984248ff..d1a7218ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,12 @@ uv sync # Activate the environment ``` +### update packages + +```shell +uv lock --upgrade +``` + ## Running tests ```shell @@ -94,7 +100,7 @@ These links are not relevant for docstrings. ### Deploy manually -``` +```shell mkdocs gh-deploy --force ``` diff --git a/mkdocs.yml b/mkdocs.yml index d4a889181..f98811037 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,8 +26,8 @@ theme: - navigation.tabs - navigation.tabs.sticky - content.code.copy - - content.action.edit - - content.action.view + # - content.action.edit edit source / view source # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+action#contentaction + # - content.action.view - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#anchor-following # - toc.integrate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=table#navigation-integration - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/?h=naviga#back-to-top-button diff --git a/uv.lock b/uv.lock index 7721991c7..e870c676e 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version < '3.12'", ] [[package]] @@ -34,7 +35,8 @@ name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings" }, + { name = "argon2-cffi-bindings", version = "21.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ @@ -45,8 +47,11 @@ wheels = [ name = "argon2-cffi-bindings" version = "21.2.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } wheels = [ @@ -62,6 +67,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, ] +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + [[package]] name = "arrow" version = "1.3.0" @@ -123,10 +163,12 @@ dev = [ docs = [ { name = "ipywidgets" }, { name = "jupyterlab" }, + { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material" }, { name = "mkdocs-open-in-new-tab" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "pandas" }, ] [package.metadata] @@ -166,10 +208,12 @@ dev = [ docs = [ { name = "ipywidgets" }, { name = "jupyterlab" }, + { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material" }, { name = "mkdocs-open-in-new-tab" }, { name = "mkdocstrings", extras = ["python"] }, + { name = "pandas" }, ] [[package]] @@ -224,14 +268,14 @@ wheels = [ [[package]] name = "basedpyright" -version = "1.31.1" +version = "1.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodejs-wheel-binaries" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/39/e2870a3739dce055a5b7822d027843c9ba9b3453dcb4b226d9b0e9d486f4/basedpyright-1.31.1.tar.gz", hash = "sha256:4e4d922a385f45dc93e50738d1131ec4533fee5d338b700ef2d28e2e0412e642", size = 22067890, upload-time = "2025-08-03T13:41:15.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/32/561d61dc99789b999b86f5e8683658ea7d096b16d2886aacffb3482ab637/basedpyright-1.31.2.tar.gz", hash = "sha256:dd18ed85770f80723d4378b0a0f05f24ef205b71ba4b525242abf1782ed16d8f", size = 22068420, upload-time = "2025-08-13T14:05:41.28Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/cc/8bca3b3a48d6a03a4b857a297fb1473ed1b9fa111be2d20c01f11112e75c/basedpyright-1.31.1-py3-none-any.whl", hash = "sha256:8b647bf07fff929892db4be83a116e6e1e59c13462ecb141214eb271f6785ee5", size = 11540576, upload-time = "2025-08-03T13:41:11.571Z" }, + { url = "https://files.pythonhosted.org/packages/46/70/96e39d0724a08622a248ddc8dfd56c1cf3465b5aaeff414dc39ba7b679ee/basedpyright-1.31.2-py3-none-any.whl", hash = "sha256:b3541fba56a69de826f77a15f8b864648d1cfbcb11a3ca530d82982e65e78d19", size = 11540670, upload-time = "2025-08-13T14:05:38.631Z" }, ] [[package]] @@ -494,77 +538,77 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, - { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, - { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" }, - { url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, - { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, - { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, - { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, - { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" }, - { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, - { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" }, - { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" }, - { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, - { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, - { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" }, - { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" }, - { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, - { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" }, - { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, - { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" }, - { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" }, - { url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload-time = "2025-08-10T21:26:44.132Z" }, - { url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload-time = "2025-08-10T21:26:45.598Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload-time = "2025-08-10T21:26:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload-time = "2025-08-10T21:26:48.65Z" }, - { url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload-time = "2025-08-10T21:26:50.368Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload-time = "2025-08-10T21:26:52.045Z" }, - { url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload-time = "2025-08-10T21:26:53.533Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload-time = "2025-08-10T21:26:55.11Z" }, - { url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload-time = "2025-08-10T21:26:56.637Z" }, - { url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload-time = "2025-08-10T21:26:58.207Z" }, - { url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload-time = "2025-08-10T21:26:59.707Z" }, - { url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload-time = "2025-08-10T21:27:01.242Z" }, - { url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload-time = "2025-08-10T21:27:03.442Z" }, - { url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload-time = "2025-08-10T21:27:05.124Z" }, - { url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload-time = "2025-08-10T21:27:06.68Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload-time = "2025-08-10T21:27:08.398Z" }, - { url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload-time = "2025-08-10T21:27:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload-time = "2025-08-10T21:27:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload-time = "2025-08-10T21:27:13.254Z" }, - { url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload-time = "2025-08-10T21:27:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload-time = "2025-08-10T21:27:16.297Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload-time = "2025-08-10T21:27:18.22Z" }, - { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, +version = "7.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload-time = "2025-08-17T00:24:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload-time = "2025-08-17T00:24:35.881Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload-time = "2025-08-17T00:24:37.61Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload-time = "2025-08-17T00:24:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload-time = "2025-08-17T00:24:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload-time = "2025-08-17T00:24:41.904Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload-time = "2025-08-17T00:24:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload-time = "2025-08-17T00:24:44.906Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload-time = "2025-08-17T00:24:46.241Z" }, + { url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload-time = "2025-08-17T00:24:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload-time = "2025-08-17T00:24:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" }, + { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" }, + { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" }, + { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" }, + { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" }, + { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" }, + { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" }, + { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" }, ] [package.optional-dependencies] @@ -673,53 +717,69 @@ wheels = [ [[package]] name = "fastjsonschema" -version = "2.21.1" +version = "2.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload-time = "2024-12-02T10:55:15.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] [[package]] name = "fonttools" -version = "4.59.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, - { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, - { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" }, - { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" }, - { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" }, - { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" }, - { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" }, - { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, +version = "4.59.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/7f/29c9c3fe4246f6ad96fee52b88d0dc3a863c7563b0afc959e36d78b965dc/fonttools-4.59.1.tar.gz", hash = "sha256:74995b402ad09822a4c8002438e54940d9f1ecda898d2bb057729d7da983e4cb", size = 3534394, upload-time = "2025-08-14T16:28:14.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/62/9667599561f623d4a523cc9eb4f66f3b94b6155464110fa9aebbf90bbec7/fonttools-4.59.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4909cce2e35706f3d18c54d3dcce0414ba5e0fb436a454dffec459c61653b513", size = 2778815, upload-time = "2025-08-14T16:26:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/8f/78/cc25bcb2ce86033a9df243418d175e58f1956a35047c685ef553acae67d6/fonttools-4.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbec204fa9f877641747f2d9612b2b656071390d7a7ef07a9dbf0ecf9c7195c", size = 2341631, upload-time = "2025-08-14T16:26:30.396Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cc/fcbb606dd6871f457ac32f281c20bcd6cc77d9fce77b5a4e2b2afab1f500/fonttools-4.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39dfd42cc2dc647b2c5469bc7a5b234d9a49e72565b96dd14ae6f11c2c59ef15", size = 5022222, upload-time = "2025-08-14T16:26:32.447Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/c0b1cf2b74d08eb616a80dbf5564351fe4686147291a25f7dce8ace51eb3/fonttools-4.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b11bc177a0d428b37890825d7d025040d591aa833f85f8d8878ed183354f47df", size = 4966512, upload-time = "2025-08-14T16:26:34.621Z" }, + { url = "https://files.pythonhosted.org/packages/a4/26/51ce2e3e0835ffc2562b1b11d1fb9dafd0aca89c9041b64a9e903790a761/fonttools-4.59.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b9b4c35b3be45e5bc774d3fc9608bbf4f9a8d371103b858c80edbeed31dd5aa", size = 5001645, upload-time = "2025-08-14T16:26:36.876Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/ef0b23f4266349b6d5ccbd1a07b7adc998d5bce925792aa5d1ec33f593e3/fonttools-4.59.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:01158376b8a418a0bae9625c476cebfcfcb5e6761e9d243b219cd58341e7afbb", size = 5113777, upload-time = "2025-08-14T16:26:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/d0/da/b398fe61ef433da0a0472cdb5d4399124f7581ffe1a31b6242c91477d802/fonttools-4.59.1-cp311-cp311-win32.whl", hash = "sha256:cf7c5089d37787387123f1cb8f1793a47c5e1e3d1e4e7bfbc1cc96e0f925eabe", size = 2215076, upload-time = "2025-08-14T16:26:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/94/bd/e2624d06ab94e41c7c77727b2941f1baed7edb647e63503953e6888020c9/fonttools-4.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:c866eef7a0ba320486ade6c32bfc12813d1a5db8567e6904fb56d3d40acc5116", size = 2262779, upload-time = "2025-08-14T16:26:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/6e069cc4cb8881d164a9bd956e9df555bc62d3eb36f6282e43440200009c/fonttools-4.59.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43ab814bbba5f02a93a152ee61a04182bb5809bd2bc3609f7822e12c53ae2c91", size = 2769172, upload-time = "2025-08-14T16:26:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/b9/98/ec4e03f748fefa0dd72d9d95235aff6fef16601267f4a2340f0e16b9330f/fonttools-4.59.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f04c3ffbfa0baafcbc550657cf83657034eb63304d27b05cff1653b448ccff6", size = 2337281, upload-time = "2025-08-14T16:26:47.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b1/890360a7e3d04a30ba50b267aca2783f4c1364363797e892e78a4f036076/fonttools-4.59.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d601b153e51a5a6221f0d4ec077b6bfc6ac35bfe6c19aeaa233d8990b2b71726", size = 4909215, upload-time = "2025-08-14T16:26:49.682Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/2490599550d6c9c97a44c1e36ef4de52d6acf742359eaa385735e30c05c4/fonttools-4.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c735e385e30278c54f43a0d056736942023c9043f84ee1021eff9fd616d17693", size = 4951958, upload-time = "2025-08-14T16:26:51.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/bd053f6f7634234a9b9805ff8ae4f32df4f2168bee23cafd1271ba9915a9/fonttools-4.59.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1017413cdc8555dce7ee23720da490282ab7ec1cf022af90a241f33f9a49afc4", size = 4894738, upload-time = "2025-08-14T16:26:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a1/3cd12a010d288325a7cfcf298a84825f0f9c29b01dee1baba64edfe89257/fonttools-4.59.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5c6d8d773470a5107052874341ed3c487c16ecd179976d81afed89dea5cd7406", size = 5045983, upload-time = "2025-08-14T16:26:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/8a2c3f6619cc43cf87951405337cc8460d08a4e717bb05eaa94b335d11dc/fonttools-4.59.1-cp312-cp312-win32.whl", hash = "sha256:2a2d0d33307f6ad3a2086a95dd607c202ea8852fa9fb52af9b48811154d1428a", size = 2203407, upload-time = "2025-08-14T16:26:58.165Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f2/a19b874ddbd3ebcf11d7e25188ef9ac3f68b9219c62263acb34aca8cde05/fonttools-4.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b9e4fa7eaf046ed6ac470f6033d52c052481ff7a6e0a92373d14f556f298dc0", size = 2251561, upload-time = "2025-08-14T16:27:00.646Z" }, + { url = "https://files.pythonhosted.org/packages/19/5e/94a4d7f36c36e82f6a81e0064d148542e0ad3e6cf51fc5461ca128f3658d/fonttools-4.59.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:89d9957b54246c6251345297dddf77a84d2c19df96af30d2de24093bbdf0528b", size = 2760192, upload-time = "2025-08-14T16:27:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/f50712fc33ef9d06953c660cefaf8c8fe4b8bc74fa21f44ee5e4f9739439/fonttools-4.59.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8156b11c0d5405810d216f53907bd0f8b982aa5f1e7e3127ab3be1a4062154ff", size = 2332694, upload-time = "2025-08-14T16:27:04.883Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/5a9fc21c354bf8613215ce233ab0d933bd17d5ff4c29693636551adbc7b3/fonttools-4.59.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8387876a8011caec52d327d5e5bca705d9399ec4b17afb8b431ec50d47c17d23", size = 4889254, upload-time = "2025-08-14T16:27:07.02Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e5/54a6dc811eba018d022ca2e8bd6f2969291f9586ccf9a22a05fc55f91250/fonttools-4.59.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb13823a74b3a9204a8ed76d3d6d5ec12e64cc5bc44914eb9ff1cdac04facd43", size = 4949109, upload-time = "2025-08-14T16:27:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/db/15/b05c72a248a95bea0fd05fbd95acdf0742945942143fcf961343b7a3663a/fonttools-4.59.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1ca10da138c300f768bb68e40e5b20b6ecfbd95f91aac4cc15010b6b9d65455", size = 4888428, upload-time = "2025-08-14T16:27:11.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/71/c7d6840f858d695adc0c4371ec45e3fb1c8e060b276ba944e2800495aca4/fonttools-4.59.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2beb5bfc4887a3130f8625349605a3a45fe345655ce6031d1bac11017454b943", size = 5032668, upload-time = "2025-08-14T16:27:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/57be4aca6f1312e2bc4d811200dd822325794e05bdb26eeff0976edca651/fonttools-4.59.1-cp313-cp313-win32.whl", hash = "sha256:419f16d750d78e6d704bfe97b48bba2f73b15c9418f817d0cb8a9ca87a5b94bf", size = 2201832, upload-time = "2025-08-14T16:27:16.126Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/1899a6175a5f900ed8730a0d64f53ca1b596ed7609bfda033cf659114258/fonttools-4.59.1-cp313-cp313-win_amd64.whl", hash = "sha256:c536f8a852e8d3fa71dde1ec03892aee50be59f7154b533f0bf3c1174cfd5126", size = 2250673, upload-time = "2025-08-14T16:27:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/f6ba82c22f118d9985c37fea65d8d715ca71300d78b6c6e90874dc59f11d/fonttools-4.59.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d5c3bfdc9663f3d4b565f9cb3b8c1efb3e178186435b45105bde7328cfddd7fe", size = 2758606, upload-time = "2025-08-14T16:27:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/3a/81/84aa3d0ce27b0112c28b67b637ff7a47cf401cf5fbfee6476e4bc9777580/fonttools-4.59.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ea03f1da0d722fe3c2278a05957e6550175571a4894fbf9d178ceef4a3783d2b", size = 2330187, upload-time = "2025-08-14T16:27:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/17/41/b3ba43f78afb321e2e50232c87304c8d0f5ab39b64389b8286cc39cdb824/fonttools-4.59.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57a3708ca6bfccb790f585fa6d8f29432ec329618a09ff94c16bcb3c55994643", size = 4832020, upload-time = "2025-08-14T16:27:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/3af871c7fb325a68938e7ce544ca48bfd2c6bb7b357f3c8252933b29100a/fonttools-4.59.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:729367c91eb1ee84e61a733acc485065a00590618ca31c438e7dd4d600c01486", size = 4930687, upload-time = "2025-08-14T16:27:26.484Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4f/299fc44646b30d9ef03ffaa78b109c7bd32121f0d8f10009ee73ac4514bc/fonttools-4.59.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f8ef66ac6db450193ed150e10b3b45dde7aded10c5d279968bc63368027f62b", size = 4875794, upload-time = "2025-08-14T16:27:28.887Z" }, + { url = "https://files.pythonhosted.org/packages/90/cf/a0a3d763ab58f5f81ceff104ddb662fd9da94248694862b9c6cbd509fdd5/fonttools-4.59.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:075f745d539a998cd92cb84c339a82e53e49114ec62aaea8307c80d3ad3aef3a", size = 4985780, upload-time = "2025-08-14T16:27:30.858Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/ba76511aaae143d89c29cd32ce30bafb61c477e8759a1590b8483f8065f8/fonttools-4.59.1-cp314-cp314-win32.whl", hash = "sha256:c2b0597522d4c5bb18aa5cf258746a2d4a90f25878cbe865e4d35526abd1b9fc", size = 2205610, upload-time = "2025-08-14T16:27:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/b250e69d6caf35bc65cddbf608be0662d741c248f2e7503ab01081fc267e/fonttools-4.59.1-cp314-cp314-win_amd64.whl", hash = "sha256:e9ad4ce044e3236f0814c906ccce8647046cc557539661e35211faadf76f283b", size = 2255376, upload-time = "2025-08-14T16:27:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/11/f3/0bc63a23ac0f8175e23d82f85d6ee693fbd849de7ad739f0a3622182ad29/fonttools-4.59.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:652159e8214eb4856e8387ebcd6b6bd336ee258cbeb639c8be52005b122b9609", size = 2826546, upload-time = "2025-08-14T16:27:36.783Z" }, + { url = "https://files.pythonhosted.org/packages/e9/46/a3968205590e068fdf60e926be329a207782576cb584d3b7dcd2d2844957/fonttools-4.59.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:43d177cd0e847ea026fedd9f099dc917da136ed8792d142298a252836390c478", size = 2359771, upload-time = "2025-08-14T16:27:39.678Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ff/d14b4c283879e8cb57862d9624a34fe6522b6fcdd46ccbfc58900958794a/fonttools-4.59.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e54437651e1440ee53a95e6ceb6ee440b67a3d348c76f45f4f48de1a5ecab019", size = 4831575, upload-time = "2025-08-14T16:27:41.885Z" }, + { url = "https://files.pythonhosted.org/packages/9c/04/a277d9a584a49d98ca12d3b2c6663bdf333ae97aaa83bd0cdabf7c5a6c84/fonttools-4.59.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6065fdec8ff44c32a483fd44abe5bcdb40dd5e2571a5034b555348f2b3a52cea", size = 5069962, upload-time = "2025-08-14T16:27:44.284Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/3d2ae69d96c4cdee6dfe7598ca5519a1514487700ca3d7c49c5a1ad65308/fonttools-4.59.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42052b56d176f8b315fbc09259439c013c0cb2109df72447148aeda677599612", size = 4942926, upload-time = "2025-08-14T16:27:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c17379e0048d03ce26b38e4ab0e9a98280395b00529e093fe2d663ac0658/fonttools-4.59.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bcd52eaa5c4c593ae9f447c1d13e7e4a00ca21d755645efa660b6999425b3c88", size = 4958678, upload-time = "2025-08-14T16:27:48.555Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/c5543a1540abdfb4d375e3ebeb84de365ab9b153ec14cb7db05f537dd1e7/fonttools-4.59.1-cp314-cp314t-win32.whl", hash = "sha256:02e4fdf27c550dded10fe038a5981c29f81cb9bc649ff2eaa48e80dab8998f97", size = 2266706, upload-time = "2025-08-14T16:27:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/99/85bff6e674226bc8402f983e365f07e76d990e7220ba72bcc738fef52391/fonttools-4.59.1-cp314-cp314t-win_amd64.whl", hash = "sha256:412a5fd6345872a7c249dac5bcce380393f40c1c316ac07f447bc17d51900922", size = 2329994, upload-time = "2025-08-14T16:27:52.36Z" }, + { url = "https://files.pythonhosted.org/packages/0f/64/9d606e66d498917cd7a2ff24f558010d42d6fd4576d9dd57f0bd98333f5a/fonttools-4.59.1-py3-none-any.whl", hash = "sha256:647db657073672a8330608970a984d51573557f328030566521bc03415535042", size = 1130094, upload-time = "2025-08-14T16:28:12.048Z" }, ] [[package]] @@ -743,16 +803,40 @@ 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 = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + [[package]] name = "griffe" -version = "1.11.1" +version = "1.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/0f/9cbd56eb047de77a4b93d8d4674e70cd19a1ff64d7410651b514a1ed93d5/griffe-1.11.1.tar.gz", hash = "sha256:d54ffad1ec4da9658901eb5521e9cddcdb7a496604f67d8ae71077f03f549b7e", size = 410996, upload-time = "2025-08-11T11:38:35.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/ca/29f36e00c74844ae50d139cf5a8b1751887b2f4d5023af65d460268ad7aa/griffe-1.12.1.tar.gz", hash = "sha256:29f5a6114c0aeda7d9c86a570f736883f8a2c5b38b57323d56b3d1c000565567", size = 411863, upload-time = "2025-08-14T21:08:15.38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/a3/451ffd422ce143758a39c0290aaa7c9727ecc2bcc19debd7a8f3c6075ce9/griffe-1.11.1-py3-none-any.whl", hash = "sha256:5799cf7c513e4b928cfc6107ee6c4bc4a92e001f07022d97fd8dee2f612b6064", size = 138745, upload-time = "2025-08-11T11:38:33.964Z" }, + { url = "https://files.pythonhosted.org/packages/13/f2/4fab6c3e5bcaf38a44cc8a974d2752eaad4c129e45d6533d926a30edd133/griffe-1.12.1-py3-none-any.whl", hash = "sha256:2d7c12334de00089c31905424a00abcfd931b45b8b516967f224133903d302cc", size = 138940, upload-time = "2025-08-14T21:08:13.382Z" }, ] [[package]] @@ -869,7 +953,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.14'" }, + { name = "zipp", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -997,14 +1081,14 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.2.1" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, ] [[package]] @@ -1060,7 +1144,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.0" +version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1068,9 +1152,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [package.optional-dependencies] @@ -1204,7 +1288,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.4.5" +version = "4.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1221,9 +1305,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/89/695805a6564bafe08ef2505f3c473ae7140b8ba431d381436f11bdc2c266/jupyterlab-4.4.5.tar.gz", hash = "sha256:0bd6c18e6a3c3d91388af6540afa3d0bb0b2e76287a7b88ddf20ab41b336e595", size = 23037079, upload-time = "2025-07-20T09:21:30.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/5c/14f0852233d60d30bf0f22a817d6c20ac555d73526cc915274f97c07a2b9/jupyterlab-4.4.6.tar.gz", hash = "sha256:e0b720ff5392846bdbc01745f32f29f4d001c071a4bff94d8b516ba89b5a4157", size = 23040936, upload-time = "2025-08-15T11:44:15.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/74/e144ce85b34414e44b14c1f6bf2e3bfe17964c8e5670ebdc7629f2e53672/jupyterlab-4.4.5-py3-none-any.whl", hash = "sha256:e76244cceb2d1fb4a99341f3edc866f2a13a9e14c50368d730d75d8017be0863", size = 12267763, upload-time = "2025-07-20T09:21:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/38/6182d63f39428821e705e86fba61704fc69769a24ca5a9578c2c04986c9a/jupyterlab-4.4.6-py3-none-any.whl", hash = "sha256:e877e930f46dde2e3ee9da36a935c6cd4fdb15aa7440519d0fde696f9fadb833", size = 12268564, upload-time = "2025-08-15T11:44:11.42Z" }, ] [[package]] @@ -1631,6 +1715,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] +[[package]] +name = "mkdocs-git-revision-date-localized-plugin" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f8/a17ec39a4fc314d40cc96afdc1d401e393ebd4f42309d454cc940a2cf38a/mkdocs_git_revision_date_localized_plugin-1.4.7.tar.gz", hash = "sha256:10a49eff1e1c3cb766e054b9d8360c904ce4fe8c33ac3f6cc083ac6459c91953", size = 450473, upload-time = "2025-05-28T18:26:20.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b6/106fcc15287e7228658fbd0ad9e8b0d775becced0a089cc39984641f4a0f/mkdocs_git_revision_date_localized_plugin-1.4.7-py3-none-any.whl", hash = "sha256:056c0a90242409148f1dc94d5c9d2c25b5b8ddd8de45489fa38f7fa7ccad2bc4", size = 25382, upload-time = "2025-05-28T18:26:18.907Z" }, +] + [[package]] name = "mkdocs-jupyter" version = "0.25.1" @@ -1650,11 +1749,12 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.16" +version = "9.6.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, + { name = "click" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -1665,9 +1765,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/02/51115cdda743e1551c5c13bdfaaf8c46b959acc57ba914d8ec479dd2fe1f/mkdocs_material-9.6.17.tar.gz", hash = "sha256:48ae7aec72a3f9f501a70be3fbd329c96ff5f5a385b67a1563e5ed5ce064affe", size = 4032898, upload-time = "2025-08-15T16:09:21.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/0f0d44c92c8f3068930da495b752244bd59fd87b5b0f9571fa2d2a93aee7/mkdocs_material-9.6.17-py3-none-any.whl", hash = "sha256:221dd8b37a63f52e580bcab4a7e0290e4a6f59bd66190be9c3d40767e05f9417", size = 9229230, upload-time = "2025-08-15T16:09:18.301Z" }, ] [[package]] @@ -1715,16 +1815,16 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "1.16.12" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/7c/6dfd8ad59c0eebae167168528ed6cad00116f58ef2327686149f7b25d175/mkdocstrings_python-1.17.0.tar.gz", hash = "sha256:c6295962b60542a9c7468a3b515ce8524616ca9f8c1a38c790db4286340ba501", size = 200408, upload-time = "2025-08-14T21:18:14.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/b1fcc937f4ecd372f3e857162dea67c45c1e2eedbac80447be516e3372bb/mkdocstrings_python-1.17.0-py3-none-any.whl", hash = "sha256:49903fa355dfecc5ad0b891e78ff5d25d30ffd00846952801bbe8331e123d4b0", size = 124778, upload-time = "2025-08-14T21:18:12.821Z" }, ] [[package]] @@ -1955,6 +2055,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" }, + { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" }, + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, +] + [[package]] name = "pandocfilters" version = "1.5.1" @@ -2293,6 +2434,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, ] +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2454,7 +2604,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2462,9 +2612,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -2623,27 +2773,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, ] [[package]] @@ -2695,6 +2846,15 @@ 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 = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2891,6 +3051,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -2923,42 +3092,42 @@ wheels = [ [[package]] name = "uv" -version = "0.8.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/a1/4dea87c10875b441d906f82df42d725a4a04c2e8ae720d9fa01e1f75e3dc/uv-0.8.9.tar.gz", hash = "sha256:54d76faf5338d1e5643a32b048c600de0cdaa7084e5909106103df04f3306615", size = 3478291, upload-time = "2025-08-12T02:32:37.187Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/d8/a2a24d30660b5f05f86699f86b642b1193bea1017e77e5e5d3e1c64f7bcc/uv-0.8.9-py3-none-linux_armv6l.whl", hash = "sha256:4633c693c79c57a77c52608cbca8a6bb17801bfa223326fbc5c5142654c23cc3", size = 18477020, upload-time = "2025-08-12T02:31:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/4d/21/937e590fb08ce4c82503fddb08b54613c0d42dd06c660460f8f0552dd3a7/uv-0.8.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cdc11cbc81824e51ebb1bac35745a79048557e869ef9da458e99f1c3a96c7f9", size = 18486975, upload-time = "2025-08-12T02:31:54.804Z" }, - { url = "https://files.pythonhosted.org/packages/60/a8/e6fc3e204731aa26b09934bbdecc8d6baa58a2d9e55b59b13130bacf8e52/uv-0.8.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b20ee83e3bf294e0b1347d0b27c56ea1a4fa7eeff4361fbf1f39587d4273059", size = 17178749, upload-time = "2025-08-12T02:31:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3e/3104a054bb6e866503a13114ee969d4b66227ebab19a38e3468f36c03a87/uv-0.8.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3418315e624f60a1c4ed37987b35d5ff0d03961d380e7e7946a3378499d5d779", size = 17790897, upload-time = "2025-08-12T02:31:59.451Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/ab64cca644f40bf85fb9b3a9050aad25af7882a1d774a384fc473ef9c697/uv-0.8.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7efe01b3ed9816e07e6cd4e088472a558a1d2946177f31002b4c42cd55cb4604", size = 18124831, upload-time = "2025-08-12T02:32:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/08/d1/68a001e3ad5d0601ea9ff348b54a78c8ba87fd2a6b6b5e27b379f6f3dff0/uv-0.8.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e571132495d7ab24d2f0270c559d6facd4224745d9db7dff8c20ec0c71ae105a", size = 18924774, upload-time = "2025-08-12T02:32:04.479Z" }, - { url = "https://files.pythonhosted.org/packages/ed/71/1b252e523eb875aa4ac8d06d5f8df175fa2d29e13da347d5d4823bce6c47/uv-0.8.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:67507c66837d8465daaad9f2ccd7da7af981d8c94eb8e32798f62a98c28de82d", size = 20256335, upload-time = "2025-08-12T02:32:07.12Z" }, - { url = "https://files.pythonhosted.org/packages/30/fc/062a25088b30a0fd27e4cc46baa272dd816acdec252b120d05a16d63170a/uv-0.8.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3162f495805a26fba5aacbee49c8650e1e74313c7a2e6df6aec5de9d1299087", size = 19920018, upload-time = "2025-08-12T02:32:10.041Z" }, - { url = "https://files.pythonhosted.org/packages/d8/55/90a0dc35938e68509ff8e8a49ff45b0fd13f3a44752e37d8967cd9d19316/uv-0.8.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60eb70afeb1c66180e12a15afd706bcc0968dbefccf7ef6e5d27a1aaa765419b", size = 19235553, upload-time = "2025-08-12T02:32:12.361Z" }, - { url = "https://files.pythonhosted.org/packages/ae/a4/2db5939a3a993a06bca0a42e2120b4385bf1a4ff54242780701759252052/uv-0.8.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011d2b2d4781555f7f7d29d2f0d6b2638fc60eeff479406ed570052664589e6a", size = 19259174, upload-time = "2025-08-12T02:32:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c9/c52249b5f40f8eb2157587ae4b997942335e4df312dfb83b16b5ebdecc61/uv-0.8.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:97621843e087a68c0b4969676367d757e1de43c00a9f554eb7da35641bdff8a2", size = 18048069, upload-time = "2025-08-12T02:32:16.955Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ca/524137719fb09477e57c5983fa8864f824f5858b29fc679c0416634b79f0/uv-0.8.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b1be6a7b49d23b75d598691cc5c065a9e3cdf5e6e75d7b7f42f24d758ceef3c4", size = 18943440, upload-time = "2025-08-12T02:32:19.212Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b8/877bf9a52207023a8bf9b762bed3853697ed71c5c9911a4e31231de49a23/uv-0.8.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:91598361309c3601382c552dc22256f70b2491ad03357b66caa4be6fdf1111dd", size = 18075581, upload-time = "2025-08-12T02:32:21.732Z" }, - { url = "https://files.pythonhosted.org/packages/96/de/272d4111ff71765bcbfd3ecb4d4fff4073f08cc38b3ecdb7272518c3fe93/uv-0.8.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc81df9dd7571756e34255592caab92821652face35c3f52ad05efaa4bcc39d3", size = 18420275, upload-time = "2025-08-12T02:32:24.488Z" }, - { url = "https://files.pythonhosted.org/packages/90/15/fecfc6665d1bfc5c7dbd32ff1d63413ac43d7f6d16d76fdc4d2513cbe807/uv-0.8.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9ef728e0a5caa2bb129c009a68b30819552e7addf934916a466116e302748bed", size = 19354288, upload-time = "2025-08-12T02:32:27.714Z" }, - { url = "https://files.pythonhosted.org/packages/52/b5/9fef88ac0cc3ca71ff718fa7d7e90c1b3a8639b041c674825aae00d24bf5/uv-0.8.9-py3-none-win32.whl", hash = "sha256:a347c2f2630a45a3b7ceae28a78a528137edfec4847bb29da1561bd8d1f7d254", size = 18197270, upload-time = "2025-08-12T02:32:30.288Z" }, - { url = "https://files.pythonhosted.org/packages/04/0a/dacd483c9726d2b74e42ee1f186aabab508222114f3099a7610ad0f78004/uv-0.8.9-py3-none-win_amd64.whl", hash = "sha256:dc12048cdb53210d0c7218bb403ad30118b1fe8eeff3fbcc184c13c26fcc47d4", size = 20221458, upload-time = "2025-08-12T02:32:32.706Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7e/f2b35278304673dcf9e8fe84b6d15531d91c59530dcf7919111f39a8d28f/uv-0.8.9-py3-none-win_arm64.whl", hash = "sha256:53332de28e9ee00effb695a15cdc70b2455d6b5f6b596d556076b5dd1fd3aa26", size = 18805689, upload-time = "2025-08-12T02:32:35.036Z" }, +version = "0.8.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/4f/1a670b2b6473282b36894c22612f76d7fec81d67f5794081d0240a2d960a/uv-0.8.12.tar.gz", hash = "sha256:c06788fccf8057173d6f76018399fbee757a1f06fe0041acbe27aa74322374f5", size = 3518136, upload-time = "2025-08-19T00:00:13.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/73/e098295c892ccdd535448d56aee1873feeff5b085c79675d9bed3c685972/uv-0.8.12-py3-none-linux_armv6l.whl", hash = "sha256:d53de2398d664a325848e952f1d88d88d0564773effb55b9cb279ed4826d61e0", size = 18630720, upload-time = "2025-08-18T23:59:18.479Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/d47d9862d27b17a78982e900f49647e7209cebecd10004a61833286628b0/uv-0.8.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9c75edaef18a1eb536e4f3ce21ab400f3420c4a22e081f57d5343e14b5064cde", size = 18712285, upload-time = "2025-08-18T23:59:22.746Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ea/8d93b58237dbf6d909e5241bb994720ffe860120de9dbdfe4c75d2c424c6/uv-0.8.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:43abe9466446330b991a9c0702853d8f13ce68b47a6c94a439e42eb349f6c85e", size = 17327286, upload-time = "2025-08-18T23:59:25.268Z" }, + { url = "https://files.pythonhosted.org/packages/44/d5/2e696692f54e65a47b91ad00dbe10dedd2225fb72a8d60ffb0f0daecdc34/uv-0.8.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:533af0f2392b391a09ddda3c7e9f9246a3aca3ecbfd54e20e7a6e13120d5682f", size = 17964361, upload-time = "2025-08-18T23:59:27.976Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4d/5f7b2c81b8698737bd3e33e80f4131a8989a4f0d643a68c0fe89c50468c0/uv-0.8.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d68712d682ea770a2ed6e85c06de3ef50253933c7a84d8961dcb8f438a9de2", size = 18285408, upload-time = "2025-08-18T23:59:30.546Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/23ef817bae1bb21e21465354bdaf01905a25d9c1e012acabf8cb9a1200f3/uv-0.8.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aba50b617622f5931c7ec4e81a14517e84efb343233c4976689a372e7e4fd98", size = 19047065, upload-time = "2025-08-18T23:59:33.792Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/99b8cd88aed73a8103c18232ba1beee7a80d385d0b89845d8aef30c4bdc4/uv-0.8.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e850ef65d37acd7c762f544ec960939f17a846926bb985085020f6244e73f9ee", size = 20396732, upload-time = "2025-08-18T23:59:37.155Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/63553e3201f854a9daf985522c95d05ab4fa5f8c9e3433a782c0f3a87f4b/uv-0.8.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c2c8b7ee57aaf7dd765ed4c5e864561d5b37e36265d5e082bc6ac0e88332708", size = 20046411, upload-time = "2025-08-18T23:59:40.061Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/fd83725a96c9a733e52d8026c5a679f43207781d5f46e5751a21049d2099/uv-0.8.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac70a29b6c135cb77ad045315a17d976175bc92115ef751f7fc9bcd8be23b4ee", size = 19444961, upload-time = "2025-08-18T23:59:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/2d/31/89714afcb319a6aa7f0984ebcdbe183c65785f2297b8ea51f18d6a6afc15/uv-0.8.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0febae768d9fa3968957cf918e0daf6d0e7026946640c5967b69b7fa7dc19a83", size = 19356506, upload-time = "2025-08-18T23:59:45.277Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/8dda72705fb142630cf224abd0ab924fb8d1e556eef2a1fc9d78b3d82da8/uv-0.8.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:887892cd8242c24d630b47ac14a632547f7ed6af95a4be2c214beaa9e0f57c13", size = 18219278, upload-time = "2025-08-18T23:59:47.814Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ef/01b85a535e261ff4044a2cb9baa77fb6ab8540f42dde2fcc0b8f31ad0d1f/uv-0.8.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b874d62e85b155e9fa5056d780a4dc14cd27b39d6a8147c5bd677b5cb1dad41c", size = 19081357, upload-time = "2025-08-18T23:59:50.766Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b5/9435e4dec8b091399e2aa59225ba77779f3089ae6a6c149a144677514cfc/uv-0.8.12-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:52f5bdb7c9df0139dd64d1718261a9effa5c7602871a4726a573f7aa74c9ce48", size = 18242319, upload-time = "2025-08-18T23:59:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1c/6db7dd73eb9de8b152d1b6a0acc76b3b0e148bffa5b6ce0b13a3a4a0d795/uv-0.8.12-py3-none-musllinux_1_1_i686.whl", hash = "sha256:449eb6465e02b929a7c512ab50812336aec231b68608299f8b45ba38fdcc5eb6", size = 18563102, upload-time = "2025-08-18T23:59:55.941Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/f89df904fe16b1f036768a250c756390bdd2db6e9ddb87c9ba476518914b/uv-0.8.12-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:7a590cd21ae678db703a5059d455106198310ba64131733cebac77b37e2d88f9", size = 19506827, upload-time = "2025-08-18T23:59:58.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3f/cdab93dea8780af173e60ff26def7f5bc67e1812407e1e5822795ba9cad6/uv-0.8.12-py3-none-win32.whl", hash = "sha256:6e8554da8efd022c8cebf843166500b234e30efdcaaa6a30c58dcae125a1bbb9", size = 18455052, upload-time = "2025-08-19T00:00:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/70e74708edfe785e091c4d61ee15e9d784f4b883294a18a6fb3cf032d1ea/uv-0.8.12-py3-none-win_amd64.whl", hash = "sha256:51681666b3c30f764c1c7829dd6ca79d51506737a2058cc57b06028b9f6db34b", size = 20321443, upload-time = "2025-08-19T00:00:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/9a9e85bee7320a17e22e80d9c98d4cc0510407f791e8389726be0d9e199e/uv-0.8.12-py3-none-win_arm64.whl", hash = "sha256:0039141789ed96242dd6a48b10500138beafc2a63df0a4676363dceec4064d99", size = 18942201, upload-time = "2025-08-19T00:00:11.364Z" }, ] [[package]] name = "virtualenv" -version = "20.33.1" +version = "20.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, ] [[package]] @@ -3044,59 +3213,74 @@ wheels = [ [[package]] name = "zstandard" -version = "0.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, - { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, - { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, - { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, - { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, - { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, - { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, - { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, - { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, - { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, - { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, - { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, - { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, - { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, - { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, - { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, - { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, - { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, - { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/1b/c20b2ef1d987627765dcd5bf1dadb8ef6564f00a87972635099bb76b7a05/zstandard-0.24.0.tar.gz", hash = "sha256:fe3198b81c00032326342d973e526803f183f97aa9e9a98e3f897ebafe21178f", size = 905681, upload-time = "2025-08-17T18:36:36.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/1f/5c72806f76043c0ef9191a2b65281dacdf3b65b0828eb13bb2c987c4fb90/zstandard-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:addfc23e3bd5f4b6787b9ca95b2d09a1a67ad5a3c318daaa783ff90b2d3a366e", size = 795228, upload-time = "2025-08-17T18:21:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/3059bd5cd834666a789251d14417621b5c61233bd46e7d9023ea8bc1043a/zstandard-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b005bcee4be9c3984b355336283afe77b2defa76ed6b89332eced7b6fa68b68", size = 640520, upload-time = "2025-08-17T18:21:48.162Z" }, + { url = "https://files.pythonhosted.org/packages/57/07/f0e632bf783f915c1fdd0bf68614c4764cae9dd46ba32cbae4dd659592c3/zstandard-0.24.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:3f96a9130171e01dbb6c3d4d9925d604e2131a97f540e223b88ba45daf56d6fb", size = 5347682, upload-time = "2025-08-17T18:21:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4c/63523169fe84773a7462cd090b0989cb7c7a7f2a8b0a5fbf00009ba7d74d/zstandard-0.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd0d3d16e63873253bad22b413ec679cf6586e51b5772eb10733899832efec42", size = 5057650, upload-time = "2025-08-17T18:21:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/c6/16/49013f7ef80293f5cebf4c4229535a9f4c9416bbfd238560edc579815dbe/zstandard-0.24.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b7a8c30d9bf4bd5e4dcfe26900bef0fcd9749acde45cdf0b3c89e2052fda9a13", size = 5404893, upload-time = "2025-08-17T18:21:54.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/38/78e8bcb5fc32a63b055f2b99e0be49b506f2351d0180173674f516cf8a7a/zstandard-0.24.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:52cd7d9fa0a115c9446abb79b06a47171b7d916c35c10e0c3aa6f01d57561382", size = 5452389, upload-time = "2025-08-17T18:21:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/81671f05619edbacd49bd84ce6899a09fc8299be20c09ae92f6618ccb92d/zstandard-0.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0f6fc2ea6e07e20df48752e7700e02e1892c61f9a6bfbacaf2c5b24d5ad504b", size = 5558888, upload-time = "2025-08-17T18:21:58.68Z" }, + { url = "https://files.pythonhosted.org/packages/49/cc/e83feb2d7d22d1f88434defbaeb6e5e91f42a4f607b5d4d2d58912b69d67/zstandard-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e46eb6702691b24ddb3e31e88b4a499e31506991db3d3724a85bd1c5fc3cfe4e", size = 5048038, upload-time = "2025-08-17T18:22:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/7a5c57ff49ef8943877f85c23368c104c2aea510abb339a2dc31ad0a27c3/zstandard-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5e3b9310fd7f0d12edc75532cd9a56da6293840c84da90070d692e0bb15f186", size = 5573833, upload-time = "2025-08-17T18:22:02.402Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/64519983cd92535ba4bdd4ac26ac52db00040a52d6c4efb8d1764abcc343/zstandard-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76cdfe7f920738ea871f035568f82bad3328cbc8d98f1f6988264096b5264efd", size = 4961072, upload-time = "2025-08-17T18:22:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/72/ab/3a08a43067387d22994fc87c3113636aa34ccd2914a4d2d188ce365c5d85/zstandard-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f2fe35ec84908dddf0fbf66b35d7c2878dbe349552dd52e005c755d3493d61c", size = 5268462, upload-time = "2025-08-17T18:22:06.095Z" }, + { url = "https://files.pythonhosted.org/packages/49/cf/2abb3a1ad85aebe18c53e7eca73223f1546ddfa3bf4d2fb83fc5a064c5ca/zstandard-0.24.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:aa705beb74ab116563f4ce784fa94771f230c05d09ab5de9c397793e725bb1db", size = 5443319, upload-time = "2025-08-17T18:22:08.572Z" }, + { url = "https://files.pythonhosted.org/packages/40/42/0dd59fc2f68f1664cda11c3b26abdf987f4e57cb6b6b0f329520cd074552/zstandard-0.24.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:aadf32c389bb7f02b8ec5c243c38302b92c006da565e120dfcb7bf0378f4f848", size = 5822355, upload-time = "2025-08-17T18:22:10.537Z" }, + { url = "https://files.pythonhosted.org/packages/99/c0/ea4e640fd4f7d58d6f87a1e7aca11fb886ac24db277fbbb879336c912f63/zstandard-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e40cd0fc734aa1d4bd0e7ad102fd2a1aefa50ce9ef570005ffc2273c5442ddc3", size = 5365257, upload-time = "2025-08-17T18:22:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/27/a9/92da42a5c4e7e4003271f2e1f0efd1f37cfd565d763ad3604e9597980a1c/zstandard-0.24.0-cp311-cp311-win32.whl", hash = "sha256:cda61c46343809ecda43dc620d1333dd7433a25d0a252f2dcc7667f6331c7b61", size = 435559, upload-time = "2025-08-17T18:22:17.29Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/2c8e5c681ae4937c007938f954a060fa7c74f36273b289cabdb5ef0e9a7e/zstandard-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b95fc06489aa9388400d1aab01a83652bc040c9c087bd732eb214909d7fb0dd", size = 505070, upload-time = "2025-08-17T18:22:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/10/a2f27a66bec75e236b575c9f7b0d7d37004a03aa2dcde8e2decbe9ed7b4d/zstandard-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad9fd176ff6800a0cf52bcf59c71e5de4fa25bf3ba62b58800e0f84885344d34", size = 461507, upload-time = "2025-08-17T18:22:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/0bd281d9154bba7fc421a291e263911e1d69d6951aa80955b992a48289f6/zstandard-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2bda8f2790add22773ee7a4e43c90ea05598bffc94c21c40ae0a9000b0133c3", size = 795710, upload-time = "2025-08-17T18:22:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/b250a2eef515caf492e2d86732e75240cdac9d92b04383722b9753590c36/zstandard-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc76de75300f65b8eb574d855c12518dc25a075dadb41dd18f6322bda3fe15d5", size = 640336, upload-time = "2025-08-17T18:22:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/3ba6b522306d9bf097aac8547556b98a4f753dc807a170becaf30dcd6f01/zstandard-0.24.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d2b3b4bda1a025b10fe0269369475f420177f2cb06e0f9d32c95b4873c9f80b8", size = 5342533, upload-time = "2025-08-17T18:22:22.326Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ec/22bc75bf054e25accdf8e928bc68ab36b4466809729c554ff3a1c1c8bce6/zstandard-0.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b84c6c210684286e504022d11ec294d2b7922d66c823e87575d8b23eba7c81f", size = 5062837, upload-time = "2025-08-17T18:22:24.416Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/33edfc9d286e517fb5b51d9c3210e5bcfce578d02a675f994308ca587ae1/zstandard-0.24.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c59740682a686bf835a1a4d8d0ed1eefe31ac07f1c5a7ed5f2e72cf577692b00", size = 5393855, upload-time = "2025-08-17T18:22:26.786Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/59254e9b29da6215fb3a717812bf87192d89f190f23817d88cb8868c47ac/zstandard-0.24.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6324fde5cf5120fbf6541d5ff3c86011ec056e8d0f915d8e7822926a5377193a", size = 5451058, upload-time = "2025-08-17T18:22:28.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c7/31674cb2168b741bbbe71ce37dd397c9c671e73349d88ad3bca9e9fae25b/zstandard-0.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51a86bd963de3f36688553926a84e550d45d7f9745bd1947d79472eca27fcc75", size = 5546619, upload-time = "2025-08-17T18:22:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/e6/01/1a9f22239f08c00c156f2266db857545ece66a6fc0303d45c298564bc20b/zstandard-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d82ac87017b734f2fb70ff93818c66f0ad2c3810f61040f077ed38d924e19980", size = 5046676, upload-time = "2025-08-17T18:22:33.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/91/6c0cf8fa143a4988a0361380ac2ef0d7cb98a374704b389fbc38b5891712/zstandard-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92ea7855d5bcfb386c34557516c73753435fb2d4a014e2c9343b5f5ba148b5d8", size = 5576381, upload-time = "2025-08-17T18:22:35.391Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/1526080e22e78871e786ccf3c84bf5cec9ed25110a9585507d3c551da3d6/zstandard-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3adb4b5414febf074800d264ddf69ecade8c658837a83a19e8ab820e924c9933", size = 4953403, upload-time = "2025-08-17T18:22:37.266Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d0/a3a833930bff01eab697eb8abeafb0ab068438771fa066558d96d7dafbf9/zstandard-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6374feaf347e6b83ec13cc5dcfa70076f06d8f7ecd46cc71d58fac798ff08b76", size = 5267396, upload-time = "2025-08-17T18:22:39.757Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/90a0db9a61cd4769c06374297ecfcbbf66654f74cec89392519deba64d76/zstandard-0.24.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13fc548e214df08d896ee5f29e1f91ee35db14f733fef8eabea8dca6e451d1e2", size = 5433269, upload-time = "2025-08-17T18:22:42.131Z" }, + { url = "https://files.pythonhosted.org/packages/ce/58/fc6a71060dd67c26a9c5566e0d7c99248cbe5abfda6b3b65b8f1a28d59f7/zstandard-0.24.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0a416814608610abf5488889c74e43ffa0343ca6cf43957c6b6ec526212422da", size = 5814203, upload-time = "2025-08-17T18:22:44.017Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/89573d4393e3ecbfa425d9a4e391027f58d7810dec5cdb13a26e4cdeef5c/zstandard-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d66da2649bb0af4471699aeb7a83d6f59ae30236fb9f6b5d20fb618ef6c6777", size = 5359622, upload-time = "2025-08-17T18:22:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/60/ff/2cbab815d6f02a53a9d8d8703bc727d8408a2e508143ca9af6c3cca2054b/zstandard-0.24.0-cp312-cp312-win32.whl", hash = "sha256:ff19efaa33e7f136fe95f9bbcc90ab7fb60648453b03f95d1de3ab6997de0f32", size = 435968, upload-time = "2025-08-17T18:22:49.493Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/8f96b8ddb7ad12344218fbd0fd2805702dafd126ae9f8a1fb91eef7b33da/zstandard-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc05f8a875eb651d1cc62e12a4a0e6afa5cd0cc231381adb830d2e9c196ea895", size = 505195, upload-time = "2025-08-17T18:22:47.193Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4a/bfca20679da63bfc236634ef2e4b1b4254203098b0170e3511fee781351f/zstandard-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:b04c94718f7a8ed7cdd01b162b6caa1954b3c9d486f00ecbbd300f149d2b2606", size = 461605, upload-time = "2025-08-17T18:22:48.317Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/db949de3bf81ed122b8ee4db6a8d147a136fe070e1015f5a60d8a3966748/zstandard-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e4ebb000c0fe24a6d0f3534b6256844d9dbf042fdf003efe5cf40690cf4e0f3e", size = 795700, upload-time = "2025-08-17T18:22:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/99/56/fc04395d6f5eabd2fe6d86c0800d198969f3038385cb918bfbe94f2b0c62/zstandard-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:498f88f5109666c19531f0243a90d2fdd2252839cd6c8cc6e9213a3446670fa8", size = 640343, upload-time = "2025-08-17T18:22:51.999Z" }, + { url = "https://files.pythonhosted.org/packages/9b/0f/0b0e0d55f2f051d5117a0d62f4f9a8741b3647440c0ee1806b7bd47ed5ae/zstandard-0.24.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0a9e95ceb180ccd12a8b3437bac7e8a8a089c9094e39522900a8917745542184", size = 5342571, upload-time = "2025-08-17T18:22:53.734Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/d74e49f04fbd62d4b5d89aeb7a29d693fc637c60238f820cd5afe6ca8180/zstandard-0.24.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bcf69e0bcddbf2adcfafc1a7e864edcc204dd8171756d3a8f3340f6f6cc87b7b", size = 5062723, upload-time = "2025-08-17T18:22:55.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/97/df14384d4d6a004388e6ed07ded02933b5c7e0833a9150c57d0abc9545b7/zstandard-0.24.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:10e284748a7e7fbe2815ca62a9d6e84497d34cfdd0143fa9e8e208efa808d7c4", size = 5393282, upload-time = "2025-08-17T18:22:57.655Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/8f5c520e59a4d41591b30b7568595eda6fd71c08701bb316d15b7ed0613a/zstandard-0.24.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1bda8a85e5b9d5e73af2e61b23609a8cc1598c1b3b2473969912979205a1ff25", size = 5450895, upload-time = "2025-08-17T18:22:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/02aba892327a67ead8cba160ee835cfa1fc292a9dcb763639e30c07da58b/zstandard-0.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b14bc92af065d0534856bf1b30fc48753163ea673da98857ea4932be62079b1", size = 5546353, upload-time = "2025-08-17T18:23:01.457Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/96c52afcde44da6a5313a1f6c356349792079808f12d8b69a7d1d98ef353/zstandard-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b4f20417a4f511c656762b001ec827500cbee54d1810253c6ca2df2c0a307a5f", size = 5046404, upload-time = "2025-08-17T18:23:03.418Z" }, + { url = "https://files.pythonhosted.org/packages/da/b6/eefee6b92d341a7db7cd1b3885d42d30476a093720fb5c181e35b236d695/zstandard-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:337572a7340e1d92fd7fb5248c8300d0e91071002d92e0b8cabe8d9ae7b58159", size = 5576095, upload-time = "2025-08-17T18:23:05.331Z" }, + { url = "https://files.pythonhosted.org/packages/a3/29/743de3131f6239ba6611e17199581e6b5e0f03f268924d42468e29468ca0/zstandard-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df4be1cf6e8f0f2bbe2a3eabfff163ef592c84a40e1a20a8d7db7f27cfe08fc2", size = 4953448, upload-time = "2025-08-17T18:23:07.225Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/bd36ef49fba82e307d69d93b5abbdcdc47d6a0bcbc7ffbbfe0ef74c2fec5/zstandard-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6885ae4b33aee8835dbdb4249d3dfec09af55e705d74d9b660bfb9da51baaa8b", size = 5267388, upload-time = "2025-08-17T18:23:09.127Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/a4cfe1b871d3f1ce1f88f5c68d7e922e94be0043f3ae5ed58c11578d1e21/zstandard-0.24.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:663848a8bac4fdbba27feea2926049fdf7b55ec545d5b9aea096ef21e7f0b079", size = 5433383, upload-time = "2025-08-17T18:23:11.343Z" }, + { url = "https://files.pythonhosted.org/packages/77/26/f3fb85f00e732cca617d4b9cd1ffa6484f613ea07fad872a8bdc3a0ce753/zstandard-0.24.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:05d27c953f2e0a3ecc8edbe91d6827736acc4c04d0479672e0400ccdb23d818c", size = 5813988, upload-time = "2025-08-17T18:23:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8c/d7e3b424b73f3ce66e754595cbcb6d94ff49790c9ac37d50e40e8145cd44/zstandard-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77b8b7b98893eaf47da03d262816f01f251c2aa059c063ed8a45c50eada123a5", size = 5359756, upload-time = "2025-08-17T18:23:15.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/f1f0e11f1b295138f9da7e7ae22dcd9a1bb96a9544fa3b31507e431288f5/zstandard-0.24.0-cp313-cp313-win32.whl", hash = "sha256:cf7fbb4e54136e9a03c7ed7691843c4df6d2ecc854a2541f840665f4f2bb2edd", size = 435957, upload-time = "2025-08-17T18:23:18.835Z" }, + { url = "https://files.pythonhosted.org/packages/9f/03/ab8b82ae5eb49eca4d3662705399c44442666cc1ce45f44f2d263bb1ae31/zstandard-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:d64899cc0f33a8f446f1e60bffc21fa88b99f0e8208750d9144ea717610a80ce", size = 505171, upload-time = "2025-08-17T18:23:16.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/12/89a2ecdea4bc73a934a30b66a7cfac5af352beac94d46cf289e103b65c34/zstandard-0.24.0-cp313-cp313-win_arm64.whl", hash = "sha256:57be3abb4313e0dd625596376bbb607f40059d801d51c1a1da94d7477e63b255", size = 461596, upload-time = "2025-08-17T18:23:17.603Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/f3d2c4d64aacee4aab89e788783636884786b6f8334c819f09bff1aa207b/zstandard-0.24.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b7fa260dd2731afd0dfa47881c30239f422d00faee4b8b341d3e597cface1483", size = 795747, upload-time = "2025-08-17T18:23:19.968Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/9d3e5f6627e4cb5e511803788be1feee2f0c3b94594591e92b81db324253/zstandard-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e05d66239d14a04b4717998b736a25494372b1b2409339b04bf42aa4663bf251", size = 640475, upload-time = "2025-08-17T18:23:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/be/5d/48e66abf8c146d95330e5385633a8cfdd556fa8bd14856fe721590cbab2b/zstandard-0.24.0-cp314-cp314-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:622e1e04bd8a085994e02313ba06fbcf4f9ed9a488c6a77a8dbc0692abab6a38", size = 5343866, upload-time = "2025-08-17T18:23:23.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/6c/65fe7ba71220a551e082e4a52790487f1d6bb8dfc2156883e088f975ad6d/zstandard-0.24.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:55872e818598319f065e8192ebefecd6ac05f62a43f055ed71884b0a26218f41", size = 5062719, upload-time = "2025-08-17T18:23:25.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/68/15ed0a813ff91be80cc2a610ac42e0fc8d29daa737de247bbf4bab9429a1/zstandard-0.24.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb2446a55b3a0fd8aa02aa7194bd64740015464a2daaf160d2025204e1d7c282", size = 5393090, upload-time = "2025-08-17T18:23:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/d4/89/e560427b74fa2da6a12b8f3af8ee29104fe2bb069a25e7d314c35eec7732/zstandard-0.24.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2825a3951f945fb2613ded0f517d402b1e5a68e87e0ee65f5bd224a8333a9a46", size = 5450383, upload-time = "2025-08-17T18:23:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/0498328cbb1693885509f2fc145402b108b750a87a3af65b7250b10bd896/zstandard-0.24.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09887301001e7a81a3618156bc1759e48588de24bddfdd5b7a4364da9a8fbc20", size = 5546142, upload-time = "2025-08-17T18:23:31.281Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8a/64aa15a726594df3bf5d8decfec14fe20cd788c60890f44fcfc74d98c2cc/zstandard-0.24.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:98ca91dc9602cf351497d5600aa66e6d011a38c085a8237b370433fcb53e3409", size = 4953456, upload-time = "2025-08-17T18:23:33.234Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/e94879c5cd6017af57bcba08519ed1228b1ebb15681efd949f4a00199449/zstandard-0.24.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e69f8e534b4e254f523e2f9d4732cf9c169c327ca1ce0922682aac9a5ee01155", size = 5268287, upload-time = "2025-08-17T18:23:35.145Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/1a3b3a93f953dbe9e77e2a19be146e9cd2af31b67b1419d6cc8e8898d409/zstandard-0.24.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:444633b487a711e34f4bccc46a0c5dfbe1aee82c1a511e58cdc16f6bd66f187c", size = 5433197, upload-time = "2025-08-17T18:23:36.969Z" }, + { url = "https://files.pythonhosted.org/packages/39/83/b6eb1e1181de994b29804e1e0d2dc677bece4177f588c71653093cb4f6d5/zstandard-0.24.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7d3fe9e1483171e9183ffdb1fab07c5fef80a9c3840374a38ec2ab869ebae20", size = 5813161, upload-time = "2025-08-17T18:23:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d3/2fb4166561591e9d75e8e35c79182aa9456644e2f4536f29e51216d1c513/zstandard-0.24.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:27b6fa72b57824a3f7901fc9cc4ce1c1c834b28f3a43d1d4254c64c8f11149d4", size = 5359831, upload-time = "2025-08-17T18:23:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/6a9227315b774f64a67445f62152c69b4e5e49a52a3c7c4dad8520a55e20/zstandard-0.24.0-cp314-cp314-win32.whl", hash = "sha256:fdc7a52a4cdaf7293e10813fd6a3abc0c7753660db12a3b864ab1fb5a0c60c16", size = 444448, upload-time = "2025-08-17T18:23:45.151Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/67acaba311013e0798cb96d1a2685cb6edcdfc1cae378b297ea7b02c319f/zstandard-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:656ed895b28c7e42dd5b40dfcea3217cfc166b6b7eef88c3da2f5fc62484035b", size = 516075, upload-time = "2025-08-17T18:23:42.8Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/45fd8921263cea0228b20aa31bce47cc66016b2aba1afae1c6adcc3dbb1f/zstandard-0.24.0-cp314-cp314-win_arm64.whl", hash = "sha256:0101f835da7de08375f380192ff75135527e46e3f79bef224e3c49cb640fef6a", size = 476847, upload-time = "2025-08-17T18:23:43.892Z" }, ] From d5792a463f09da8f4d57f44bd6044916c60fd350 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 14:53:58 +1000 Subject: [PATCH 15/18] Provide default for socket --- docs/notebooks/run_mode.ipynb | 10 +++++----- src/async_kernel/kernel.py | 15 ++++++++------- tests/test_kernel.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/notebooks/run_mode.ipynb b/docs/notebooks/run_mode.ipynb index 8d4a561c0..0b9d499a0 100644 --- a/docs/notebooks/run_mode.ipynb +++ b/docs/notebooks/run_mode.ipynb @@ -53,11 +53,11 @@ "outputs": [], "source": [ "from async_kernel import utils\n", - "from async_kernel.typing import KernelConcurrencyMode, MsgType, RunMode, SocketID\n", + "from async_kernel.typing import KernelConcurrencyMode, MsgType, RunMode\n", "\n", "kernel = utils.get_kernel()\n", "\n", - "kernel.get_run_mode(SocketID.shell, MsgType.comm_msg)" + "kernel.get_run_mode(MsgType.comm_msg)" ] }, { @@ -88,10 +88,10 @@ "outputs": [], "source": [ "kernel.concurrency_mode = KernelConcurrencyMode.blocking\n", - "print(kernel.concurrency_mode, kernel.get_run_mode(SocketID.shell, MsgType.comm_msg))\n", + "print(kernel.concurrency_mode, kernel.get_run_mode(MsgType.comm_msg))\n", "\n", "kernel.concurrency_mode = KernelConcurrencyMode.default\n", - "print(kernel.concurrency_mode, kernel.get_run_mode(SocketID.shell, MsgType.comm_msg))" + "print(kernel.concurrency_mode, kernel.get_run_mode(MsgType.comm_msg))" ] }, { @@ -369,7 +369,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13.final.0" + "version": "3.13.6.final.0" } }, "nbformat": 4, diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 7973b565a..3c9ed858e 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -20,7 +20,6 @@ import time import traceback import uuid -from collections.abc import Callable from contextlib import asynccontextmanager from logging import Logger, LoggerAdapter from pathlib import Path @@ -59,7 +58,7 @@ ) if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Callable, Generator + from collections.abc import AsyncGenerator, Callable, Generator, Iterable from types import CoroutineType, FrameType from anyio.abc import TaskStatus @@ -609,7 +608,7 @@ async def handle_message_request(self, job: Job, /) -> None: handler = self.get_handler(socket_id, msg_type) except (ValueError, TypeError): return - run_mode = self.get_run_mode(socket_id, msg_type, job=job) + run_mode = self.get_run_mode(msg_type, socket_id=socket_id, job=job) self.log.debug("%s %s run mode %s handler: %s", socket_id, msg_type, run_mode, handler) job["run_mode"] = run_mode runner = _wrap_handler(self.run_handler, handler) @@ -625,9 +624,9 @@ async def handle_message_request(self, job: Job, /) -> None: def get_run_mode( self, - socket_id: SocketID, msg_type: MsgType, *, + socket_id: Literal[SocketID.shell, SocketID.control] = SocketID.shell, concurrency_mode: KernelConcurrencyMode | NoValue = NoValue, # pyright: ignore[reportInvalidTypeForm] job: Job | None = None, ) -> RunMode: @@ -683,6 +682,8 @@ def get_run_mode( def all_concurrency_run_modes( self, + socket_ids: Iterable[Literal[SocketID.shell, SocketID.control]] = (SocketID.shell, SocketID.control), + msg_types: Iterable[MsgType] = MsgType, ) -> dict[ Literal["SocketID", "KernelConcurrencyMode", "MsgType", "RunMode"], tuple[SocketID, KernelConcurrencyMode, MsgType, RunMode | None], @@ -690,11 +691,11 @@ def all_concurrency_run_modes( """Generates a dictionary containing all combinations of SocketID, KernelConcurrencyMode, and MsgType, along with their corresponding RunMode (if available).""" data: list[Any] = [] - for socket_id in [SocketID.shell, SocketID.control]: + for socket_id in socket_ids: for concurrency_mode in KernelConcurrencyMode: - for msg_type in MsgType: + for msg_type in msg_types: try: - mode = self.get_run_mode(socket_id, msg_type, concurrency_mode=concurrency_mode) + mode = self.get_run_mode(msg_type, socket_id=socket_id, concurrency_mode=concurrency_mode) except ValueError: mode = None data.append((socket_id, concurrency_mode, msg_type, mode)) diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 2e37cfefa..44f8f4dd2 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -528,7 +528,7 @@ async def test_get_run_mode( ): job["msg"]["content"]["code"] = code job["msg"]["content"]["silent"] = silent - mode = kernel.get_run_mode(socket_id, MsgType.execute_request, job=job) + mode = kernel.get_run_mode(MsgType.execute_request, socket_id=socket_id, job=job) assert mode is expected From 7a850cf552182929b0269ad637b5df621f648d58 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 15:22:12 +1000 Subject: [PATCH 16/18] Update ci --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++---- .github/workflows/pre-commit.yml | 14 ------------ CONTRIBUTING.md | 4 ++-- 3 files changed, 35 insertions(+), 20 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b0030515..9afa07e46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,12 +23,41 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install the project - run: uv sync --locked --dev - - - name: Run the test + - name: Run tests timeout-minutes: 5 run: uv run pytest -v - name: Type checking with basedpyright run: uv run basedpyright + + coverage: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: + - "3.12" + - "3.13" + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.8.6" + python-version: ${{ matrix.python-version }} + + - name: Run tests with coverage + run: uv run pytest -vv --cov + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - uses: pre-commit/action@v3.0.1 + - uses: pre-commit-ci/lite-action@v1.1.0 + if: always() diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 637fbad9d..000000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Pre-commit - -on: [push] -jobs: - main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - uses: pre-commit/action@v3.0.1 - - uses: pre-commit-ci/lite-action@v1.1.0 - if: always() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1a7218ed..d39ee743d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ uv lock --upgrade ## Running tests ```shell -pytest +uv run pytest ``` ## Running tests with coverage @@ -31,7 +31,7 @@ pytest We are aiming for 100% code coverage on CI (Linux). Any new code should also update tests to maintain coverage. ```shell -pytest -vv --cov +uv run pytest -vv --cov ``` ## Code Styling From 0c1d14da6afb89c3617a4d266c466c872221b0d1 Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 16:59:31 +1000 Subject: [PATCH 17/18] Fix tests and coverage --- .github/workflows/ci.yml | 2 +- .vscode/settings.json | 2 +- src/async_kernel/kernel.py | 33 +++++++++++++++++---------------- tests/conftest.py | 2 +- tests/test_debugger.py | 3 +++ tests/test_kernel.py | 21 ++++++++++++++++++--- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9afa07e46..0d03c7b6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Run tests with coverage - run: uv run pytest -vv --cov + run: uv run pytest -vv --cov --cov-fail-under=100 pre-commit: runs-on: ubuntu-latest diff --git a/.vscode/settings.json b/.vscode/settings.json index d4171cbc7..738977e10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,5 @@ "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" ], "spellright.language": ["en"], - "spellright.documentTypes": ["markdown", "latex", "plaintext", "python"] + "spellright.documentTypes": ["markdown", "latex", "plaintext"] } diff --git a/src/async_kernel/kernel.py b/src/async_kernel/kernel.py index 3c9ed858e..50315c78c 100644 --- a/src/async_kernel/kernel.py +++ b/src/async_kernel/kernel.py @@ -605,8 +605,9 @@ async def handle_message_request(self, job: Job, /) -> None: try: msg_type = MsgType(job["msg"]["header"]["msg_type"]) socket_id = job["socket_id"] - handler = self.get_handler(socket_id, msg_type) + handler = self.get_handler(msg_type) except (ValueError, TypeError): + self.log.debug("Invalid job %s", job) return run_mode = self.get_run_mode(msg_type, socket_id=socket_id, job=job) self.log.debug("%s %s run mode %s handler: %s", socket_id, msg_type, run_mode, handler) @@ -702,9 +703,9 @@ def all_concurrency_run_modes( data_ = zip(*data, strict=True) return dict(zip(["SocketID", "KernelConcurrencyMode", "MsgType", "RunMode"], data_, strict=True)) - def get_handler(self, socket_id: SocketID, msg_type: MsgType) -> HandlerType: + def get_handler(self, msg_type: MsgType) -> HandlerType: if not callable(f := getattr(self, msg_type, None)): - msg = "A handler was not found for " + msg = f"A handler was not found for {msg_type=}" raise TypeError(msg) return f # pyright: ignore[reportReturnType] @@ -778,11 +779,11 @@ def topic(self, topic) -> bytes: """prefixed topic for IOPub messages""" return (f"kernel.{topic}").encode() - async def kernel_info_request(self, job: Job[Content]) -> Content: + async def kernel_info_request(self, job: Job[Content], /) -> Content: """Handle a ke[rnel info request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info).""" return self.kernel_info - async def comm_info_request(self, job: Job[Content]) -> Content: + async def comm_info_request(self, job: Job[Content], /) -> Content: """Handle a [comm info request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info).""" c = job["msg"]["content"] target_name = c.get("target_name", None) @@ -793,7 +794,7 @@ async def comm_info_request(self, job: Job[Content]) -> Content: } return {"comms": comms} - async def execute_request(self, job: Job[ExecuteContent]) -> Content: + async def execute_request(self, job: Job[ExecuteContent], /) -> Content: """Handle a [execute request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute).""" c = job["msg"]["content"] if ( @@ -857,7 +858,7 @@ async def execute_request(self, job: Job[ExecuteContent]) -> Content: self._stop_on_error_time = time.monotonic() return content - async def complete_request(self, job: Job[Content]) -> Content: + async def complete_request(self, job: Job[Content], /) -> Content: """Handle a [completion request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion).""" c = job["msg"]["content"] code: str = c["code"] @@ -883,7 +884,7 @@ async def complete_request(self, job: Job[Content]) -> Content: "metadata": {"_jupyter_types_experimental": comps}, } - async def is_complete_request(self, job: Job[Content]) -> Content: + async def is_complete_request(self, job: Job[Content], /) -> Content: """Handle a [is_complete request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness).""" status, indent_spaces = self.shell.input_transformer_manager.check_complete(job["msg"]["content"]["code"]) content = {"status": status} @@ -891,7 +892,7 @@ async def is_complete_request(self, job: Job[Content]) -> Content: content["indent"] = " " * indent_spaces return content - async def inspect_request(self, job: Job[Content]) -> Content: + async def inspect_request(self, job: Job[Content], /) -> Content: """Handle a [inspect request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#introspection).""" c = job["msg"]["content"] detail_level = int(c.get("detail_level", 0)) @@ -910,7 +911,7 @@ async def inspect_request(self, job: Job[Content]) -> Content: content["found"] = False return content - async def history_request(self, job: Job[Content]) -> Content: + async def history_request(self, job: Job[Content], /) -> Content: """Handle a [history request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#history).""" c = job["msg"]["content"] history_manager = self.shell.history_manager @@ -929,19 +930,19 @@ async def history_request(self, job: Job[Content]) -> Content: hist = [] return {"history": list(hist)} - async def comm_open(self, job: Job[Content]) -> None: + async def comm_open(self, job: Job[Content], /) -> None: """Handle a [comm open request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#opening-a-comm).""" self.comm_manager.comm_open(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_msg(self, job: Job[Content]) -> None: + async def comm_msg(self, job: Job[Content], /) -> None: """Handle a [comm msg request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-messages).""" self.comm_manager.comm_msg(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def comm_close(self, job: Job[Content]) -> None: + async def comm_close(self, job: Job[Content], /) -> None: """Handle a [comm close request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#tearing-down-comms).""" self.comm_manager.comm_close(stream=job["socket"], ident=job["ident"], msg=job["msg"]) # pyright: ignore[reportArgumentType] - async def interrupt_request(self, job: Job[Content]) -> Content: + async def interrupt_request(self, job: Job[Content], /) -> Content: """Handle a [interrupt request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt) (control only).""" self._interrupt_requested = True if sys.platform == "win32": @@ -953,13 +954,13 @@ async def interrupt_request(self, job: Job[Content]) -> Content: interrupter() return {} - async def shutdown_request(self, job: Job[Content]) -> Content: + async def shutdown_request(self, job: Job[Content], /) -> Content: """Handle a [shutdown request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-shutdown) (control only).""" await self.debugger.disconnect() Caller().call_no_context(self.stop) return {"status": "ok", "restart": job["msg"]["content"].get("restart", False)} - async def debug_request(self, job: Job[Content]) -> Content: + async def debug_request(self, job: Job[Content], /) -> Content: """Handle a [debug request](https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request) (control only).""" return await self.debugger.process_request(job["msg"]["content"]) diff --git a/tests/conftest.py b/tests/conftest.py index 0a38fe0ea..e8cc91eff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,7 +32,6 @@ @pytest.hookimpl def pytest_configure(config): os.environ["PYTEST_TIMEOUT"] = str(1e6) if async_kernel.utils.LAUNCHED_BY_DEBUGPY else str(utils.TIMEOUT) - os.environ["MPLBACKEND"] = utils.MATPLOTLIB_INLINE_BACKEND @pytest.fixture(scope="module") @@ -52,6 +51,7 @@ async def kernel(anyio_backend, transport: str, tmp_path_factory): os.environ["IPYTHONDIR"] = str(tmp_path_factory.mktemp("ipython_config")) kernel = Kernel() kernel.connection_file = str(connection_file.resolve()) + os.environ["MPLBACKEND"] = utils.MATPLOTLIB_INLINE_BACKEND # Set this implicitly kernel.transport = transport async with kernel.start_in_context(): yield kernel diff --git a/tests/test_debugger.py b/tests/test_debugger.py index e76feb32e..d2a877139 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -68,6 +68,9 @@ async def test_debugger(subprocess_kernels_client): assert reply["status"] == "ok" assert reply["success"] + reply = await send_debug_request(client, "configurationDone") + assert reply["status"] == "ok" + # Debugger needs to be stopped on a breakpoint # The steps below expect the 'debugger' to be in a various state (stopped or running) code = """ diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 44f8f4dd2..5b49428df 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -5,6 +5,7 @@ from __future__ import annotations +import inspect import logging import pathlib import threading @@ -349,14 +350,16 @@ async def test_interrupt_request_blocking_exec_request(subprocess_kernels_client async def test_interrupt_request_blocking_task(subprocess_kernels_client): - code = f""" - {RunMode.task} - time.sleep(100) + code = """ + import time + from async_kernel import Caller + await Caller().call_soon(time.sleep, 100) """ client = subprocess_kernels_client msg_id = client.execute(code, reply=False) await utils.check_pub_message(client, msg_id, execution_state="busy") await utils.check_pub_message(client, msg_id, msg_type="execute_input") + await anyio.sleep(0.2) for _ in range(2): # Blocking calls in tasks need to be interrupted twice await utils.send_control_message(client, MsgType.interrupt_request) reply = await utils.get_reply(client, msg_id) @@ -509,6 +512,18 @@ async def test_invalid_message(client, channel): await utils.clear_iopub(client) +async def test_kernel_get_handler(kernel: Kernel): + with pytest.raises(TypeError): + kernel.get_handler("invalid mode") # pyright: ignore[reportArgumentType] + for msg_type in MsgType: + handler = kernel.get_handler(msg_type) + assert inspect.iscoroutinefunction(handler) + sig = inspect.signature(handler) + assert len(sig.parameters) == 1 + param = sig.parameters["job"] + assert param.kind == param.POSITIONAL_ONLY + + @pytest.mark.parametrize( ("code", "silent", "socket_id", "expected"), [ From ab4aa56d31e8f7acbc726f50b71832f2dc9098ae Mon Sep 17 00:00:00 2001 From: Alan Fleming Date: Wed, 20 Aug 2025 18:08:00 +1000 Subject: [PATCH 18/18] Rename workflows. --- .github/workflows/ci.yml | 3 ++- .github/workflows/publish-docs.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d03c7b6c..d11d7e1a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Async-kernel test +name: CI on: [push] jobs: @@ -49,6 +49,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Run tests with coverage + timeout-minutes: 5 run: uv run pytest -vv --cov --cov-fail-under=100 pre-commit: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 21fb99c61..9b9cc10cc 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,4 +1,4 @@ -name: docs +name: Publish docs on: push: branches: