pax_global_header00006660000000000000000000000064151042161230014505gustar00rootroot0000000000000052 comment=2059c85d94289b6a128b020b469d7b5ae169684a choreographer-1.2.1/000077500000000000000000000000001510421612300143365ustar00rootroot00000000000000choreographer-1.2.1/.github/000077500000000000000000000000001510421612300156765ustar00rootroot00000000000000choreographer-1.2.1/.github/workflows/000077500000000000000000000000001510421612300177335ustar00rootroot00000000000000choreographer-1.2.1/.github/workflows/publish_testpypi.yml000066400000000000000000000062651510421612300240760ustar00rootroot00000000000000# .github/workflows/publish_testpypi.yml --- name: test-n-build on: workflow_dispatch: push: tags: - v* jobs: super-test: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python_v: ['3.8', '3.9', '3.10', '3.12', '3.13', '3.14', '3.14t'] # chrome_v: ['-1'] name: Build and Test runs-on: ${{ matrix.os }} env: UV_PYTHON: ${{ matrix.python_v }} steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: astral-sh/setup-uv@v5 - name: Install Dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt-get update && sudo apt-get install xvfb timeout-minutes: 1 # must actually checkout for version determination - run: git checkout ${{ github.ref_name }} - run: uv python install ${{ matrix.python_v }} # don't modify sync file! messes up version! - run: uv sync --all-extras --locked --no-sources - run: git status - run: git diff --quiet HEAD || { echo "Working tree dirty"; exit 1; } - run: uv build - name: Reinstall from wheel run: > uv pip install dist/choreographer-$(uv run --no-sync --with setuptools-git-versioning setuptools-git-versioning)-py3-none-any.whl - run: uv run --no-sync python --version - run: uv pip freeze - run: uv run --no-sync choreo_get_chrome -v #--i ${{ matrix.chrome_v }} - name: Diagnose run: uv run --no-sync choreo_diagnose --no-run - name: Test if: ${{ ! runner.debug && matrix.os != 'ubuntu-latest' }} run: uv run --no-sync poe test timeout-minutes: 8 - name: Test (Linux) if: ${{ ! runner.debug && matrix.os == 'ubuntu-latest' }} run: xvfb-run uv run --no-sync poe test timeout-minutes: 8 - name: Test (Debug) if: ${{ runner.debug && matrix.os != 'ubuntu-latest' }} env: CHOREO_ENABLE_DEBUG: 1 run: uv run --no-sync poe debug-test timeout-minutes: 20 - name: Test (Debug, Linux) if: ${{ runner.debug && matrix.os == 'ubuntu-latest' }} env: CHOREO_ENABLE_DEBUG: 1 run: xvfb-run uv run --no-sync poe debug-test timeout-minutes: 8 testpypi-publish: name: Upload release to TestPyPI needs: super-test if: ${{ !cancelled() && !failure() && github.event_name == 'push' && github.run_attempt == 1 }} runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/choreographer # Signs this workflow so pypi trusts it permissions: id-token: write steps: - name: Checkout uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v4 - uses: actions/setup-python@v5 with: python-version-file: "pyproject.toml" - run: git checkout ${{ github.ref_name }} - run: uv sync --locked --all-extras --no-sources - run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/. choreographer-1.2.1/.github/workflows/ruff.yml000066400000000000000000000003031510421612300214140ustar00rootroot00000000000000--- name: ruff-wf on: pull_request jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/ruff-action@v3 with: src: 'src' choreographer-1.2.1/.github/workflows/test.yml000066400000000000000000000031441510421612300214370ustar00rootroot00000000000000--- name: test-wf on: pull_request: push: tags-ignore: - v* jobs: test-all: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - uses: actions/setup-python@v5 with: python-version-file: "pyproject.toml" - name: Install Dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt-get update && sudo apt-get install xvfb timeout-minutes: 1 - name: Install choreographer run: uv sync --no-sources --all-extras --locked - name: Install google-chrome-for-testing run: uv run --no-sources choreo_get_chrome -v - name: Diagnose run: uv run --no-sources choreo_diagnose --no-run timeout-minutes: 1 - name: Test if: ${{ ! runner.debug && matrix.os != 'ubuntu-latest' }} run: uv run --no-sources poe test timeout-minutes: 7 - name: Test (Linux) if: ${{ ! runner.debug && matrix.os == 'ubuntu-latest' }} run: xvfb-run uv run --no-sources poe test timeout-minutes: 7 - name: Test (Debug) if: ${{ runner.debug && matrix.os != 'ubuntu-latest' }} env: CHOREO_ENABLE_DEBUG: 1 run: uv run --no-sources poe debug-test timeout-minutes: 20 - name: Test (Debug, Linux) if: ${{ runner.debug && matrix.os == 'ubuntu-latest' }} env: CHOREO_ENABLE_DEBUG: 1 run: xvfb-run uv run --no-sources poe debug-test timeout-minutes: 7 choreographer-1.2.1/.gitignore000066400000000000000000000003101510421612300163200ustar00rootroot00000000000000# specific browser_exe # normal stuff .venv # ignore build artifacts *.egg-info/ build/ __pycache__ # editor files *.sw* # ipynb .ipynb_checkpoints/ # builds results/* !results/placeholder dist/ choreographer-1.2.1/.pre-commit-config.yaml000066400000000000000000000053011510421612300206160ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks %YAML 1.2 --- exclude: "site/.*" default_install_hook_types: [pre-commit, commit-msg] default_stages: [pre-commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-toml - id: debug-statements - repo: https://github.com/asottile/add-trailing-comma rev: v4.0.0 hooks: - id: add-trailing-comma - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.14.4 hooks: # Run the linter. - id: ruff types_or: [python, pyi] # Run the formatter. - id: ruff-format types_or: [python, pyi] # options: ignore one line things [E701] - repo: https://github.com/adrienverge/yamllint rev: v1.37.1 hooks: - id: yamllint name: yamllint description: This hook runs yamllint. entry: yamllint language: python types: [file, yaml] args: ['-d', "{\ extends: default,\ rules: {\ colons: { max-spaces-after: -1 }\ }\ }"] - repo: https://github.com/rhysd/actionlint rev: v1.7.8 hooks: - id: actionlint name: Lint GitHub Actions workflow files description: Runs actionlint to lint GitHub Actions workflow files language: golang types: ["yaml"] files: ^\.github/workflows/ entry: actionlint - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: - id: gitlint name: gitlint description: Checks your git commit messages for style. language: python additional_dependencies: ["./gitlint-core[trusted-deps]"] entry: gitlint args: [--staged, --msg-filename] stages: [commit-msg] - repo: https://github.com/crate-ci/typos rev: v1 hooks: - id: typos - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - id: detect-secrets name: Detect secrets language: python entry: detect-secrets-hook args: [''] - repo: https://github.com/rvben/rumdl-pre-commit rev: v0.0.173 # Use the latest release tag hooks: - id: rumdl # To only check (default): # args: [] # To automatically fix issues: # args: [--fix] - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.407 # pin a tag; latest as of 2025-10-01 hooks: - id: pyright choreographer-1.2.1/.python_version000066400000000000000000000000041510421612300174170ustar00rootroot000000000000003.8 choreographer-1.2.1/CHANGELOG.txt000066400000000000000000000045411510421612300163720ustar00rootroot00000000000000v1.2.1 - Use custom threadpool for functions that could be running during shutdown: Python's stdlib threadpool isn't available during interpreter shutdown, nor `atexit`- so they cannot be started or shutdown during `atexit`, or relied upon at all. We use a custom threadpool during `Browser.close()` so that it can be leveraged during atexit, and now we use it during `Browser.open()` since certain use patterns have that function running during shutdown. - Remove site directory - Improve error messaging - Organize functions internally - Improve choreo_diagnose - Add Roadmap - Bump logistro dependency v1.2.0 - Delete zipfile after downloading - Upgrade logistro to reduce sideeffects - Look for several chromium variants in all OSes by default - Replace ThreadpoolExecutor w/ manually managed pool, allowing use of `.close()` within `atexit`. v1.1.2 - Appease stricter typer - Add lock to mark channel open and is_ready function that checks open and close v1.1.1 - Fix bad module access v1.1.0 v1.1.0rc1 - Force custom json encoder to use stdlib json pkg v1.1.0rc0 - Add option to register a custom json encoder v1.0.10 - Simple typing fixes v1.0.9 - Serializer now accepts unsigned ints - Serializer better differentiates between pd/xr v1.0.8 - Lower default logging verbosity v1.0.7 - Revert ldd strategy to one of docs instead of injecting deps - Moved some verbose logging to DEBUG2 from debug - Make default download path public v1.0.6 - Package in chromium deps and use them if ldd shows they're needed - Add env var LDD_FAIL and flag --ldd_fail to always fail if deps needed - Add env var FORCED_PACKAGED_DEPS and flag --forced-packaged-deps to do as read - Fix some API bugs in choreo_diagnose v1.0.5 - Add Browser.is_isolated() returning if /tmp is sandboxed v1.0.4 - Fix to fetch latest known chrome version, not latest version v1.0.3 - Fix syntax to make compatible with python 3.8 - Remove lots of CLI commands - Improve logging v1.0.2 - no changes, noop v1.0.1 - Bugfix: Check for future cancellation before setting, improves error handling. - Improve error messages and add some type checking for user experience v1.0.0 - Increase wait for checking regular close - Decrease freeze for manual bad-close cleanup - Squash race condition - Improve parallelization by disabling site-per-process - Add option for whole new window with create_tab - General logging improvements choreographer-1.2.1/CODE_OF_CONDUCT.md000066400000000000000000000060071510421612300171400ustar00rootroot00000000000000# Code of Conduct In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * using welcoming and inclusive language, * being respectful of differing viewpoints and experiences, * gracefully accepting constructive criticism, * focusing on what is best for the community, and * showing empathy towards other community members. Examples of unacceptable behavior by participants include: * the use of sexualized language or imagery and unwelcome sexual attention or advances, * trolling, insulting/derogatory comments, and personal or political attacks, * public or private harassment, * publishing others' private information, such as a physical or electronic address, without explicit permission, and * other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/) version 1.4. choreographer-1.2.1/LICENSE.md000066400000000000000000000020521510421612300157410ustar00rootroot00000000000000# MIT License Copyright (c) Plotly, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. choreographer-1.2.1/README.md000066400000000000000000000115771510421612300156300ustar00rootroot00000000000000# Choreographer choreographer allows remote control of browsers from Python. It was created to support image generation from browser-based charting tools, but can be used for other purposes as well. choreographer is available [PyPI](https://pypi.org/project/choreographer) and [github](https://github.com/plotly/choreographer). ## Wait—I Thought This Was Kaleido? [Kaleido][kaleido] is a cross-platform library for generating static images of plots. The original implementation included a custom build of Chrome, which has proven very difficult to maintain. In contrast, this package uses the Chrome binary on the user's machine in the same way as testing tools like [Puppeteer][puppeteer]; the next step is to re-implement Kaleido as a layer on top of it. ## Status choreographer is a work in progress: only Chrome-ish browsers are supported at the moment, though we hope to add others. (Pull requests are greatly appreciated.) Note that we strongly recommend using async/await with this package, but it is not absolutely required. The synchronous functions in this package are intended as building blocks for other asynchronous strategies that Python may favor over async/await in the future. ## Testing ### Process Control Tests - Verbose: `pytest -W error -vvv tests/test_process.py` - Quiet:`pytest -W error -v tests/test_process.py` ### Browser Interaction Tests - Verbose: `pytest --debug -W error -vvv --ignore=tests/test_process.py` - Quiet :`pytest -W error -v --ignore=tests/test_process.py` You can also add "--no-headless" if you want to see the browser pop up. ### Writing Tests - Separate async and sync test files. Add `_sync.py` to synchronous tests. - For process tests, copy the fixtures in `test_process.py` file. - For API tests, use `test_placeholder.py` as the minimum template. ## Help Wanted We need your help to test this package on different platforms and for different use cases. To get started: 1. Clone this repository. 1. Create and activate a Python virtual environment. 1. Install this repository using `pip install .` or the equivalent. 1. Run `dtdoctor` and paste the output into an issue in this repository. ## Quickstart with `asyncio` Save the following code to `example.py` and run with Python. ```python import asyncio import choreographer as choreo async def example(): browser = await choreo.Browser(headless=False) tab = await browser.create_tab("https://google.com") await asyncio.sleep(3) await tab.send_command("Page.navigate", params={"url": "https://github.com"}) await asyncio.sleep(3) if __name__ == "__main__": asyncio.run(example()) ``` Step by step, this example: 1. Imports the required libraries. 1. Defines an `async` function (because `await` can only be used inside `async` functions). 1. Asks choreographer to create a browser. `headless=False` tells it to display the browser on the screen; the default is no display. 1. Wait three seconds for the browser to be created. 1. Create another tab. (Note that users can't rearrange programmatically-generated tabs using the mouse, but that's OK: we're not trying to replace testing tools like [Puppeteer][puppeteer].) 1. Sleep again. 1. Runs the example function. See [the devtools reference][devtools-ref] for a list of possible commands. ### Subscribing to Events Try adding the following to the example shown above: ```python # Callback for printing result async def dump_event(response): print(str(response)) # Callback for raising result as error async def error_event(response): raise Exception(str(response)) browser.subscribe("Target.targetCrashed", error_event) new_tab.subscribe("Page.loadEventFired", dump_event) browser.subscribe("Target.*", dump_event) # dumps all "Target" events response = await new_tab.subscribe_once("Page.lifecycleEvent") # do something with response browser.unsubscribe("Target.*") # events are always sent to a browser or tab, # but the documentation isn't always clear which. # Dumping all: `browser.subscribe("*", dump_event)` (on tab too) # can be useful (but verbose) for debugging. ``` ## Synchronous Use You can use this library without `asyncio`, ```python my_browser = choreo.Browser() # blocking until open ``` However, you must call `browser.pipe.read_jsons(blocking=True|False)` manually, and organizing the results. `browser.run_output_thread()` starts another thread constantly printing messages received from the browser but it can't be used with `asyncio` nor will it play nice with any other read. In other words, unless you're really, really sure you know what you're doing, use `asyncio`. ## Low-Level Use We provide a `Browser` and `Tab` interface, but there are lower-level `Target` and `Session` interfaces if needed. [devtools-ref]: https://chromedevtools.github.io/devtools-protocol/ [kaleido]: https://pypi.org/project/kaleido/ [puppeteer]: https://pptr.dev/ choreographer-1.2.1/ROADMAP.md000066400000000000000000000013531510421612300157450ustar00rootroot00000000000000# Roadmap - [ ] Working on better diagnostic information - [ ] Explain to user when their system has security restrictions - [ ] Eliminate synchronous API: it's unused, hard to maintain, and nearly worthless - [ ] Diagnose function should collect a JSON and then print that - [ ] Allow user to build and send their own JSONS - [ ] Get serialization out of the lock - [ ] Add a websockets extra - [ ] Support Firefox - [ ] Support LadyBird (!) - [ ] Test against multiple docker containers - [ ] Do browser-rolling tests - [ ] Do documentation - [ ] Browser Open/Close Status/PipeCheck status should happen at broker level - [ ] Broker should probably be opening browser and running watchdog... - [ ] Add a connect only for websockets choreographer-1.2.1/docs/000077500000000000000000000000001510421612300152665ustar00rootroot00000000000000choreographer-1.2.1/docs/Ubuntu_Errata.md000066400000000000000000000013011510421612300203630ustar00rootroot00000000000000# Ubuntu Security Workarounds ## Restrictions Ubuntu has special rules around programs installed outside of its package manager. ### Sandbox #### Root Because sandboxing depends on running subprocesses in underpriviledged modes using OS-level APIs, and if you run as root, all your root process have root-privileges, sandboxing wont work if run as root. #### Ubuntu Ubuntu doesn't let non-package-manager installed apps run w/ sandboxing tools. ### Temporary Files Ubuntu doesn't let apps share temporary files. ## Impacts Even though we recommend testing with our "known-later-version", we have to, in order to test sandbox, use the package manager of ubuntu. So sometimes there are failures. choreographer-1.2.1/mkdocs.yml000066400000000000000000000015151510421612300163430ustar00rootroot00000000000000--- ### Site metadata ### site_name: choreographer repo_name: github ### Build settings ### docs_dir: 'docs/' nav: - Readme: >- { "dest": "README.md", "src": "../README.md", "replace": {"src='docs/": "src='"} } - User API: >- { "api": "choreographer", "test": ["exports", "_prefix_local"], "tree": "none" } - Developer API: - >- { "api": "choreographer", "test": ["exports", "_prefix_local"], "tree": "packages" } # CLI tools? theme: name: material markdown_extensions: - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences plugins: - quimeta - quicopy - quiapi choreographer-1.2.1/pyproject.toml000066400000000000000000000106071510421612300172560ustar00rootroot00000000000000[build-system] requires = ["setuptools>=65.0.0", "wheel", "setuptools-git-versioning"] build-backend = "setuptools.build_meta" [tool.setuptools] [tool.setuptools.packages.find] where = ["src"] namespaces = false [tool.setuptools-git-versioning] enabled = true [tool.setuptools.package-data] choreographer = ['resources/last_known_good_chrome.json'] [project] name = "choreographer" description = "Devtools Protocol implementation for chrome." readme = "README.md" requires-python = ">=3.8" license = { "file" = "LICENSE.md" } dynamic = ["version"] authors = [ {name = "Andrew Pikul", email="ajpikul@gmail.com"}, {name = "Neyberson Atencio", email="neyberatencio@gmail.com"} ] maintainers = [ {name = "Andrew Pikul", email = "ajpikul@gmail.com"}, ] dependencies = [ "logistro>=2.0.1", "simplejson>=3.19.3", ] [project.urls] Homepage = "https://github.com/plotly/choreographer" Repository = "https://github.com/plotly/choreographer" [project.scripts] choreo_diagnose = "choreographer.cli._cli_utils_no_qa:diagnose" choreo_get_chrome = "choreographer.cli._cli_utils:get_chrome_cli" [dependency-groups] dev = [ "pytest", "pytest-asyncio; python_version < '3.14'", "pytest-asyncio>=1.2.0; python_version >= '3.14'", "pytest-xdist", "async-timeout", "numpy; python_version < '3.11'", "numpy>=2.3.3; python_version >= '3.11'", "mypy>=1.14.1", "types-simplejson>=3.19.0.20241221", "poethepoet>=0.30.0", "pyright>=1.1.406", ] # uv doens't allow dependency groups to have separate python requirements # it resolves everything all at once # this group we need to require higher python # and only resolve if explicitly asked for #docs = [ # "mkquixote @ git+ssh://git@github.com/geopozo/mkquixote; python_version >= '3.11'", # "mkdocs>=1.6.1", # "mkdocs-material>=9.5.49", #] [tool.uv.sources] #mkquixote = { path = "../mkquixote", editable = true } #logistro = { path = "../logistro", editable = true } [tool.ruff] src = ["src"] [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN", # no types "EM", # allow strings in raise(), despite python being ugly about it "TRY003", # allow long error messages inside raise() "D203", # No blank before class docstring (D211 = require blank line) "D212", # Commit message style docstring is D213, ignore D212 "COM812", # manual says linter rule conflicts with formatter "ISC001", # manual says litner rule conflicts with formatter "RET504", # Allow else if unnecessary because more readable "RET505", # Allow else if unnecessary because more readable "RET506", # Allow else if unnecessary because more readable "RET507", # Allow else if unnecessary because more readable "RET508", # Allow else if unnecessary because more readable "RUF012", # We don't do typing, so no typing "SIM105", # Too opionated (try-except-pass) "PT003", # scope="function" implied but I like readability "G004", # fstrings in my logs ] [tool.ruff.lint.per-file-ignores] "tests/*" = [ "D", # ignore docstring errors "S101", # allow assert "INP001", # no need for __init__ in test directories "T201", # if we're printing in tests, there is a reason "ERA001" ] [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" log_cli = false addopts = "--import-mode=append" [tool.poe] executor.type = "virtualenv" [tool.poe.tasks] test_proc = "pytest --log-level=1 -W error -n auto -v -rfE --capture=fd tests/test_process.py" test_fn = "pytest --log-level=1 -W error -n auto -v -rfE --capture=fd --ignore=tests/test_process.py" debug-test_proc = "pytest --log-level=1 -W error -vvvx -rA --show-capture=no --capture=no tests/test_process.py" debug-test_fn = "pytest --log-level=1 -W error -vvvx -rA --show-capture=no --capture=no --ignore=tests/test_process.py" [tool.poe.tasks.test] sequence = ["test_proc", "test_fn"] help = "Run all tests quickly" [tool.poe.tasks.debug-test] sequence = ["debug-test_proc", "debug-test_fn"] help = "Run test by test, slowly, quitting after first error" [tool.poe.tasks.filter-test] cmd = "pytest --log-level=1 -W error -vvvx -rA --capture=no --show-capture=no" help = "Run any/all tests one by one with basic settings: can include filename and -k filters" [tool.pyright] venvPath = "." venv = ".venv" include = ["src", "tests"] choreographer-1.2.1/src/000077500000000000000000000000001510421612300151255ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/000077500000000000000000000000001510421612300177555ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/__init__.py000066400000000000000000000012461510421612300220710ustar00rootroot00000000000000""" choreographer is a browser controller for python. choreographer is natively async, so while there are two main entrypoints: classes `Browser` and `BrowserSync`, the sync version is very limited, functioning as a building block for more featureful implementations. See the main README for a quickstart. """ import os if os.getenv("CHOREO_ENABLE_DEBUG"): import sys import logistro logistro.betterConfig(level=1) print("DEBUG MODE!", file=sys.stderr) # noqa: T201 from .browser_async import ( Browser, Tab, ) from .browser_sync import ( BrowserSync, TabSync, ) __all__ = [ "Browser", "BrowserSync", "Tab", "TabSync", ] choreographer-1.2.1/src/choreographer/_brokers/000077500000000000000000000000001510421612300215635ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/_brokers/__init__.py000066400000000000000000000003511510421612300236730ustar00rootroot00000000000000from ._async import Broker from ._sync import BrokerSync __all__ = [ "Broker", "BrokerSync", ] # note: should brokers be responsible for closing browser on bad pipe? # note: should the broker be the watchdog, in that case? choreographer-1.2.1/src/choreographer/_brokers/_async.py000066400000000000000000000313231510421612300234130ustar00rootroot00000000000000from __future__ import annotations import asyncio import warnings from functools import partial from typing import TYPE_CHECKING import logistro from choreographer import channels, protocol from choreographer.utils import _manual_thread_pool # afrom choreographer.channels import ChannelClosedError if TYPE_CHECKING: from typing import Any, MutableMapping from choreographer.browser_async import Browser from choreographer.channels._interface_type import ChannelInterface from choreographer.protocol.devtools_async import Session, Target _logger = logistro.getLogger(__name__) class UnhandledMessageWarning(UserWarning): pass class Broker: """Broker is a middleware implementation for asynchronous implementations.""" _browser: Browser """Browser is a reference to the Browser object this broker is brokering for.""" _channel: ChannelInterface """ Channel will be the ChannelInterface implementation (pipe or websocket) that the broker communicates on. """ futures: MutableMapping[protocol.MessageKey, asyncio.Future[Any]] """A mapping of all the futures for all sent commands.""" _subscriptions_futures: MutableMapping[ str, MutableMapping[ str, list[asyncio.Future[Any]], ], ] """A mapping of session id: subscription: list[futures]""" def __init__(self, browser: Browser, channel: ChannelInterface) -> None: """ Construct a broker for a synchronous arragenment w/ both ends. Args: browser: The sync browser implementation. channel: The channel the browser uses to talk on. """ self._browser = browser self._channel = channel self._background_tasks: set[asyncio.Task[Any]] = set() # if its a task you dont want canceled at close (like the close task) self._background_tasks_cancellable: set[asyncio.Task[Any]] = set() # if its a user task, can cancel self._current_read_task: asyncio.Task[Any] | None = None self.futures = {} self._subscriptions_futures = {} self._write_lock = asyncio.Lock() self._executor = _manual_thread_pool.ManualThreadExecutor( max_workers=2, name="readwrite_thread", ) def new_subscription_future( self, session_id: str, subscription: str, ) -> asyncio.Future[Any]: _logger.debug( f"Session {session_id} is subscribing to {subscription} one time.", ) if session_id not in self._subscriptions_futures: self._subscriptions_futures[session_id] = {} if subscription not in self._subscriptions_futures[session_id]: self._subscriptions_futures[session_id][subscription] = [] future = asyncio.get_running_loop().create_future() self._subscriptions_futures[session_id][subscription].append(future) return future def clean(self) -> None: _logger.debug("Cancelling message futures") for future in self.futures.values(): if not future.done(): _logger.debug2(f"Cancelling {future}") future.cancel() _logger.debug("Cancelling read task") if self._current_read_task and not self._current_read_task.done(): _logger.debug2(f"Cancelling read: {self._current_read_task}") self._current_read_task.cancel() _logger.debug("Cancelling subscription-futures") for session in self._subscriptions_futures.values(): for query in session.values(): for future in query: if not future.done(): _logger.debug2(f"Cancelling {future}") future.cancel() _logger.debug("Cancelling background tasks") for task in self._background_tasks_cancellable: if not task.done(): _logger.debug2(f"Cancelling {task}") task.cancel() self._executor.shutdown(wait=True, cancel_futures=True) def run_read_loop(self) -> None: # noqa: C901, PLR0915 complexity def check_read_loop_error(result: asyncio.Future[Any]) -> None: if result.cancelled(): _logger.debug("Readloop cancelled") return e = result.exception() if e: _logger.debug("Error in readloop. Will post a close() task.") self._background_tasks.add( asyncio.create_task(self._browser.close()), ) if isinstance(e, channels.ChannelClosedError): _logger.debug("PipeClosedError caught") _logger.debug2("Full Error:", exc_info=e) elif isinstance(e, asyncio.CancelledError): _logger.debug("CancelledError caught.") _logger.debug2("Full Error:", exc_info=e) else: _logger.error("Error in run_read_loop.", exc_info=e) raise e async def read_loop() -> None: # noqa: PLR0912, PLR0915, C901 loop = asyncio.get_running_loop() fn = partial(self._channel.read_jsons, blocking=True) responses = await loop.run_in_executor( executor=self._executor, func=fn, ) _logger.debug(f"Channel read found {len(responses)} json objects.") for response in responses: error = protocol.get_error_from_result(response) key = protocol.calculate_message_key(response) if not key and error: raise protocol.DevtoolsProtocolError(response) # looks for event that we should handle internally self._check_for_closed_session(response) # surrounding lines overlap in idea if protocol.is_event(response): event_session_id = response.get( "sessionId", "", ) _logger.debug2(f"Is event for {event_session_id}") x = self._get_target_session_by_session_id( event_session_id, ) if not x: continue _, event_session = x if not event_session: _logger.error("Found an event that returned no session.") continue _logger.debug( f"Received event {response['method']} for " f"{event_session_id} targeting {event_session}.", ) session_futures = self._subscriptions_futures.get( event_session_id, ) _logger.debug2( "Checking for event subscription future.", ) if session_futures: for query in session_futures: match = ( query.endswith("*") and response["method"].startswith(query[:-1]) ) or (response["method"] == query) if match: _logger.debug2( "Found event subscription future.", ) for future in session_futures[query]: if not future.done(): future.set_result(response) session_futures[query] = [] _logger.debug2( "Checking for event subscription callback.", ) for query in list(event_session.subscriptions): match = ( query.endswith("*") and response["method"].startswith(query[:-1]) ) or (response["method"] == query) _logger.debug2( "Found event subscription callback.", ) if match: t: asyncio.Task[Any] = asyncio.create_task( event_session.subscriptions[query][0](response), ) self._background_tasks_cancellable.add(t) if not event_session.subscriptions[query][1]: event_session.unsubscribe(query) elif key: _logger.debug(f"Have a response with key {key}") if key in self.futures: _logger.debug(f"Found future for key {key}") future = self.futures.pop(key) elif "error" in response: raise protocol.DevtoolsProtocolError(response) else: raise RuntimeError(f"Couldn't find a future for key: {key}") if not future.done(): future.set_result(response) else: warnings.warn( f"Unhandled message type:{response!s}", UnhandledMessageWarning, stacklevel=1, ) read_task = asyncio.create_task(read_loop()) read_task.add_done_callback(check_read_loop_error) self._current_read_task = read_task read_task = asyncio.create_task(read_loop()) read_task.add_done_callback(check_read_loop_error) self._current_read_task = read_task async def write_json( self, obj: protocol.BrowserCommand, ) -> protocol.BrowserResponse: protocol.verify_params(obj) key = protocol.calculate_message_key(obj) _logger.debug1(f"Broker writing {obj['method']} with key {key}") if not key: raise RuntimeError( "Message strangely formatted and " "choreographer couldn't figure it out why.", ) loop = asyncio.get_running_loop() future: asyncio.Future[protocol.BrowserResponse] = loop.create_future() self.futures[key] = future _logger.debug(f"Created future: {key} {future}") try: async with self._write_lock: # this should be a queue not a lock loop = asyncio.get_running_loop() await loop.run_in_executor( self._executor, self._channel.write_json, obj, ) except (_manual_thread_pool.ExecutorClosedError, asyncio.CancelledError) as e: if not future.cancel() or not future.cancelled(): await future # it wasn't canceled, so listen to it before raising raise channels.ChannelClosedError("Executor is closed.") from e except Exception as e: # noqa: BLE001 future.set_exception(e) del self.futures[key] _logger.debug(f"Future for {key} deleted.") return await future def _get_target_session_by_session_id( self, session_id: str, ) -> tuple[Target, Session] | None: if session_id == "": return (self._browser, self._browser.sessions[session_id]) for tab in self._browser.tabs.values(): if session_id in tab.sessions: return (tab, tab.sessions[session_id]) if session_id in self._browser.sessions: return (self._browser, self._browser.sessions[session_id]) return None def _check_for_closed_session(self, response: protocol.BrowserResponse) -> bool: if "method" in response and response["method"] == "Target.detachedFromTarget": session_closed = response["params"].get( "sessionId", "", ) if session_closed == "": _logger.debug2("Found closed session through events.") return True x = self._get_target_session_by_session_id(session_closed) if x: target_closed, _ = x else: return False if target_closed: target_closed._remove_session(session_closed) # noqa: SLF001 _logger.debug( "Using intern subscription key: " "'Target.detachedFromTarget'. " f"Session {session_closed} was closed.", ) _logger.debug2("Found closed session through events.") return True return False else: return False choreographer-1.2.1/src/choreographer/_brokers/_sync.py000066400000000000000000000045431510421612300232560ustar00rootroot00000000000000from __future__ import annotations import json from threading import Thread from typing import TYPE_CHECKING import logistro from choreographer import protocol from choreographer.channels import ChannelClosedError if TYPE_CHECKING: from typing import Any from choreographer.browser_sync import BrowserSync from choreographer.channels._interface_type import ChannelInterface _logger = logistro.getLogger(__name__) class BrokerSync: """BrokerSync is a middleware implementation for synchronous browsers.""" _browser: BrowserSync """Browser is a reference to the Browser object this broker is brokering for.""" _channel: ChannelInterface """ Channel will be the ChannelInterface implementation (pipe or websocket) that the broker communicates on. """ def __init__(self, browser: BrowserSync, channel: ChannelInterface) -> None: """ Construct a broker for a synchronous arragenment w/ both ends. Args: browser: The sync browser implementation. channel: The channel the browser uses to talk on. """ self._browser = browser self._channel = channel def run_output_thread(self, **kwargs: Any) -> None: """ Run a thread which dumps all browser messages. kwargs is passed to print. Raises: ChannelClosedError: When the channel is closed, this error is raised. """ def run_print() -> None: try: while True: responses = self._channel.read_jsons() for response in responses: print(json.dumps(response, indent=4), **kwargs) # noqa: T201 print in the point except ChannelClosedError: print("ChannelClosedError caught.", **kwargs) # noqa: T201 print is the point _logger.info("Starting thread to dump output to stdout.") Thread(target=run_print).start() def write_json(self, obj: protocol.BrowserCommand) -> protocol.MessageKey | None: """ Send an object down the channel. Args: obj: An object to be serialized to json and written to the channel. """ protocol.verify_params(obj) key = protocol.calculate_message_key(obj) self._channel.write_json(obj) return key def clean(self) -> None: pass choreographer-1.2.1/src/choreographer/browser_async.py000066400000000000000000000404401510421612300232110ustar00rootroot00000000000000"""Provides the async api: `Browser`, `Tab`.""" from __future__ import annotations import asyncio import os import subprocess import warnings from asyncio import Lock from typing import TYPE_CHECKING import logistro from choreographer import protocol from ._brokers import Broker from .browsers import BrowserClosedError, BrowserDepsError, BrowserFailedError, Chromium from .channels import ChannelClosedError, Pipe from .protocol.devtools_async import Session, Target from .utils import TmpDirWarning, _manual_thread_pool from .utils._kill import kill if TYPE_CHECKING: from pathlib import Path from types import TracebackType from typing import Any, Generator, MutableMapping from typing_extensions import Self # 3.9 needs this, could be from typing in 3.10 from .browsers._interface_type import BrowserImplInterface from .channels._interface_type import ChannelInterface _logger = logistro.getLogger(__name__) # Since I added locks to pipes, do we need locks here? class Tab(Target): """A wrapper for `Target`, so user can use `Tab`, not `Target`.""" async def close(self) -> None: """Close the tab.""" await self._broker._browser.close_tab(target_id=self.target_id) # noqa: SLF001 class Browser(Target): """`Browser` is the async implementation of `Browser`.""" subprocess: subprocess.Popen[bytes] | subprocess.Popen[str] """A reference to the `Popen` object.""" tabs: MutableMapping[str, Tab] """A mapping by target_id of all the targets which are open tabs.""" targets: MutableMapping[str, Target] """A mapping by target_id of ALL the targets.""" # Don't init instance attributes with mutables _watch_dog_task: asyncio.Task[Any] | None = None def _make_lock(self) -> None: self._open_lock = Lock() async def _is_open(self) -> bool: # Did we acquire the lock? If so, return true, we locked open. # If we are open, we did not lock open. # fuck, go through this again if self._open_lock.locked(): return True await self._open_lock.acquire() return False def _release_lock(self) -> bool: try: if self._open_lock.locked(): self._open_lock.release() return True else: return False except RuntimeError: return False def __init__( self, path: str | Path | None = None, *, browser_cls: type[BrowserImplInterface] = Chromium, channel_cls: type[ChannelInterface] = Pipe, **kwargs: Any, ) -> None: """ Construct a new browser instance. Args: path: The path to the browser executable. browser_cls: The type of browser (default: `Chromium`). channel_cls: The type of channel to browser (default: `Pipe`). kwargs: The arguments that the browser_cls takes. For example, headless=True/False, enable_gpu=True/False, etc. """ _logger.debug("Attempting to open new browser.") self._process_executor = _manual_thread_pool.ManualThreadExecutor( max_workers=3, name="checking_close", ) self._make_lock() self.tabs = {} self.targets = {} # Compose Resources self._channel = channel_cls() self._broker = Broker(self, self._channel) self._browser_impl = browser_cls(self._channel, path, **kwargs) def is_isolated(self) -> bool: """Return if process is isolated.""" return self._browser_impl.is_isolated() async def open(self) -> None: """Open the browser.""" _logger.info("Opening browser.") if await self._is_open(): raise RuntimeError("Can't re-open the browser") # asyncio's equiv doesn't work in all situations if hasattr(self._browser_impl, "logger_parser"): parser = self._browser_impl.logger_parser else: parser = None self._logger_pipe, _ = logistro.getPipeLogger( "browser_proc", parser=parser, ) def run() -> subprocess.Popen[bytes] | subprocess.Popen[str]: # depends on args self._browser_impl.pre_open() cli = self._browser_impl.get_cli() stderr = self._logger_pipe env = self._browser_impl.get_env() args = self._browser_impl.get_popen_args() return subprocess.Popen( # noqa: S603 cli, stderr=stderr, env=env, **args, ) _logger.debug("Trying to open browser.") loop = asyncio.get_running_loop() self.subprocess = await loop.run_in_executor( self._process_executor, run, ) super().__init__("0", self._broker) self._add_session(Session("", self._broker)) try: _logger.debug("Starting watchdog") self._watch_dog_task = asyncio.create_task(self._watchdog()) _logger.debug("Opening channel.") self._channel.open() # should this and below be in a broker run _logger.debug("Running read loop") self._broker.run_read_loop() _logger.debug("Populating Targets") await asyncio.sleep(0) # let watchdog start await self.populate_targets() except (BrowserClosedError, BrowserFailedError, asyncio.CancelledError) as e: if ( hasattr(self._browser_impl, "missing_libs") and self._browser_impl.missing_libs # type: ignore[reportAttributeAccessIssue] ): raise BrowserDepsError from e raise BrowserFailedError( "The browser seemed to close immediately after starting.", "You can set the `logging.Logger` level lower to see more output.", "You may try installing a known working copy of Chrome by running ", "`$ choreo_get_chrome`." "" "It may be your browser auto-updated and will now work upon " "restart. The browser we tried to start is located at " f"{self._browser_impl.path}.", ) from e async def __aenter__(self) -> Self: """Open browser as context to launch on entry and close on exit.""" await self.open() return self # for use with `await Browser()` def __await__(self) -> Generator[Any, Any, Browser]: """If you await the `Browser()`, it will implicitly call `open()`.""" return self.__aenter__().__await__() async def _is_closed(self, wait: int | None = 0) -> bool: if not hasattr(self, "subprocess"): return True if wait == 0: # poll returns None if its open _is_open = self.subprocess.poll() is None return not _is_open else: try: loop = asyncio.get_running_loop() await loop.run_in_executor( self._process_executor, self.subprocess.wait, wait, ) except subprocess.TimeoutExpired: return False except asyncio.CancelledError: return True return True # we encapsulate a portion of close that relates solely to browser shutdown # that's _close(), but close() handles everything around it async def _close(self) -> None: if await self._is_closed(): _logger.debug("No _close(), already is closed") return try: _logger.debug("Trying Browser.close") await self.send_command("Browser.close") except (BrowserClosedError, BrowserFailedError): _logger.debug("Browser is closed trying to send Browser.close") return except ChannelClosedError: _logger.debug("Can't send Browser.close on close channel") except asyncio.CancelledError: _logger.debug("Close was cancelled, _broker must be shutting down.") self._channel.close() if await self._is_closed(wait=3): return if await self._is_closed(): _logger.debug("Browser is closed after closing channel") return _logger.warning("Resorting to unclean kill browser.") kill(self.subprocess) if await self._is_closed(wait=6): return else: raise RuntimeError("Couldn't close or kill browser subprocess") async def close(self) -> None: """Close the browser.""" _logger.info("Closing browser.") if self._watch_dog_task: _logger.debug("Cancelling watchdog.") self._watch_dog_task.cancel() if not self._release_lock(): return # it can never be mid open here, because all of these must # run on the same thread. Do not push open or close to threads. try: _logger.debug("Starting browser close methods.") await self._close() _logger.debug("Browser close methods finished.") except ProcessLookupError: pass self._broker.clean() _logger.debug("Broker cleaned up.") if self._logger_pipe: os.close(self._logger_pipe) # subprocess has it open anyway # could have closed this copy immediately _logger.debug("Logging pipe closed.") self._channel.close() # was not blocky when comment written _logger.debug("Browser channel closed.") self._browser_impl.clean() # os blocky/hangy across networks _logger.debug("Browser implementation cleaned up.") self._process_executor.shutdown(wait=False, cancel_futures=True) async def __aexit__( self, type_: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, ) -> bool | None: """Close the browser.""" await self.close() return None async def _watchdog(self) -> None: _executor = _manual_thread_pool.ManualThreadExecutor( max_workers=1, name="watchdog_wait", ) try: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=TmpDirWarning) _logger.debug("In watchdog") loop = asyncio.get_running_loop() _logger.debug2("Running wait.") await loop.run_in_executor( _executor, self.subprocess.wait, ) _logger.warning("Wait expired, Browser is being closed by watchdog.") self._watch_dog_task = ( None # no need for close to cancel, we're going to finish soon ) await self.close() await asyncio.sleep(1) await loop.run_in_executor( _executor, self._browser_impl.clean, ) # this is a backup except asyncio.CancelledError: pass finally: _executor.shutdown(wait=False, cancel_futures=True) _logger.debug("Watchdog full shutdown (in finally:)") def _add_tab(self, tab: Tab) -> None: if not isinstance(tab, Tab): raise TypeError(f"tab must be an object of {self._tab_type}") self.tabs[tab.target_id] = tab def _remove_tab(self, target_id: str) -> None: if isinstance(target_id, Tab): target_id = target_id.target_id del self.tabs[target_id] def get_tab(self) -> Tab | None: """ Get the first tab if there is one. Useful for default tabs. Returns: A tab object. """ if self.tabs.values(): return next(iter(self.tabs.values())) return None async def populate_targets(self) -> None: """Solicit the actual browser for all targets to add to the browser object.""" if await self._is_closed(): raise BrowserClosedError("populate_targets() called on a closed browser") response = await self.send_command("Target.getTargets") if "error" in response: raise RuntimeError("Could not get targets") from Exception( response["error"], ) for json_response in response["result"]["targetInfos"]: if ( json_response["type"] == "page" and json_response["targetId"] not in self.tabs ): target_id = json_response["targetId"] new_tab = Tab(target_id, self._broker) try: await new_tab.create_session() except protocol.DevtoolsProtocolError as e: if e.code == protocol.Ecode.TARGET_NOT_FOUND: _logger.warning( f"Target {target_id} not found (could be closed before)", ) continue else: raise self._add_tab(new_tab) _logger.debug(f"The target {target_id} was added") async def create_session(self) -> Session: """ Create a browser session. Only in supported browsers, is experimental. Returns: A session object. """ if await self._is_closed(): raise BrowserClosedError("create_session() called on a closed browser") warnings.warn( # noqa: B028 "Creating new sessions on Browser() only works with some " "versions of Chrome, it is experimental.", protocol.ExperimentalFeatureWarning, ) response = await self.send_command("Target.attachToBrowserTarget") if "error" in response: raise RuntimeError( "Could not create session", ) from protocol.DevtoolsProtocolError( response, ) session_id = response["result"]["sessionId"] new_session = Session(session_id, self._broker) self._add_session(new_session) return new_session async def create_tab( self, url: str = "", width: int | None = None, height: int | None = None, *, window: bool = False, ) -> Tab: """ Create a new tab. Args: url: the url to navigate to, default "" width: the width of the tab (headless only) height: the height of the tab (headless only) window: default False, if true, create new window, not tab Returns: a tab. """ if await self._is_closed(): raise BrowserClosedError("create_tab() called on a closed browser.") params: MutableMapping[str, Any] = {"url": url} if width: params["width"] = width if height: params["height"] = height if window: params["newWindow"] = True response = await self.send_command("Target.createTarget", params=params) if "error" in response: raise RuntimeError( "Could not create tab", ) from protocol.DevtoolsProtocolError( response, ) target_id = response["result"]["targetId"] new_tab = Tab(target_id, self._broker) self._add_tab(new_tab) await new_tab.create_session() return new_tab async def close_tab(self, target_id: str) -> protocol.BrowserResponse: """ Close a tab by its id. Args: target_id: the targetId of the tab to close. """ if await self._is_closed(): raise BrowserClosedError("close_tab() called on a closed browser") if isinstance(target_id, Target): target_id = target_id.target_id # NOTE: we don't need to manually remove sessions because # sessions are intrinsically handled by events response = await self.send_command( command="Target.closeTarget", params={"targetId": target_id}, ) self._remove_tab(target_id) if "error" in response: raise RuntimeError( "Could not close tab", ) from protocol.DevtoolsProtocolError( response, ) return response choreographer-1.2.1/src/choreographer/browser_sync.py000066400000000000000000000146111510421612300230510ustar00rootroot00000000000000"""Provides the sync api: `BrowserSync`, `TabSync`.""" from __future__ import annotations import os import subprocess from threading import Lock from typing import TYPE_CHECKING import logistro from ._brokers import BrokerSync from .browsers import BrowserClosedError, BrowserFailedError, Chromium from .channels import ChannelClosedError, Pipe from .protocol.devtools_sync import SessionSync, TargetSync from .utils._kill import kill if TYPE_CHECKING: from pathlib import Path from types import TracebackType from typing import Any, MutableMapping from typing_extensions import Self # 3.9 needs this, could be from typing in 3.10 from .browsers._interface_type import BrowserImplInterface from .channels._interface_type import ChannelInterface _logger = logistro.getLogger(__name__) class TabSync(TargetSync): """A wrapper for `TargetSync`, so user can use `TabSync`, not `TargetSync`.""" class BrowserSync(TargetSync): """`BrowserSync` is the sync implementation of `Browser`.""" # A list of the types that are essential to use # with this class tabs: MutableMapping[str, TabSync] """A mapping by target_id of all the targets which are open tabs.""" targets: MutableMapping[str, TargetSync] """A mapping by target_id of ALL the targets.""" # Don't init instance attributes with mutables def _make_lock(self) -> None: self._open_lock = Lock() def _is_open(self) -> bool: return not self._open_lock.acquire(blocking=False) def _release_lock(self) -> bool: try: if self._open_lock.locked(): self._open_lock.release() return True else: return False except RuntimeError: return False def __init__( self, path: str | Path | None = None, *, browser_cls: type[BrowserImplInterface] = Chromium, channel_cls: type[ChannelInterface] = Pipe, **kwargs: Any, ) -> None: """ Construct a new browser instance. Args: path: The path to the browser executable. browser_cls: The type of browser (default: `Chromium`). channel_cls: The type of channel to browser (default: `Pipe`). kwargs: The arguments that the browser_cls takes. For example, headless=True/False, enable_gpu=True/False, etc. """ _logger.debug("Attempting to open new browser.") self._make_lock() self.tabs = {} self.targets = {} # Compose Resources self._channel = channel_cls() self._broker = BrokerSync(self, self._channel) self._browser_impl = browser_cls(self._channel, path, **kwargs) if hasattr(browser_cls, "logger_parser"): parser = browser_cls.logger_parser else: parser = None self._logger_pipe, _ = logistro.getPipeLogger( "browser_proc", parser=parser, ) def open(self) -> None: """Open the browser.""" if self._is_open(): raise RuntimeError("Can't re-open the browser") self.subprocess = subprocess.Popen( # noqa: S603 self._browser_impl.get_cli(), stderr=self._logger_pipe, env=self._browser_impl.get_env(), **self._browser_impl.get_popen_args(), ) super().__init__("0", self._broker) self._add_session(SessionSync("", self._broker)) self._channel.open() def __enter__(self) -> Self: """Open browser as context to launch on entry and close on exit.""" self.open() return self def _is_closed(self, wait: int | None = 0) -> bool: if wait == 0: return self.subprocess.poll() is None else: try: self.subprocess.wait(wait) except subprocess.TimeoutExpired: return False return True def _close(self) -> None: if self._is_closed(): return try: self.send_command("Browser.close") except (BrowserClosedError, BrowserFailedError): _logger.debug("Browser is closed trying to send Browser.close") return except ChannelClosedError: _logger.debug("Can send browser.close on close channel") self._channel.close() if self._is_closed(wait=3): return # try kiling kill(self.subprocess) if self._is_closed(wait=4): return else: raise RuntimeError("Couldn't close or kill browser subprocess") def close(self) -> None: """Close the browser.""" self._broker.clean() _logger.debug("Broker cleaned up.") if not self._release_lock(): return try: _logger.debug("Trying to close browser.") self._close() _logger.debug("browser._close() called successfully.") except ProcessLookupError: pass if self._logger_pipe: os.close(self._logger_pipe) _logger.debug("Logging pipe closed.") self._channel.close() _logger.debug("Browser channel closed.") self._browser_impl.clean() _logger.debug("Browser implementation cleaned up.") def __exit__( self, type_: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, ) -> None: # None instead of False is fine, eases type checking """Close the browser.""" self.close() def _add_tab(self, tab: TabSync) -> None: if not isinstance(tab, TabSync): raise TypeError("tab must be an object of TabSync") self.tabs[tab.target_id] = tab def _remove_tab(self, target_id: str) -> None: if isinstance(target_id, TabSync): target_id = target_id.target_id del self.tabs[target_id] def get_tab(self) -> TabSync | None: """Get the first tab if there is one. Useful for default tabs.""" if self.tabs.values(): return next(iter(self.tabs.values())) return None # wrap our broker for convenience def start_output_thread(self, **kwargs: Any) -> None: """ Start a separate thread that dumps all messages received to stdout. Args: kwargs: passed directly to print(). """ self._broker.run_output_thread(**kwargs) choreographer-1.2.1/src/choreographer/browsers/000077500000000000000000000000001510421612300216235ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/browsers/__init__.py000066400000000000000000000005211510421612300237320ustar00rootroot00000000000000"""Contains implementations of browsers that choreographer can open.""" from ._errors import BrowserClosedError, BrowserDepsError, BrowserFailedError from .chromium import ChromeNotFoundError, Chromium __all__ = [ "BrowserClosedError", "BrowserDepsError", "BrowserFailedError", "ChromeNotFoundError", "Chromium", ] choreographer-1.2.1/src/choreographer/browsers/_chrome_constants.py000066400000000000000000000063541510421612300257150ustar00rootroot00000000000000from __future__ import annotations import os import platform from dataclasses import dataclass chrome_names = ( "chrome", "Chrome", "google-chrome", "google-chrome-stable", "Chrome.app", "Google Chrome", "Google Chrome.app", "Google Chrome for Testing", ) chromium_names = ("chromium", "chromium-browser", "Chromium") edge_names = ( "msedge", "Microsoft Edge", "microsoft-edge", "microsoft-edge-beta", "microsoft-edge-dev", ) brave_names = ("brave", "Brave Browser", "brave-browser") vivaldi_names = ("vivaldi", "Vivaldi") if platform.system() == "Windows": _windows_app_dirs = [ _p for _p in [ os.environ.get("PROGRAMFILES", ""), os.environ.get("PROGRAMFILES(X86)", ""), os.environ.get("LOCALAPPDATA", ""), ] if _p ] _chrome_suffix = r"\Google\Chrome\Application\chrome.exe" typical_chrome_paths = tuple(_p + _chrome_suffix for _p in _windows_app_dirs) _chromium_suffix = r"\Chromium\Application\chrome.exe" typical_chromium_paths = tuple(_p + _chromium_suffix for _p in _windows_app_dirs) _edge_suffix = r"\Microsoft\Edge\Application\msedge.exe" typical_edge_paths = tuple(_p + _edge_suffix for _p in _windows_app_dirs) _brave_suffix = r"\BraveSoftware\Brave-Browser\Application\brave.exe" typical_brave_paths = tuple(_p + _edge_suffix for _p in _windows_app_dirs) _vivaldi_suffix = r"\Vivaldi\Application\vivaldi.exe" typical_vivaldi_paths = tuple(_p + _brave_suffix for _p in _windows_app_dirs) elif platform.system() == "Linux": typical_chrome_paths = ( "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/usr/bin/chrome", ) typical_chromium_paths = ( "/usr/bin/chromium", "/usr/bin/chromium-browser", ) typical_edge_paths = ( "/usr/bin/microsoft-edge", "/usr/bin/microsoft-edge-beta", "/usr/bin/microsoft-edge-dev", ) typical_brave_paths = ( "/usr/bin/brave-browser", "/opt/brave.com/brave/brave-browser", ) typical_vivaldi_paths = ("/usr/bin/vivaldi",) else: # assume mac, or system == "Darwin" typical_chrome_paths = ( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", ) typical_chromium_paths = ("/Applications/Chromium.app/Contents/MacOS/Chromium",) typical_edge_paths = ( "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", ) typical_brave_paths = ( "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", ) typical_vivaldi_paths = ("/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",) @dataclass(frozen=True) class BrowserInfo: __slots__ = ("exe_names", "ms_prog_id", "typical_paths") exe_names: tuple[str, ...] ms_prog_id: str typical_paths: tuple[str, ...] chromium_based_browsers = { "chrome": BrowserInfo(chrome_names, "ChromeHTML", typical_chrome_paths), "chromium": BrowserInfo(chromium_names, "ChromiumHTML", typical_chromium_paths), "edge": BrowserInfo(edge_names, "MSEdgeHTM", typical_edge_paths), "brave": BrowserInfo(brave_names, "BraveHTML", typical_brave_paths), "vivaldi": BrowserInfo(vivaldi_names, "VivaldiHTM", typical_vivaldi_paths), } choreographer-1.2.1/src/choreographer/browsers/_errors.py000066400000000000000000000027661510421612300236630ustar00rootroot00000000000000class BrowserClosedError(RuntimeError): """An error for when the browser is closed accidentally (during access).""" class BrowserFailedError(RuntimeError): """An error for when the browser fails to launch.""" class BrowserDepsError(BrowserFailedError): """An error for when the browser is closed because of missing libs.""" def __init__(self) -> None: msg = ( "It seems like you are running a slim version of your " "operating system and are missing some common dependencies. " "The following command should install the required " "dependencies on most systems:\n" "\n" "$ sudo apt update && sudo apt-get install libnss3 " "libatk-bridge2.0-0 libcups2 libxcomposite1 libxdamage1 " "libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 " "libcairo2 libasound2\n" "\n" "If you have already run the above command and are still " "seeing this error, or the above command fails, consult the " "Kaleido documentation for operating system to install " "chromium dependencies.\n" "\n" "For support, run the command `choreo_diagnose` and create " "an issue with its output." ) super().__init__(msg) # BrowserDeps being a more specific type of Failure. # And Closed not necessarily being related (you can intentionally closed, # and something else can error because its closed.) choreographer-1.2.1/src/choreographer/browsers/_interface_type.py000066400000000000000000000030541510421612300253370ustar00rootroot00000000000000"""Provides the basic protocol class (the abstract base) for a protocol.""" from __future__ import annotations from typing import TYPE_CHECKING, Protocol if TYPE_CHECKING: import logging from pathlib import Path from typing import Any, Mapping, MutableMapping, Sequence from choreographer.channels._interface_type import ChannelInterface class BrowserImplInterface(Protocol): """Defines the basic interface of a channel.""" path: str | Path | None """The OS path to the operating system.""" @classmethod def find_browser( cls, *, skip_local: bool, skip_typical: bool = False, ) -> str | None: ... ### Tries to find a working copy of itself, using our OS methods ### as well as its own methods. See the Chromium implementation. @classmethod def logger_parser( cls, record: logging.LogRecord, _old: MutableMapping[str, Any], ) -> bool: ... # This method will be used as the `filter()` method on the `logging.Filter()` # attached to all incoming logs from the browser process. # The log will be entirely ignored if return is False. def __init__( self, channel: ChannelInterface, path: Path | str | None = None, **kwargs: Any, ) -> None: ... def pre_open(self) -> None: ... def get_popen_args(self) -> Mapping[str, Any]: ... def get_cli(self) -> Sequence[str]: ... def get_env(self) -> MutableMapping[str, str]: ... def clean(self) -> None: ... def is_isolated(self) -> bool: ... choreographer-1.2.1/src/choreographer/browsers/_unix_pipe_chromium_wrapper.py000066400000000000000000000032451510421612300300030ustar00rootroot00000000000000""" _unix_pipe_chromium_wrapper.py provides proper fds to chrome. By running chromium in a new process (this wrapper), we guarantee the user hasn't stolen one of our desired file descriptors, which the OS gives away first-come-first-serve everytime someone opens a file. chromium demands we use 3 and 4. """ from __future__ import annotations import os # importing modules has side effects, so we do this before imports # ruff: noqa: E402 # chromium reads on 3, writes on 4 os.dup2(0, 3) # make our stdin their input os.dup2(1, 4) # make our stdout their output _inheritable = True os.set_inheritable(4, _inheritable) os.set_inheritable(3, _inheritable) import signal import subprocess import sys from functools import partial from typing import TYPE_CHECKING import logistro if TYPE_CHECKING: from types import FrameType _logger = logistro.getLogger("chrome_wrapper") # we're a wrapper, the cli is everything that came after us cli = sys.argv[1:] print(f"wrapper CLI: {cli}", file=sys.stderr) # noqa: T201 goes to pipe/logger anyway process = subprocess.Popen(cli, pass_fds=(3, 4)) # noqa: S603 untrusted input def kill_proc( process: subprocess.Popen[bytes], _sig_num: int, _frame: FrameType | None, ) -> None: process.terminate() process.wait(5) # 5 seconds to clean up nicely, it's a lot process.kill() kp = partial(kill_proc, process) signal.signal(signal.SIGTERM, kp) signal.signal(signal.SIGINT, kp) process.wait() # not great but it seems that # pipe isn't always closed when chrome closes # so we pretend to be chrome and send a bye instead # also, above depends on async/sync, platform, etc print("{bye}") # noqa: T201 we need print here choreographer-1.2.1/src/choreographer/browsers/chromium.py000066400000000000000000000262611510421612300240270ustar00rootroot00000000000000"""Provides a class proving tools for running chromium browsers.""" from __future__ import annotations import os import platform import re import subprocess import sys from pathlib import Path from typing import TYPE_CHECKING import logistro if platform.system() == "Windows": import msvcrt from choreographer.channels import Pipe from choreographer.utils import TmpDirectory, get_browser_path from ._chrome_constants import chromium_based_browsers if TYPE_CHECKING: import logging from typing import Any, Mapping, MutableMapping, Sequence from choreographer.channels._interface_type import ChannelInterface _chromium_wrapper_path = ( Path(__file__).resolve().parent / "_unix_pipe_chromium_wrapper.py" ) _packaged_chromium_libs = Path(__file__).resolve().parent / "packaged_chromium_libs" _logger = logistro.getLogger(__name__) def _is_exe(path: str | Path) -> bool: try: return os.access(path, os.X_OK) except: # noqa: E722 bare except ok, weird errors, best effort. return False _logs_parser_regex = re.compile(r"\d*:\d*:\d*\/\d*\.\d*:") class ChromeNotFoundError(RuntimeError): """Raise when browser path can't be determined.""" class Chromium: """ Chromium represents an implementation of the chromium browser. It also includes chromium-like browsers (chrome, edge, and brave). """ path: str | Path | None """The path to the chromium executable.""" gpu_enabled: bool """True if we should use the gpu. False by default for compatibility.""" headless: bool """True if we should not show the browser, true by default.""" sandbox_enabled: bool """True to enable the sandbox. False by default.""" skip_local: bool """True if we want to avoid looking for our local download when searching path.""" tmp_dir: TmpDirectory """A reference to a temporary directory object the chromium needs to store data.""" @classmethod def find_browser( cls, *, skip_local: bool, skip_typical: bool = False, ) -> str | None: """Find a chromium based browser.""" for name, browser_data in chromium_based_browsers.items(): _logger.debug(f"Looking for a {name} browser.") path = get_browser_path( executable_names=browser_data.exe_names, skip_local=skip_local, ms_prog_id=browser_data.ms_prog_id, ) if not path and not skip_typical: for candidate in browser_data.typical_paths: if _is_exe(candidate): path = candidate break if path: return path return None @classmethod def logger_parser( cls, record: logging.LogRecord, _old: MutableMapping[str, Any], ) -> bool: """ Remove chromium timestamp from chromium's logs. This method will be used as the `filter()` method on the `logging.Filter()` attached to all incoming logs from the browser process. Args: record: the `logging.LogRecord` object to read/modify _old: data that was already stripped out. """ # replace the chromium timestamp because we do our own record.msg = _logs_parser_regex.sub("", record.msg) return True def _libs_ok(self) -> bool: """Return true if libs ok.""" if self.skip_local: _logger.debug( "If we HAVE to skip local.", ) return True _logger.debug("Checking for libs needed.") if platform.system() != "Linux": _logger.debug("We're not in linux, so no need for check.") return True p = None try: _logger.debug(f"Trying ldd {self.path}") p = subprocess.run( # noqa: S603, validating run with variables [ # noqa: S607 path is all we have "ldd", str(self.path), ], capture_output=True, timeout=5, check=True, ) except Exception as e: # noqa: BLE001 msg = "ldd failed." stderr = p.stderr.decode() if p and p.stderr else None # Log failure as INFO rather than WARNING so that it's hidden by default, # since browser may succeed even if ldd fails _logger.info( msg # noqa: G003 + in log + f" e: {e}, stderr: {stderr}", ) return False if b"not found" in p.stdout: msg = "Found deps missing in chrome" _logger.debug2(msg + f" {p.stdout.decode()}") return False _logger.debug("No problems found with dependencies") return True def __init__( self, channel: ChannelInterface, path: Path | str | None = None, **kwargs: Any, ) -> None: """ Construct a chromium browser implementation. Args: channel: the `choreographer.Channel` we'll be using (WebSockets? Pipe?) path: path to the browser kwargs: gpu_enabled (default False): Turn on GPU? Doesn't work in all envs. headless (default True): Actually launch a browser? sandbox_enabled (default False): Enable sandbox- a persnickety thing depending on environment, OS, user, etc tmp_dir (default None): Manually set the temporary directory Raises: RuntimeError: Too many kwargs, or browser not found. NotImplementedError: Pipe is the only channel type it'll accept right now. """ _logger.info(f"Chromium init'ed with kwargs {kwargs}") self.path = path self.gpu_enabled = kwargs.pop("enable_gpu", False) self.headless = kwargs.pop("headless", True) self.sandbox_enabled = kwargs.pop("enable_sandbox", False) self._tmp_dir_path = kwargs.pop("tmp_dir", None) if kwargs: raise RuntimeError( f"Chromium.get_cli() received invalid args: {kwargs.keys()}", ) self.skip_local = bool( "ubuntu" in platform.version().lower() and self.sandbox_enabled, ) if self.skip_local: _logger.warning( "Forced skipping local. Ubuntu sandbox requires package manager.", ) if not self.path: self.path = Chromium.find_browser(skip_local=self.skip_local) if not self.path: raise ChromeNotFoundError( "Browser not found. You can use get_chrome() or " "choreo_get_chrome from bash. please see documentation. " f"Local copy ignored: {self.skip_local}.", ) _logger.info(f"Found chromium path: {self.path}") self._channel = channel if not isinstance(channel, Pipe): raise NotImplementedError("Websocket style channels not implemented yet.") self._is_isolated = "snap" in str(self.path) def pre_open(self) -> None: """Prepare browser for opening.""" self.tmp_dir = TmpDirectory( path=self._tmp_dir_path, sneak=self._is_isolated, ) self.missing_libs = not self._libs_ok() _logger.info(f"Temporary directory at: {self.tmp_dir.path}") def is_isolated(self) -> bool: """ Return if /tmp directory is isolated by OS. Returns: bool indicating if /tmp is isolated. """ return self._is_isolated def get_popen_args(self) -> Mapping[str, Any]: """Return the args needed to runc chromium with `subprocess.Popen()`.""" args = {} # need to check pipe if platform.system() == "Windows": args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore [attr-defined] args["close_fds"] = False else: args["close_fds"] = True if isinstance(self._channel, Pipe): args["stdin"] = self._channel.from_choreo_to_external args["stdout"] = self._channel.from_external_to_choreo _logger.debug(f"Returning args: {args}") return args def get_cli(self) -> Sequence[str]: """Return the CLI command for chromium.""" if platform.system() != "Windows": cli = [ str(sys.executable), str(_chromium_wrapper_path), str(self.path), ] else: cli = [ str(self.path), ] if not self.gpu_enabled: cli.append("--disable-gpu") if self.headless: cli.append("--headless") if not self.sandbox_enabled: cli.append("--no-sandbox") cli.extend( [ "--disable-breakpad", "--allow-file-access-from-files", "--enable-logging=stderr", f"--user-data-dir={self.tmp_dir.path}", "--no-first-run", "--enable-unsafe-swiftshader", "--disable-dev-shm-usage", "--disable-background-media-suspend", "--disable-lazy-loading", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding", "--disable-component-update", "--disable-hang-monitor", "--disable-popup-blocking", "--disable-prompt-on-repost", "--disable-ipc-flooding-protection", "--disable-sync", "--metrics-recording-only", "--password-store=basic", "--use-mock-keychain", "--no-default-browser-check", "--no-process-per-site", "--disable-web-security", ], ) if isinstance(self._channel, Pipe): cli.append("--remote-debugging-pipe") if platform.system() == "Windows": # its gonna read on 3 # its gonna write on 4 r_handle = msvcrt.get_osfhandle(self._channel.from_choreo_to_external) # type: ignore [attr-defined] w_handle = msvcrt.get_osfhandle(self._channel.from_external_to_choreo) # type: ignore [attr-defined] _inheritable = True os.set_handle_inheritable(r_handle, _inheritable) # type: ignore [attr-defined] os.set_handle_inheritable(w_handle, _inheritable) # type: ignore [attr-defined] cli += [ f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}", ] _logger.debug(f"Returning cli: {cli}") return cli def get_env(self) -> MutableMapping[str, str]: """Return the env needed for chromium.""" env = os.environ.copy() return env def clean(self) -> None: """Clean up any leftovers form browser, like tmp files.""" if hasattr(self, "tmp_dir"): self.tmp_dir.clean() def __del__(self) -> None: """Delete the temporary file and run `clean()`.""" self.clean() choreographer-1.2.1/src/choreographer/channels/000077500000000000000000000000001510421612300215505ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/channels/README.txt000066400000000000000000000014471510421612300232540ustar00rootroot00000000000000Browsers often accept two communication channels: websockets and pipes. Both classes we implement will support `write_jsons()` and `read_jsons()` with the same interface (as well as `__init__()` and `close()`). But the browser implementation in _browsers/ will have to get specific information from the pipe/websocket in order to properly build the CLI command for the browser. For example, the CLI command needs to know the file numbers (file descriptors) for reading writing if using `Pipe()`, so `Pipe()` has the attributes: `.from_external_to_choreo` and `.from_choreo_to_external`. They're part of the interface as well. In the same vein, when websockets in implemented, the CLI command will need to know the port it's on. There may need to be a `open()` function for after the CLI command is run. choreographer-1.2.1/src/choreographer/channels/__init__.py000066400000000000000000000005611510421612300236630ustar00rootroot00000000000000""" Channels are classes that choreo and the browser use to communicate. This is a low-level part of the API. """ from ._errors import BlockWarning, ChannelClosedError, JSONError from ._wire import register_custom_encoder from .pipe import Pipe __all__ = [ "BlockWarning", "ChannelClosedError", "JSONError", "Pipe", "register_custom_encoder", ] choreographer-1.2.1/src/choreographer/channels/_errors.py000066400000000000000000000004521510421612300235760ustar00rootroot00000000000000class BlockWarning(UserWarning): """A warning for when block modification operations used on incompatible OS.""" class ChannelClosedError(IOError): """An error to throw when the channel has closed from either end or error.""" class JSONError(RuntimeError): """Another JSONError.""" choreographer-1.2.1/src/choreographer/channels/_interface_type.py000066400000000000000000000023551510421612300252670ustar00rootroot00000000000000"""Provides the basic protocol class (the abstract interface) for a channel.""" from __future__ import annotations from typing import TYPE_CHECKING, Protocol if TYPE_CHECKING: from typing import Any, Mapping, Sequence from choreographer.protocol import BrowserResponse class ChannelInterface(Protocol): """Defines the basic interface of a channel.""" # Not sure I like the obj type def write_json(self, obj: Mapping[str, Any]) -> None: ... # """ # Accept an object and send it doesnt the channel serialized. # # Args: # obj: the object to send to the browser. # # """ def read_jsons(self, *, blocking: bool = True) -> Sequence[BrowserResponse]: ... # """ # Read all available jsons in the channel and returns a list of complete ones. # # Args: # blocking: should this method block on read or return immediately. # """ def close(self) -> None: ... # """Close the channel.""" def open(self) -> None: ... # """Open the channel.""" def is_ready(self) -> bool: ... # """Return true if comm channel is active.""" # Can't docstring protocols! EW! choreographer-1.2.1/src/choreographer/channels/_wire.py000066400000000000000000000037651510421612300232420ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING import logistro import simplejson from simplejson import errors as sjerrors from ._errors import JSONError if TYPE_CHECKING: from typing import Any _logger = logistro.getLogger(__name__) _custom_encoder: type[json.JSONEncoder] | None = None def register_custom_encoder(e: type[json.JSONEncoder] | None) -> None: global _custom_encoder # noqa: PLW0603 what other choice do we have _custom_encoder = e class MultiEncoder(simplejson.JSONEncoder): """Special json encoder for numpy types.""" # docs say subclassing inferior, just pass this method as a kward def default(self, o: Any) -> Any: if hasattr(o, "dtype") and o.dtype.kind in ("i", "u") and o.shape == (): return int(o) elif hasattr(o, "dtype") and o.dtype.kind == "f" and o.shape == (): return float(o) elif hasattr(o, "dtype") and o.shape != (): if hasattr(o, "values") and hasattr(o.values, "tolist"): return o.values.tolist() if hasattr(o, "tolist"): return o.tolist() elif hasattr(o, "isoformat"): return o.isoformat() return simplejson.JSONEncoder.default(self, o) def serialize(obj: Any) -> bytes: try: if not _custom_encoder: message = simplejson.dumps( obj, ensure_ascii=False, ignore_nan=True, cls=MultiEncoder, ) else: message = json.dumps(obj, cls=_custom_encoder) except (json.JSONDecodeError, simplejson.JSONDecodeError) as e: raise JSONError from e _logger.debug(f"Serialized: {message[:15]}...{message[-15:]}, size: {len(message)}") _logger.debug2(f"Whole message: {message}") return message.encode("utf-8") def deserialize(message: str) -> Any: try: return simplejson.loads(message) except sjerrors.JSONDecodeError as e: raise JSONError from e choreographer-1.2.1/src/choreographer/channels/pipe.py000066400000000000000000000211561510421612300230640ustar00rootroot00000000000000"""Provides a channel based on operating system file pipes.""" from __future__ import annotations import os import platform import sys import warnings from threading import Lock from typing import TYPE_CHECKING import logistro from . import _wire as wire from ._errors import BlockWarning, ChannelClosedError, JSONError if TYPE_CHECKING: from typing import Any, Mapping, Sequence from choreographer.protocol import BrowserResponse _with_block = bool(sys.version_info[:3] >= (3, 12) or platform.system() != "Windows") _logger = logistro.getLogger(__name__) # should be closing my ends from the start? # if we're a pipe we expect these public attributes class Pipe: """Defines an operating system pipe.""" from_external_to_choreo: int """Consumers need this, it is the channel the browser uses to talk to choreo.""" from_choreo_to_external: int """Consumers needs this, it is the channel choreo writes to the browser on.""" shutdown_lock: Lock """Once this is locked, the pipe is closed and can't be reopened.""" def __init__(self) -> None: """Construct a pipe using os functions.""" # This is where pipe listens (from browser) # So pass the write to browser self._read_from_browser, self._write_from_browser = list(os.pipe()) # This is where pipe writes (to browser) # So pass the read to browser self._read_to_browser, self._write_to_browser = list(os.pipe()) # Popen will write stdout of wrapper to this (dupping 4) # Browser will write directly to this if not using wrapper self.from_external_to_choreo = self._write_from_browser # Popen will read this into stdin of wrapper (dupping 3) # Browser will read directly from this if not using wrapper # which dupes stdin to expected fd (4?) self.from_choreo_to_external = self._read_to_browser # These won't be used on windows directly, they'll be t-formed to # windows-style handles. But let another layer handle that. # this is just a convenience to prevent multiple shutdowns self.shutdown_lock = Lock() # should be private self._open_lock = Lock() # should be private def is_ready(self) -> bool: """Return true if pipe open.""" return not self.shutdown_lock.locked() and self._open_lock.locked() def open(self) -> None: """ Open the channel. In a sense, __init__ creates the pipe. The OS opens it. Here we're just marking it open for use, that said. We only use locks here for indications, we never actually lock, because the broker is in charge of all async/parallel stuff. """ if not self._open_lock.acquire(blocking=False): raise RuntimeError("Cannot open same pipe twice.") def write_json(self, obj: Mapping[str, Any]) -> None: """ Send one json down the pipe. Args: obj: any python object that serializes to json. """ if not self.is_ready(): raise ChannelClosedError( "The communication channel was either never " "opened or closed. Was .open() or .close() called?", ) encoded_message = wire.serialize(obj) + b"\0" _logger.debug( f"Writing message {encoded_message[:15]!r}...{encoded_message[-15:]!r}, " f"size: {len(encoded_message)}.", ) _logger.debug2(f"Full Message: {encoded_message!r}") try: ret = os.write(self._write_to_browser, encoded_message) _logger.debug( f"***Wrote {ret}/{len(encoded_message)}***", ) if ret != len(encoded_message): _logger.critical( f"***Did not write entire message. {ret}/{len(encoded_message)}***", ) except OSError as e: self.close() raise ChannelClosedError from e def read_jsons( # noqa: PLR0912, PLR0915, C901 branches, complexity self, *, blocking: bool = True, ) -> Sequence[BrowserResponse]: """ Read from the pipe and return one or more jsons in a list. Args: blocking: The read option can be set to block or not. Returns: A list of jsons. """ jsons: list[BrowserResponse] = [] if not self.is_ready(): raise ChannelClosedError( "The communication channel was either never " "opened or closed. Was .open() or .close() called?", ) if not _with_block and not blocking: warnings.warn( # noqa: B028 "Windows python version < 3.12 does not support non-blocking", BlockWarning, ) try: if _with_block: os.set_blocking(self._read_from_browser, blocking) except OSError as e: self.close() raise ChannelClosedError from e raw_buffer = None # if we fail in read, we already defined loop_count = 1 try: raw_buffer = os.read( self._read_from_browser, 10000, ) # 10MB buffer, nbd, doesn't matter w/ this _logger.debug( f"First read in loop: {raw_buffer[:15]!r}...{raw_buffer[-15:]!r}. " f"size: {len(raw_buffer)}.", ) _logger.debug2(f"Whole buffer: {raw_buffer!r}") if not raw_buffer or raw_buffer == b"{bye}\n": if raw_buffer: _logger.debug(f"Received {raw_buffer!r}. is bye?") # we seem to need {bye} even if chrome closes NOTE self.close() raise ChannelClosedError while raw_buffer[-1] != 0: _logger.debug("Partial message from browser received.") loop_count += 1 if _with_block: os.set_blocking(self._read_from_browser, True) raw_buffer += os.read(self._read_from_browser, 10000) except BlockingIOError: _logger.debug("BlockingIOError") return jsons except OSError as e: _logger.debug("OSError") self.close() if not raw_buffer or raw_buffer == b"{bye}\n": raise ChannelClosedError from e # this could be hard to test as it is a real OS corner case finally: _logger.debug( f"Total loops: {loop_count}, " f"Final size: {len(raw_buffer) if raw_buffer else 0}.", ) _logger.debug2(f"Whole buffer: {raw_buffer!r}") if raw_buffer is None: return jsons decoded_buffer = raw_buffer.decode("utf-8") raw_messages = decoded_buffer.split("\0") _logger.debug(f"Received {len(raw_messages)} raw_messages.") for raw_message in raw_messages: if raw_message: try: jsons.append(wire.deserialize(raw_message)) except JSONError: _logger.exception("JSONError decoding message. Ignoring") except: _logger.exception("Error in trying to decode JSON off our read.") raise return jsons def _unblock_fd(self, fd: int) -> None: try: if _with_block: os.set_blocking(fd, False) except Exception: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass pass def _close_fd(self, fd: int) -> None: try: os.close(fd) except Exception: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass pass def _fake_bye(self) -> None: self._unblock_fd(self._write_from_browser) try: os.write(self._write_from_browser, b"{bye}\n") except Exception: # noqa: BLE001, S110 OS errors are not consistent, catch blind + pass pass def close(self) -> None: """Close the pipe.""" if self.shutdown_lock.acquire(blocking=False): if platform.system() == "Windows": self._fake_bye() self._unblock_fd(self._write_from_browser) self._unblock_fd(self._read_from_browser) self._unblock_fd(self._write_to_browser) self._unblock_fd(self._read_to_browser) self._close_fd(self._write_to_browser) # no more writes self._close_fd(self._write_from_browser) # we're done with writes self._close_fd(self._read_from_browser) # no more attempts at read self._close_fd(self._read_to_browser) choreographer-1.2.1/src/choreographer/cli/000077500000000000000000000000001510421612300205245ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/cli/__init__.py000066400000000000000000000003231510421612300226330ustar00rootroot00000000000000"""cli provides some tools that are used on the commandline (and to download chrome).""" from ._cli_utils import ( get_chrome, get_chrome_sync, ) __all__ = [ "get_chrome", "get_chrome_sync", ] choreographer-1.2.1/src/choreographer/cli/_cli_utils.py000066400000000000000000000173531510421612300232350ustar00rootroot00000000000000from __future__ import annotations import argparse import asyncio import json import platform import shutil import sys import urllib.request import warnings import zipfile from functools import partial from pathlib import Path import logistro from choreographer.cli.defaults import default_download_path _logger = logistro.getLogger(__name__) _chrome_for_testing_url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" supported_platform_strings = ["linux64", "win32", "win64", "mac-x64", "mac-arm64"] def get_google_supported_platform_string() -> tuple[str, str, str, str]: arch_size_detected = "64" if sys.maxsize > 2**32 else "32" arch_detected = "arm" if platform.processor() == "arm" else "x" chrome_platform_detected: str | None = None if platform.system() == "Windows": chrome_platform_detected = "win" + arch_size_detected elif platform.system() == "Linux": chrome_platform_detected = "linux" + arch_size_detected elif platform.system() == "Darwin": chrome_platform_detected = "mac-" + arch_detected + arch_size_detected platform_string = "" if chrome_platform_detected in supported_platform_strings: platform_string = chrome_platform_detected return platform_string, arch_size_detected, platform.processor(), platform.system() def get_chrome_download_path() -> Path | None: _chrome_platform_detected, _, _, _ = get_google_supported_platform_string() if not _chrome_platform_detected: return None _default_exe_path = Path() if platform.system().startswith("Linux"): _default_exe_path = ( default_download_path / f"chrome-{_chrome_platform_detected}" / "chrome" ) elif platform.system().startswith("Darwin"): _default_exe_path = ( default_download_path / f"chrome-{_chrome_platform_detected}" / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" ) elif platform.system().startswith("Win"): _default_exe_path = ( default_download_path / f"chrome-{_chrome_platform_detected}" / "chrome.exe" ) return _default_exe_path # https://stackoverflow.com/questions/39296101/python-zipfile-removes-execute-permissions-from-binaries class _ZipFilePermissions(zipfile.ZipFile): def _extract_member(self, member, targetpath, pwd): # type: ignore [no-untyped-def] if not isinstance(member, zipfile.ZipInfo): member = self.getinfo(member) path = super()._extract_member(member, targetpath, pwd) # type: ignore [misc] # High 16 bits are os specific (bottom is st_mode flag) attr = member.external_attr >> 16 if attr != 0: Path(path).chmod(attr) return path def get_chrome_sync( # noqa: PLR0912, C901 arch: str | None = None, i: int | None = None, path: str | Path = default_download_path, *, verbose: bool = False, ) -> Path | str: """Download chrome synchronously: see `get_chrome()`.""" if not arch: arch, _, _, _ = get_google_supported_platform_string() if not arch: raise RuntimeError( "You must specify an arch, one of: " f"{', '.join(supported_platform_strings)}. " f"Detected {arch} is not supported.", ) if isinstance(path, str): path = Path(path) if i: _logger.info("Loading chrome from list") browser_list = json.loads( urllib.request.urlopen( # noqa: S310 audit url for schemes _chrome_for_testing_url, ).read(), ) version_obj = browser_list["versions"][i] else: _logger.info("Using last known good version of chrome") with ( Path(__file__).resolve().parent.parent / "resources" / "last_known_good_chrome.json" ).open() as f: version_obj = json.load(f) if verbose: print(version_obj["version"]) # noqa: T201 allow print in cli print(version_obj["revision"]) # noqa: T201 allow print in cli chromium_sources = version_obj["downloads"]["chrome"] url = "" for src in chromium_sources: if src["platform"] == arch: url = src["url"] break else: raise RuntimeError( "You must specify an arch, one of: " f"{', '.join(supported_platform_strings)}. " f"{arch} is not supported.", ) if not path.exists(): path.mkdir(parents=True) filename = path / "chrome.zip" with urllib.request.urlopen(url) as response, filename.open("wb") as out_file: # noqa: S310 audit url shutil.copyfileobj(response, out_file) with _ZipFilePermissions(filename, "r") as zip_ref: zip_ref.extractall(path) filename.unlink() if arch.startswith("linux"): exe_name = path / f"chrome-{arch}" / "chrome" elif arch.startswith("mac"): exe_name = ( path / f"chrome-{arch}" / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" ) elif arch.startswith("win"): exe_name = path / f"chrome-{arch}" / "chrome.exe" else: raise RuntimeError("Couldn't calculate exe_name, unsupported architecture.") return exe_name async def get_chrome( arch: str | None = None, i: int | None = None, path: str | Path = default_download_path, *, verbose: bool = False, ) -> Path | str: """ Download google chrome from google-chrome-for-testing server. Args: arch: the target platform/os, as understood by google's json directory. i: the chrome version: -1 being the latest version, 0 being the oldest still in the testing directory. path: where to download it too (the folder). verbose: print out version found """ loop = asyncio.get_running_loop() fn = partial(get_chrome_sync, arch=arch, i=i, path=path, verbose=verbose) return await loop.run_in_executor( executor=None, func=fn, ) def get_chrome_cli() -> None: if "ubuntu" in platform.version().lower(): warnings.warn( # noqa: B028 "You are using `get_browser()` on Ubuntu." " Ubuntu is **very strict** about where binaries come from." " While sandbox is already off by default, do not set" " enable_sandbox to True OR you must install from Ubuntu's" " package manager.", UserWarning, ) parser = argparse.ArgumentParser( description="Will download Chrome for testing. All arguments optional.", parents=[logistro.parser], ) parser.add_argument( "--i", "-i", type=int, dest="i", help=( "Google offers thousands of chrome versions for download. " "'-i 0' is the oldest, '-i -1' is the newest: array syntax" ), ) parser.add_argument( "--arch", dest="arch", help="linux64|win32|win64|mac-x64|mac-arm64", ) parser.add_argument( "--path", dest="path", help="Where to store the download.", ) parser.add_argument( "-v", "--verbose", dest="verbose", action="store_true", help="Display found version number if using -i (to stdout)", ) parser.set_defaults(path=default_download_path) parser.set_defaults(arch=None) parser.set_defaults(verbose=False) parsed = parser.parse_args() i = parsed.i arch = parsed.arch path = Path(parsed.path) verbose = parsed.verbose print(get_chrome_sync(arch=arch, i=i, path=path, verbose=verbose)) # noqa: T201 allow print in cli choreographer-1.2.1/src/choreographer/cli/_cli_utils_no_qa.py000066400000000000000000000115131510421612300244020ustar00rootroot00000000000000import argparse import asyncio import platform import subprocess import sys import time from pathlib import Path import logistro # diagnose function is too weird and ruff guts it # ruff has line-level and file-level QA suppression # so lets give diagnose a separate file # ruff: noqa: PLR0915, C901, S603, BLE001, S607, PERF203, TRY002, T201, PLR0912, SLF001 # ruff: noqa: PLC0415 # ruff: noqa: F401, ERA001 # temporary, sync # ruff: noqa: PLC0415 - import at time of file # in order, exceptions are: # - function complexity (statements?) # - function complexity (algo measure) # - validate subprocess input arguments # - blind exception # - partial executable path (bash not /bin/bash) # - performance overhead of try-except in loop # - make own exceptions # - no print def diagnose() -> None: logistro.betterConfig(level=1) from choreographer import Browser, BrowserSync from choreographer.browsers.chromium import Chromium from choreographer.utils._which import browser_which parser = argparse.ArgumentParser( description="tool to help debug problems", parents=[logistro.parser], ) parser.add_argument("--no-run", dest="run", action="store_false") parser.add_argument("--show", dest="headless", action="store_false") parser.set_defaults(run=True) parser.set_defaults(headless=True) args, _ = parser.parse_known_args() run = args.run headless = args.headless fail = [] print("*".center(50, "*")) print("SYSTEM:".center(50, "*")) print(platform.system()) print(platform.release()) print(platform.version()) print(platform.uname()) print("*".center(50, "*")) print("BROWSER:".center(50, "*")) try: local_path = browser_which([], verify_local=True) if local_path and not Path(local_path).exists(): print(f"Local doesn't exist at {local_path}") else: print(f"Found local: {browser_which([], verify_local=True)}") except RuntimeError: print("Didn't find local.") browser_path = Chromium.find_browser(skip_local=True) print(browser_path) print("*".center(50, "*")) print("BROWSER_INIT_CHECK (DEPS)".center(50, "*")) if not browser_path: print("No browser, found can't check for deps.") else: b = Browser() b._browser_impl.pre_open() cli = b._browser_impl.get_cli() env = b._browser_impl.get_env() # noqa: F841 args = b._browser_impl.get_popen_args() b._browser_impl.clean() del b print("*** cli:") for arg in cli: print(" " * 8 + str(arg)) # potential security issue # print("*** env:") # for k, v in env.items(): # print(" " * 8 + f"{k}:{v}") print("*** Popen args:") for k, v in args.items(): print(" " * 8 + f"{k}:{v}") print("*".center(50, "*")) print("VERSION INFO:".center(50, "*")) try: print("pip:".center(25, "*")) print(subprocess.check_output([sys.executable, "-m", "pip", "freeze"]).decode()) except Exception as e: print(f"Error w/ pip: {e}") try: print("uv:".center(25, "*")) print(subprocess.check_output(["uv", "pip", "freeze"]).decode()) except Exception as e: print(f"Error w/ uv: {e}") try: print("git:".center(25, "*")) print( subprocess.check_output( ["git", "describe", "--tags", "--long", "--always"], ).decode(), ) except Exception as e: print(f"Error w/ git: {e}") finally: print(sys.version) print(sys.version_info) print("Done with version info.".center(50, "*")) if run: print("*".center(50, "*")) print("Actual Run Tests".center(50, "*")) async def test_headless() -> None: browser = await Browser(headless=headless) await asyncio.sleep(3) await browser.close() try: print("Async Test Headless".center(50, "*")) asyncio.run(test_headless()) except Exception as e: fail.append(("Async test headless", e)) finally: print("Done with async test headless".center(50, "*")) print() sys.stdout.flush() sys.stderr.flush() if fail: import traceback for exception in fail: try: print(f"Error in: {exception[0]}") traceback.print_exception( type(exception[1]), exception[1], exception[1].__traceback__, ) except Exception: print("Couldn't print traceback for:") print(str(exception)) raise Exception( "There was an exception during full async run, see above.", ) print("Thank you! Please share these results with us!") choreographer-1.2.1/src/choreographer/cli/defaults.py000066400000000000000000000003351510421612300227060ustar00rootroot00000000000000"""Defaults used when arguments not supplied.""" from pathlib import Path default_download_path = Path(__file__).resolve().parent / "browser_exe" """The path where we download chrome if no path argument is supplied.""" choreographer-1.2.1/src/choreographer/errors.py000066400000000000000000000014131510421612300216420ustar00rootroot00000000000000"""A compilation of the errors available in choreographer.""" from ._brokers._async import UnhandledMessageWarning from .browsers import ( BrowserClosedError, BrowserDepsError, BrowserFailedError, ChromeNotFoundError, ) from .channels import BlockWarning, ChannelClosedError from .protocol import ( DevtoolsProtocolError, ExperimentalFeatureWarning, MessageTypeError, MissingKeyError, ) from .utils import TmpDirWarning __all__ = [ "BlockWarning", "BrowserClosedError", "BrowserDepsError", "BrowserFailedError", "ChannelClosedError", "ChromeNotFoundError", "DevtoolsProtocolError", "ExperimentalFeatureWarning", "MessageTypeError", "MissingKeyError", "TmpDirWarning", "UnhandledMessageWarning", ] choreographer-1.2.1/src/choreographer/protocol/000077500000000000000000000000001510421612300216165ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/protocol/__init__.py000066400000000000000000000134601510421612300237330ustar00rootroot00000000000000""" Provides various implementations of Session and Target. It includes helpers and constants for the Chrome Devtools Protocol. """ from __future__ import annotations from enum import Enum from typing import Any, MutableMapping, NewType, Optional, Tuple, cast BrowserResponse = NewType("BrowserResponse", MutableMapping[str, Any]) """The type for a response from the browser. Is really a `dict()`.""" BrowserCommand = NewType("BrowserCommand", MutableMapping[str, Any]) """The type for a command to the browser. Is really a `dict()`.""" MessageKey = NewType("MessageKey", Tuple[str, Optional[int]]) """The type for id'ing a message/response. It is `tuple(session_id, message_id)`.""" class Ecode(Enum): """Ecodes are a list of possible error codes chrome returns.""" TARGET_NOT_FOUND = -32602 """Self explanatory.""" class DevtoolsProtocolError(Exception): """Raise a general error reported by the devtools protocol.""" def __init__(self, response: BrowserResponse) -> None: """ Construct a new DevtoolsProtocolError. Args: response: the json response that contains the error """ super().__init__(response) self.code = response["error"]["code"] self.message = response["error"]["message"] class MessageTypeError(TypeError): """An error for poorly formatted devtools protocol message.""" def __init__(self, key: str, value: Any, expected_type: type) -> None: """ Construct a message about a poorly formed protocol message. Args: key: the key that has the badly typed value value: the type of the value that is incorrect expected_type: the type that was expected """ value = type(value) if not isinstance(value, type) else value super().__init__( f"Message with key {key} must have type {expected_type}, not {value}.", ) class MissingKeyError(ValueError): """An error for poorly formatted devtools protocol message.""" def __init__(self, key: str, obj: BrowserCommand) -> None: """ Construct a MissingKeyError specifying which key was missing. Args: key: the missing key obj: the message without the key """ super().__init__( f"Message missing required key/s {key}. Message received: {obj}", ) class ExperimentalFeatureWarning(UserWarning): """An warning to report that a feature may or may not work.""" def verify_params(obj: BrowserCommand) -> None: """ Verify the message obj hast he proper keys and values. Args: obj: the object to check. Raises: MissingKeyError: if a key is missing. MessageTypeError: if a value type is incorrect. RuntimeError: if there are strange keys. """ n_keys = 0 required_keys = {"id": int, "method": str} for key, type_key in required_keys.items(): if key not in obj: raise MissingKeyError(key, obj) if not isinstance(obj[key], type_key): raise MessageTypeError(key, type(obj[key]), type_key) n_keys += 2 if "params" in obj: n_keys += 1 if "sessionId" in obj: n_keys += 1 if len(obj.keys()) != n_keys: raise RuntimeError( "Message objects must have id and method keys, " "and may have params and sessionId keys.", ) def calculate_message_key(msg: BrowserResponse | BrowserCommand) -> MessageKey | None: """ Given a message to/from the browser, calculate the key corresponding to the command. Every message is uniquely identified by its sessionId and id (counter). Args: msg: the message for which to calculate the key. """ session_id = msg.get("sessionId", "") message_id = msg.get("id") if message_id is None: return None return MessageKey((session_id, message_id)) def match_message_key(response: BrowserResponse, key: MessageKey) -> bool: """ Report True if a response matches with a certain key (sessionId, id). Args: response: the object response from the browser key: the (sessionId, id) key tubple we're looking for """ session_id, message_id = key if ("session_id" not in response and session_id == "") or ( # is browser session "session_id" in response and response["session_id"] == session_id # is session ): pass else: return False if "id" in response and str(response["id"]) == str(message_id): pass else: return False return True def is_event(response: BrowserResponse) -> bool: """Return true if the browser response is an event notification.""" required_keys = {"method", "params"} return required_keys <= response.keys() and "id" not in response def get_target_id_from_result(response: BrowserResponse) -> str | None: """ Extract target id from a browser response. Args: response: the browser response to extract the targetId from. """ if "result" in response and "targetId" in response["result"]: return cast("str", response["result"]["targetId"]) else: return None def get_session_id_from_result(response: BrowserResponse) -> str | None: """ Extract session id from a browser response. Args: response: the browser response to extract the sessionId from. """ if "result" in response and "sessionId" in response["result"]: return cast("str", response["result"]["sessionId"]) else: return None def get_error_from_result(response: BrowserResponse) -> str | None: """ Extract error from a browser response. Args: response: the browser response to extract the error from. """ if "error" in response: return cast("str", response["error"]) else: return None choreographer-1.2.1/src/choreographer/protocol/devtools_async.py000066400000000000000000000232361510421612300252320ustar00rootroot00000000000000"""Provide a lower-level async interface to the Devtools Protocol.""" from __future__ import annotations import inspect from typing import TYPE_CHECKING import logistro from choreographer import protocol if TYPE_CHECKING: import asyncio from typing import Any, Callable, Coroutine, MutableMapping from choreographer._brokers import Broker _logger = logistro.getLogger(__name__) class Session: """A session is a single conversation with a single target.""" session_id: str """The id of the session given by the browser.""" message_id: int """All messages are counted per session and this is the current message id.""" subscriptions: MutableMapping[ str, tuple[ Callable[[protocol.BrowserResponse], Coroutine[Any, Any, Any]], bool, ], ] def __init__(self, session_id: str, broker: Broker) -> None: """ Construct a session from the browser as an object. A session is like an open conversation with a target. All commands are sent on sessions. Args: broker: a reference to the browser's broker session_id: the id given by the browser """ if not isinstance(session_id, str): raise TypeError("session_id must be a string") # Resources self._broker = broker # State self.session_id = session_id _logger.debug(f"New session: {session_id}") self.message_id = 0 self.subscriptions = {} async def send_command( self, command: str, params: MutableMapping[str, Any] | None = None, ) -> protocol.BrowserResponse: """ Send a devtools command on the session. https://chromedevtools.github.io/devtools-protocol/ Args: command: devtools command to send params: the parameters to send Returns: A message key (session, message id) tuple or None """ current_id = self.message_id self.message_id += 1 json_command = protocol.BrowserCommand( { "id": current_id, "method": command, }, ) if self.session_id: json_command["sessionId"] = self.session_id if params: json_command["params"] = params _logger.debug( f"Cmd '{command}', param keys '{params.keys() if params else ''}', " f"sessionId '{self.session_id}'", ) _logger.debug2(f"Full params: {str(params).replace('%', '%%')}") return await self._broker.write_json(json_command) def subscribe( self, string: str, callback: Callable[[protocol.BrowserResponse], Coroutine[Any, Any, Any]], *, repeating: bool = True, ) -> None: """ Subscribe to an event on this session. Args: string: the name of the event. Can use * wildcard at the end. callback: the callback (which takes a message dict and returns nothing) repeating: default True, should the callback execute more than once """ if not inspect.iscoroutinefunction(callback): raise TypeError( "Call back must be be `async def` type function.", ) if string in self.subscriptions: raise ValueError( "You are already subscribed to this string, " "duplicate subscriptions are not allowed.", ) else: # so this should be per session # and that means we need a list of all sessions self.subscriptions[string] = (callback, repeating) def unsubscribe(self, string: str) -> None: """ Remove a subscription. Args: string: the subscription to remove. """ if string not in self.subscriptions: return del self.subscriptions[string] def subscribe_once(self, string: str) -> asyncio.Future[Any]: """ Return a future for a browser event. Generally python asyncio doesn't recommend futures. But in this case, one must call subscribe_once and await it later, generally because they must subscribe and then provoke the event. Args: string: the event to subscribe to Returns: A future to be awaited later, the complete event. """ return self._broker.new_subscription_future(self.session_id, string) class Target: """A target like a browser, tab, or others. It sends commands. It has sessions.""" target_id: str """The browser's ID of the target.""" sessions: MutableMapping[str, Session] """A list of all the sessions for this target.""" def __init__(self, target_id: str, broker: Broker): """ Create a target after one ahs been created by the browser. Args: broker: a reference to the browser's broker target_id: the id given by the browser """ if not isinstance(target_id, str): raise TypeError("target_id must be string") # Resources self._broker = broker # States self.sessions = {} self.target_id = target_id _logger.debug(f"Created new target {target_id}.") def _add_session(self, session: Session) -> None: if not isinstance(session, Session): raise TypeError("session must be a session type class") self.sessions[session.session_id] = session def _remove_session(self, session_id: str) -> None: if isinstance(session_id, Session): session_id = session_id.session_id _ = self.sessions.pop(session_id, None) def get_session(self) -> Session: """Retrieve the first session of the target, if it exists.""" if not self.sessions.values(): raise RuntimeError( "Cannot use this method without at least one valid session", ) session = next(iter(self.sessions.values())) return session async def send_command( self, command: str, params: MutableMapping[str, Any] | None = None, ) -> protocol.BrowserResponse: """ Send a command to the first session in a target. https://chromedevtools.github.io/devtools-protocol/ Args: command: devtools command to send params: the parameters to send """ if not self.sessions.values(): raise RuntimeError("Cannot send_command without at least one valid session") session = self.get_session() return await session.send_command(command, params) async def create_session(self) -> Session: """Create a new session on this target.""" response = await self._broker._browser.send_command( # noqa: SLF001 yeah we need the browser :-( "Target.attachToTarget", params={"targetId": self.target_id, "flatten": True}, ) if "error" in response: raise RuntimeError( "Could not create session", ) from protocol.DevtoolsProtocolError( response, ) session_id = response["result"]["sessionId"] new_session = Session(session_id, self._broker) self._add_session(new_session) return new_session # async only async def close_session( self, session_id: str, ) -> protocol.BrowserResponse: """ Close a session by session_id. Args: session_id: the session to close """ if isinstance(session_id, Session): session_id = session_id.session_id response = await self._broker._browser.send_command( # noqa: SLF001 we need browser command="Target.detachFromTarget", params={"sessionId": session_id}, ) self._remove_session(session_id) if "error" in response: raise RuntimeError( "Could not close session", ) from protocol.DevtoolsProtocolError( response, ) _logger.debug(f"The session {session_id} has been closed.") return response # kinda hate, why do we need this again? def subscribe( self, string: str, callback: Callable[[protocol.BrowserResponse], Coroutine[Any, Any, Any]], *, repeating: bool = True, ) -> None: """ Subscribe to an event on the main session of this target. Args: string: the name of the event. Can use * wildcard at the end. callback: the callback (which takes a message dict and returns nothing) repeating: default True, should the callback execute more than once """ session = self.get_session() session.subscribe(string, callback, repeating=repeating) def unsubscribe(self, string: str) -> None: """ Remove a subscription. Args: string: the subscription to remove. """ session = self.get_session() session.unsubscribe(string) def subscribe_once(self, string: str) -> asyncio.Future[Any]: """ Return a future for a browser event for the first session of this target. Generally python asyncio doesn't recommend futures. But in this case, one must call subscribe_once and await it later, generally because they must subscribe and then provoke the event. Args: string: the event to subscribe to Returns: A future to be awaited later, the complete event. """ session = self.get_session() return session.subscribe_once(string) choreographer-1.2.1/src/choreographer/protocol/devtools_sync.py000066400000000000000000000105701510421612300250660ustar00rootroot00000000000000"""Provide a lower-level sync interface to the Devtools Protocol.""" from __future__ import annotations from typing import TYPE_CHECKING import logistro from choreographer import protocol if TYPE_CHECKING: from typing import Any, MutableMapping from choreographer._brokers import BrokerSync _logger = logistro.getLogger(__name__) class SessionSync: """A session is a single conversation with a single target.""" session_id: str """The id of the session given by the browser.""" message_id: int """All messages are counted per session and this is the current message id.""" def __init__(self, session_id: str, broker: BrokerSync) -> None: """ Construct a session from the browser as an object. A session is like an open conversation with a target. All commands are sent on sessions. Args: broker: a reference to the browser's broker session_id: the id given by the browser """ if not isinstance(session_id, str): raise TypeError("session_id must be a string") # Resources self._broker = broker # State self.session_id = session_id _logger.debug(f"New session: {session_id}.") self.message_id = 0 def send_command( self, command: str, params: MutableMapping[str, Any] | None = None, ) -> Any: """ Send a devtools command on the session. https://chromedevtools.github.io/devtools-protocol/ Args: command: devtools command to send params: the parameters to send Returns: A message key (session, message id) tuple or None """ current_id = self.message_id self.message_id += 1 json_command = protocol.BrowserCommand( { "id": current_id, "method": command, }, ) if self.session_id: json_command["sessionId"] = self.session_id if params: json_command["params"] = params _logger.debug( f"Sending {command} with {params} on session {self.session_id}", ) return self._broker.write_json(json_command) class TargetSync: """A target like a browser, tab, or others. It sends commands. It has sessions.""" target_id: str """The browser's ID of the target.""" sessions: MutableMapping[str, SessionSync] """A list of all the sessions for this target.""" def __init__(self, target_id: str, broker: BrokerSync): """ Create a target after one ahs been created by the browser. Args: broker: a reference to the browser's broker target_id: the id given by the browser """ if not isinstance(target_id, str): raise TypeError("target_id must be string") # Resources self._broker = broker # States self.sessions = {} self.target_id = target_id _logger.debug(f"Created new target {target_id}.") def _add_session(self, session: SessionSync) -> None: if not isinstance(session, SessionSync): raise TypeError("session must be a session type class") self.sessions[session.session_id] = session def _remove_session(self, session_id: str) -> None: if isinstance(session_id, SessionSync): session_id = session_id.session_id _ = self.sessions.pop(session_id, None) def get_session(self) -> SessionSync: """Retrieve the first session of the target, if it exists.""" if not self.sessions.values(): raise RuntimeError( "Cannot use this method without at least one valid session", ) session = next(iter(self.sessions.values())) return session def send_command( self, command: str, params: MutableMapping[str, Any] | None = None, ) -> Any: """ Send a command to the first session in a target. https://chromedevtools.github.io/devtools-protocol/ Args: command: devtools command to send params: the parameters to send """ if not self.sessions.values(): raise RuntimeError("Cannot send_command without at least one valid session") session = self.get_session() return session.send_command(command, params) choreographer-1.2.1/src/choreographer/py.typed000066400000000000000000000000001510421612300214420ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/pyrightconfig.json000066400000000000000000000000431510421612300235210ustar00rootroot00000000000000{ "typeCheckingMode": "strict" } choreographer-1.2.1/src/choreographer/resources/000077500000000000000000000000001510421612300217675ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/resources/__init__.py000066400000000000000000000000321510421612300240730ustar00rootroot00000000000000"""A data folder only.""" choreographer-1.2.1/src/choreographer/resources/last_known_good_chrome.json000066400000000000000000000060761510421612300274170ustar00rootroot00000000000000{ "version": "135.0.7011.0", "revision": "1418433", "downloads": { "chrome": [ { "platform": "linux64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/linux64/chrome-linux64.zip" }, { "platform": "mac-arm64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-arm64/chrome-mac-arm64.zip" }, { "platform": "mac-x64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-x64/chrome-mac-x64.zip" }, { "platform": "win32", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win32/chrome-win32.zip" }, { "platform": "win64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win64/chrome-win64.zip" } ], "chromedriver": [ { "platform": "linux64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/linux64/chromedriver-linux64.zip" }, { "platform": "mac-arm64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-arm64/chromedriver-mac-arm64.zip" }, { "platform": "mac-x64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-x64/chromedriver-mac-x64.zip" }, { "platform": "win32", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win32/chromedriver-win32.zip" }, { "platform": "win64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win64/chromedriver-win64.zip" } ], "chrome-headless-shell": [ { "platform": "linux64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/linux64/chrome-headless-shell-linux64.zip" }, { "platform": "mac-arm64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-arm64/chrome-headless-shell-mac-arm64.zip" }, { "platform": "mac-x64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/mac-x64/chrome-headless-shell-mac-x64.zip" }, { "platform": "win32", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win32/chrome-headless-shell-win32.zip" }, { "platform": "win64", "url": "https://storage.googleapis.com/chrome-for-testing-public/135.0.7011.0/win64/chrome-headless-shell-win64.zip" } ] } } choreographer-1.2.1/src/choreographer/utils/000077500000000000000000000000001510421612300211155ustar00rootroot00000000000000choreographer-1.2.1/src/choreographer/utils/README.txt000066400000000000000000000003101510421612300226050ustar00rootroot00000000000000sys_utils provide: a `which_browser()` function for finding the browser a more robust `TmpDirectory` class for creating and managing those a `kill()` function to be used when destroying processes. choreographer-1.2.1/src/choreographer/utils/__init__.py000066400000000000000000000003571510421612300232330ustar00rootroot00000000000000"""Contains functions and class that primarily help us with the OS.""" from ._tmpfile import TmpDirectory, TmpDirWarning from ._which import get_browser_path __all__ = [ "TmpDirWarning", "TmpDirectory", "get_browser_path", ] choreographer-1.2.1/src/choreographer/utils/_kill.py000066400000000000000000000014311510421612300225600ustar00rootroot00000000000000from __future__ import annotations import platform import subprocess import logistro _logger = logistro.getLogger(__name__) def kill(process: subprocess.Popen[bytes] | subprocess.Popen[str]) -> None: if platform.system() == "Windows": subprocess.call( # noqa: S603, false positive, input fine ["taskkill", "/F", "/T", "/PID", str(process.pid)], # noqa: S607 windows full path... stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, timeout=6, ) else: process.terminate() _logger.debug("Called terminate (a light kill).") try: process.wait(timeout=6) except subprocess.TimeoutExpired: _logger.debug("Calling kill (a heavy kill).") process.kill() choreographer-1.2.1/src/choreographer/utils/_manual_thread_pool.py000066400000000000000000000040101510421612300254560ustar00rootroot00000000000000import queue import threading from concurrent.futures import Executor, Future import logistro _logger = logistro.getLogger(__name__) class ExecutorClosedError(RuntimeError): """Raise if submitting when executor is closed.""" class ManualThreadExecutor(Executor): def __init__(self, *, max_workers=2, daemon=True, name="manual-exec"): self._q = queue.Queue() self._stop = False self._threads = [] self.name = name for i in range(max_workers): t = threading.Thread( target=self._worker, name=f"{name}-{i}", daemon=daemon, ) t.start() self._threads.append(t) def _worker(self): while True: item = self._q.get() if item is None: # sentinel return fn, args, kwargs, fut = item if fut.set_running_or_notify_cancel(): try: res = fn(*args, **kwargs) except BaseException as e: # noqa: BLE001 yes we catch and set fut.set_exception(e) else: fut.set_result(res) self._q.task_done() def submit(self, fn, *args, **kwargs): fut = Future() if self._stop: fut.set_exception(ExecutorClosedError("Cannot submit tasks.")) return fut self._q.put((fn, args, kwargs, fut)) return fut def shutdown(self, wait=True, *, cancel_futures=False): # noqa: FBT002 overriding, can't change args self._stop = True if cancel_futures: # Drain queue and cancel pending try: while True: _, _, _, fut = self._q.get_nowait() fut.cancel() self._q.task_done() except queue.Empty: pass for _ in self._threads: self._q.put(None) if wait: for t in self._threads: t.join(timeout=2.0) choreographer-1.2.1/src/choreographer/utils/_tmpfile.py000066400000000000000000000162261510421612300232750ustar00rootroot00000000000000from __future__ import annotations import os import platform import shutil import stat import sys import tempfile import time import warnings from pathlib import Path from typing import TYPE_CHECKING import logistro if TYPE_CHECKING: from typing import Any, Callable, MutableMapping, Sequence _logger = logistro.getLogger(__name__) class TmpDirWarning(UserWarning): """A warning if for whatever reason we can't eliminate the tmp dir.""" class TmpDirectory: """ The python stdlib `TemporaryDirectory` wrapper for easier use. Python's `TemporaryDirectory` suffered a couple API changes that mean you can't call it the same way for similar versions. This wrapper is also much more aggressive about deleting the directory when it's done, not necessarily relying on OS functions. """ temp_dir: tempfile.TemporaryDirectory[str] """A reference to the underlying python `TemporaryDirectory` implementation.""" path: Path """The path to the temporary directory.""" exists: bool """A flag to indicate if the directory still exists.""" def __init__(self, path: str | None = None, *, sneak: bool = False): """ Construct a wrapped `TemporaryDirectory`. Args: path: manually specify the directory to use sneak: (default False) avoid using /tmp Ubuntu's snap will sandbox /tmp """ self._with_onexc = bool(sys.version_info[:3] >= (3, 12)) args: MutableMapping[str, Any] = {} if path: args = {"dir": path} elif sneak: args = {"prefix": ".choreographer-", "dir": Path.home()} if platform.system() != "Windows": self.temp_dir = tempfile.TemporaryDirectory(**args) else: # is windows vinfo = sys.version_info[:3] if vinfo >= (3, 12): self.temp_dir = tempfile.TemporaryDirectory( # type: ignore [call-overload, unused-ignore] delete=False, ignore_cleanup_errors=True, **args, ) elif vinfo >= (3, 10): self.temp_dir = tempfile.TemporaryDirectory( # type: ignore [call-overload, unused-ignore] ignore_cleanup_errors=True, **args, ) else: self.temp_dir = tempfile.TemporaryDirectory(**args) self.path = Path(self.temp_dir.name) _logger.info(f"Temp directory created: {self.path}.") self.exists = True def _delete_manually( # noqa: C901, PLR0912 self, *, check_only: bool = False, quiet: bool = False, ) -> tuple[ int, int, Sequence[tuple[Path, BaseException]], ]: if not self.path.exists(): self.exists = False return 0, 0, [] n_dirs = 0 n_files = 0 errors = [] for root, dirs, files in os.walk(self.path, topdown=False): n_dirs += len(dirs) n_files += len(files) if not check_only: for f in files: fp = Path(root) / f _logger.debug2(f"Have file {fp}") try: fp.chmod(stat.S_IWUSR) fp.unlink(missing_ok=True) _logger.debug2("Deleted") except Exception as e: # noqa: BLE001 yes catch and report errors.append((fp, e)) for d in dirs: fp = Path(root) / d _logger.debug2(f"Have directory {fp}") try: fp.chmod(stat.S_IWUSR) fp.rmdir() _logger.debug2("Deleted") except Exception as e: # noqa: BLE001 yes catch and report errors.append((fp, e)) # clean up directory if not check_only: try: self.path.chmod(stat.S_IWUSR) self.path.rmdir() except Exception as e: # noqa: BLE001 yes catch and report errors.append((self.path, e)) if check_only: if n_dirs or n_files: self.exists = True else: self.exists = False elif errors: if not quiet: warnings.warn( # noqa: B028 "The temporary directory could not be deleted, " f"execution will continue. errors: {errors}", TmpDirWarning, ) self.exists = True else: self.exists = False return n_dirs, n_files, errors def clean(self) -> None: # noqa: C901 """Try several different ways to eliminate the temporary directory.""" try: # no faith in this python implementation, always fails with windows # very unstable recently as well, lots new arguments in tempfile package if hasattr(self, "temp_dir") and self.temp_dir: self.temp_dir.cleanup() self.exists = False _logger.info("TemporaryDirectory.cleanup() worked.") except Exception as e: # noqa: BLE001 we try many ways to clean, this is the first one _logger.info(f"TemporaryDirectory.cleanup() failed. Error {e}") # bad typing but tough def remove_readonly( func: Callable[[str], None], path: str | Path, _excinfo: Any, ) -> None: try: Path(path).chmod(stat.S_IWUSR) func(str(path)) except FileNotFoundError: pass try: if self._with_onexc: shutil.rmtree(self.path, onexc=remove_readonly) # type: ignore [call-arg, unused-ignore] else: shutil.rmtree(self.path, onerror=remove_readonly) self.exists = False if hasattr(self, "temp_dir"): del self.temp_dir _logger.info("shutil.rmtree worked.") except FileNotFoundError: self.exists = False if hasattr(self, "temp_dir"): del self.temp_dir _logger.info("shutil.rmtree worked.") except Exception as e: # noqa: BLE001 _logger.debug("Error during tmp file removal.", exc_info=e) self._delete_manually(check_only=True) if not self.exists: return _logger.info(f"shutil.rmtree() failed to delete temporary file. Error {e}") def extra_clean() -> None: i = 0 tries = 4 while self.path.exists() and i < tries: _logger.info(f"Extra manual clean executing {i}.") self._delete_manually(quiet=True) i += 1 time.sleep(2) if self.path.exists(): self._delete_manually(quiet=False) # testing doesn't look threads so I guess we'll block extra_clean() if self.path.exists(): _logger.warning("Temporary dictory couldn't be removed manually.") choreographer-1.2.1/src/choreographer/utils/_which.py000066400000000000000000000067461510421612300227450ustar00rootroot00000000000000from __future__ import annotations import os import platform import re import shutil from typing import TYPE_CHECKING import logistro from choreographer.cli._cli_utils import get_chrome_download_path _logger = logistro.getLogger() if TYPE_CHECKING: from pathlib import Path from typing import Any, Sequence def _is_exe(path: str | Path) -> bool: try: return os.access(path, os.X_OK) except: # noqa: E722 bare except ok, weird errors, best effort. return False def _which_from_windows_reg(key: str) -> str | None: try: import winreg # noqa: PLC0415 don't import if not windows pls command = winreg.QueryValueEx( # type: ignore [attr-defined] winreg.OpenKey( # type: ignore [attr-defined] winreg.HKEY_CLASSES_ROOT, # type: ignore [attr-defined] f"{key}\\shell\\open\\command", 0, winreg.KEY_READ, # type: ignore [attr-defined] ), "", )[0] exe = re.search('"(.*?)"', command).group(1) # type: ignore [union-attr] except Exception: # noqa: BLE001 don't care why, best effort search return None return exe def browser_which( executable_names: Sequence[str], *, skip_local: bool = False, ms_prog_id: str | None = None, verify_local: bool = False, ) -> str | None: """ Look for and return first name found in PATH. Args: executable_names: the list of names to look for skip_local: (default False) don't look for a choreo download of anything. ms_prog_id: A windows registry ID string to lookup program paths verify_local: (default False) force using local install. """ _logger.debug(f"Looking for browser, skipping local? {skip_local}") path = None if isinstance(executable_names, str): executable_names = [executable_names] if skip_local: _logger.debug("Skipping searching for local download of chrome.") if verify_local: raise ValueError("Cannot set both skip_local and verify_local.") else: local_chrome = get_chrome_download_path() _logger.debug(f"Looking for at local chrome download path: {local_chrome}") if local_chrome is not None and local_chrome.exists(): _logger.debug("Returning local chrome.") return str(local_chrome) else: _logger.debug(f"Local chrome not found at path: {local_chrome}.") if verify_local: raise RuntimeError("verify_local set to True, local not found.") if platform.system() == "Windows": os.environ["NoDefaultCurrentDirectoryInExePath"] = "0" # noqa: SIM112 var name set by windows if ( ms_prog_id and (path := _which_from_windows_reg(ms_prog_id)) and _is_exe(path) ): return path for exe in executable_names: path = shutil.which(exe) if path and _is_exe(path): return path return None def get_browser_path(*args: Any, **kwargs: Any) -> str | None: # noqa: D417, don't pass args explicitly """ Call `browser_which()` but check for user override first. It looks for the browser in path. Accepts the same arguments as `browser_which': Args: executable_names: the list of names to look for skip_local: (default False) don't look for a choreo download of anything. """ return os.environ.get("BROWSER_PATH", browser_which(*args, **kwargs)) choreographer-1.2.1/tests/000077500000000000000000000000001510421612300155005ustar00rootroot00000000000000choreographer-1.2.1/tests/conftest.py000066400000000000000000000073631510421612300177100ustar00rootroot00000000000000import asyncio import logging import choreographer as choreo import logistro import pytest import pytest_asyncio from choreographer import errors _logger = logistro.getLogger(__name__) @pytest.fixture(params=[True, False], ids=["enable_sandbox", ""]) def sandbox(request): return request.param @pytest.fixture(params=[True, False], ids=["enable_gpu", ""]) def gpu(request): return request.param @pytest.fixture(params=[True, False], ids=["headless", ""]) def headless(request): return request.param # --headless is the default flag for most tests, # but you can set --no-headless if you want to watch def pytest_addoption(parser): parser.addoption("--headless", action="store_true", dest="headless", default=True) parser.addoption("--no-headless", dest="headless", action="store_false") # browser fixture will supply a browser for you @pytest_asyncio.fixture(scope="function", loop_scope="function") async def browser(request): _logger.info("Fixture building browser.") headless = request.config.getoption("--headless") browser = await choreo.Browser( headless=headless, ) yield browser _logger.info("Fixture closing browser.") try: await browser.close() except errors.BrowserClosedError: pass if ( hasattr(browser._browser_impl, "tmp_dir") # noqa: SLF001 and browser._browser_impl.tmp_dir.exists # type: ignore[reportAttributeAccessIssue] # noqa: SLF001 ): raise RuntimeError( "Temporary directory not deleted successfully: " f"{browser._browser_impl.tmp_dir.path}", # type: ignore[reportAttributeAccessIssue] # noqa: SLF001 ) # add a timeout if tests requests browser # but if tests creates their own browser they are responsible # a fixture can be used to specify the timeout: timeout=10 # else it uses pytest.default_timeout @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_setup(item): # not even sure if this working # typer doesn't like item: pytest.Item w/ funcargs # but this is the recommended way # some people say do trylast yield if "browser" in item.funcargs: _logger.info("Hook setting test timeout.") raw_test_fn = item.obj timeouts = [k for k in item.funcargs if k.startswith("timeout")] timeout = ( item.funcargs[timeouts[-1]] if timeouts else pytest.default_timeout # type: ignore[reportAttributeAccessIssue] ) if ( item.get_closest_marker("asyncio") and timeout ): # "closest" because markers can be function/session/package etc async def wrapped_test_fn(*args, **kwargs): try: return await asyncio.wait_for( raw_test_fn(*args, **kwargs), timeout=timeout, ) except TimeoutError: pytest.fail( f"Test {item.name} failed a timeout. " "This can be extended, but shouldn't be. See conftest.py.", ) item.obj = wrapped_test_fn def pytest_configure(): # change this by command line TODO pytest.default_timeout = 20 # type: ignore[reportAttributeAccessIssue] # pytest shuts down its capture before logging/threads finish @pytest.fixture(scope="session", autouse=True) def cleanup_logging_handlers(request): capture = request.config.getoption("--capture") != "no" try: yield finally: if capture: _logger.info("Conftest cleaning up handlers.") for handler in logging.root.handlers[:]: handler.flush() if isinstance(handler, logging.StreamHandler): logging.root.removeHandler(handler) choreographer-1.2.1/tests/pyrightconfig.json000066400000000000000000000000451510421612300212460ustar00rootroot00000000000000{ "typeCheckingMode": "standard" } choreographer-1.2.1/tests/test_browser.py000066400000000000000000000102031510421612300205700ustar00rootroot00000000000000import choreographer as choreo import logistro import pytest from choreographer import errors from choreographer.protocol import devtools_async # We no longer use live URLs to as not depend on the network # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _logger = logistro.getLogger(__name__) @pytest.mark.asyncio async def test_create_and_close_tab(browser): _logger.info("testing...") tab = await browser.create_tab("") assert isinstance(tab, choreo.Tab) assert tab.target_id in browser.tabs await browser.close_tab(tab) assert tab.target_id not in browser.tabs @pytest.mark.asyncio async def test_create_and_close_session(browser): _logger.info("testing...") with pytest.warns(errors.ExperimentalFeatureWarning): session = await browser.create_session() assert isinstance(session, devtools_async.Session) assert session.session_id in browser.sessions await browser.close_session(session) assert session.session_id not in browser.sessions # Along with testing, this could be repurposed as well to diagnose # This deserves some thought re. difference between write_json and send_command @pytest.mark.asyncio async def test_broker_write_json(browser): _logger.info("testing...") # Test valid request with correct id and method response = await browser._broker.write_json( # noqa: SLF001 {"id": 0, "method": "Target.getTargets"}, ) assert "result" in response and "targetInfos" in response["result"] # noqa: PT018 I like this assertion # Test invalid method name should return error response = await browser._broker.write_json( # noqa: SLF001 {"id": 2, "method": "dkadklqwmd"}, ) assert "error" in response # Test missing 'id' key with pytest.raises( errors.MissingKeyError, ): await browser._broker.write_json( # noqa: SLF001 {"method": "Target.getTargets"}, ) # Test missing 'method' key with pytest.raises( errors.MissingKeyError, ): await browser._broker.write_json( # noqa: SLF001 {"id": 1}, ) # Test empty dictionary with pytest.raises( errors.MissingKeyError, ): await browser._broker.write_json({}) # noqa: SLF001 # Test invalid parameter in the message with pytest.raises( RuntimeError, ): await browser._broker.write_json( # noqa: SLF001 {"id": 0, "method": "Target.getTargets", "invalid_parameter": "kamksamdk"}, ) # Test int method should return error with pytest.raises( errors.MessageTypeError, ): await browser._broker.write_json( # noqa: SLF001 {"id": 3, "method": 12345}, ) # Test non-integer id should return error with pytest.raises( errors.MessageTypeError, ): await browser._broker.write_json( # noqa: SLF001 {"id": "string", "method": "Target.getTargets"}, ) @pytest.mark.asyncio async def test_browser_send_command(browser): _logger.info("testing...") # Test valid request with correct command response = await browser.send_command(command="Target.getTargets") assert "result" in response and "targetInfos" in response["result"] # noqa: PT018 I like this assertion # Test invalid method name should return error response = await browser.send_command(command="dkadklqwmd") assert "error" in response # Test int method should return error with pytest.raises( errors.MessageTypeError, ): await browser.send_command(command=12345) @pytest.mark.asyncio async def test_populate_targets(browser): _logger.info("testing...") await browser.send_command(command="Target.createTarget", params={"url": ""}) await browser.populate_targets() assert len(browser.tabs) >= 1 @pytest.mark.asyncio async def test_get_tab(browser): _logger.info("testing...") await browser.create_tab("") assert browser.get_tab() == next(iter(browser.tabs.values())) await browser.create_tab() await browser.create_tab("") assert browser.get_tab() == next(iter(browser.tabs.values())) choreographer-1.2.1/tests/test_browser_search.py000066400000000000000000000025761510421612300221330ustar00rootroot00000000000000import os import platform import stat import sys from pathlib import Path import pytest from choreographer.browsers import chromium from choreographer.cli._cli_utils import get_chrome_download_path def test_internal(tmp_path): if platform.system() == "Windows": pytest.skip("Windows hard to trick about PATH.") _o = str(os.environ["PATH"]) os.environ["PATH"] = str(tmp_path) print(os.environ["PATH"]) names = ["chrome", "chromium", "msedge", "brave", "vivaldi"] paths = [] for n in names: p = tmp_path / (n) p.touch() p.chmod(p.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) paths.append(p) for _p, _n in zip(paths, names): _r = chromium.Chromium.find_browser(skip_local=True, skip_typical=True) assert _r assert Path(_r).stem == _n _p.unlink() os.environ["PATH"] = _o def test_canary(): # This ensures we are finding the local install if os.getenv("TEST_SYSTEM_BROWSER"): pytest.skip("Okay, no need to test for local.") _r = chromium.Chromium.find_browser(skip_local=False, skip_typical=True) print(sys.path, file=sys.stderr) print(f"found {_r}", file=sys.stderr) print(f"download path: {get_chrome_download_path()}", file=sys.stderr) assert _r assert Path(_r) == get_chrome_download_path() # if _r != get_chrome_download_path(): choreographer-1.2.1/tests/test_placeholder.py000066400000000000000000000005431510421612300213750ustar00rootroot00000000000000import asyncio import logistro import pytest # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _logger = logistro.getLogger(__name__) async def test_placeholder(browser): _logger.info("testing...") assert "result" in await browser.send_command("Target.getTargets") await asyncio.sleep(0) choreographer-1.2.1/tests/test_process.py000066400000000000000000000077461510421612300206050ustar00rootroot00000000000000import asyncio import os import platform import signal import subprocess import choreographer as choreo import logistro import pytest from async_timeout import timeout from choreographer import errors # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _logger = logistro.getLogger(__name__) @pytest.mark.asyncio(loop_scope="function") async def test_context(headless, sandbox, gpu): _logger.info("testing...") if sandbox and "ubuntu" in platform.version().lower(): pytest.skip( "Ubuntu doesn't support sandbox unless installed from snap.", ) elif sandbox: _logger.info( "Not skipping sandbox: " f"Sandbox: {sandbox}," f"Version: {platform.version().lower()}", ) async with timeout(pytest.default_timeout): # type: ignore[reportAttributeAccessIssue] async with choreo.Browser( headless=headless, enable_sandbox=sandbox, enable_gpu=gpu, ) as browser: response = await browser.send_command(command="Target.getTargets") assert "result" in response and "targetInfos" in response["result"] # noqa: PT018 combined assert if len(response["result"]["targetInfos"]) == 0: await browser.create_tab() assert isinstance(browser.get_tab(), choreo.Tab) tab = browser.get_tab() assert tab is not None assert len(tab.sessions) == 1 # let asyncio do some cleaning up if it wants, may prevent warnings await asyncio.sleep(0) if hasattr(browser._browser_impl, "tmp_dir"): # noqa: SLF001 assert not browser._browser_impl.tmp_dir.exists # type: ignore[reportAttributeAccessIssue] # noqa: SLF001 @pytest.mark.asyncio(loop_scope="function") async def test_no_context(headless, sandbox, gpu): _logger.info("testing...") if sandbox and "ubuntu" in platform.version().lower(): pytest.skip("Ubuntu doesn't support sandbox unless installed from snap.") browser = await choreo.Browser( headless=headless, enable_sandbox=sandbox, enable_gpu=gpu, ) try: async with timeout(pytest.default_timeout): # type: ignore[reportAttributeAccessIssue] response = await browser.send_command(command="Target.getTargets") assert "result" in response and "targetInfos" in response["result"] # noqa: PT018 combined assert if len(response["result"]["targetInfos"]) == 0: await browser.create_tab() assert isinstance(browser.get_tab(), choreo.Tab) tab = browser.get_tab() assert tab is not None assert len(tab.sessions) == 1 finally: await browser.close() await asyncio.sleep(0) if hasattr(browser._browser_impl, "tmp_dir"): # noqa: SLF001 assert not browser._browser_impl.tmp_dir.exists # type: ignore[reportAttributeAccessIssue] # noqa: SLF001 # Harass choreographer with a kill in this test to see if its clean in a way # tempdir may survive protected by chromium subprocess surviving the kill @pytest.mark.asyncio(loop_scope="function") async def test_watchdog(headless): _logger.info("testing...") browser = await choreo.Browser( headless=headless, ) if platform.system() == "Windows": # Blocking process here because it ensures the kill will occur rn subprocess.call( # noqa: S603, ASYNC221 sanitize input, blocking process ["taskkill", "/F", "/T", "/PID", str(browser.subprocess.pid)], # noqa: S607 stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) else: os.kill(browser.subprocess.pid, signal.SIGKILL) await asyncio.sleep(0.5) with pytest.raises( (errors.ChannelClosedError, errors.BrowserClosedError, asyncio.CancelledError), ): await browser.send_command(command="Target.getTargets") await browser.close() await asyncio.sleep(0) choreographer-1.2.1/tests/test_serializer.py000066400000000000000000000037751510421612300212760ustar00rootroot00000000000000from __future__ import annotations try: from datetime import UTC, datetime # type: ignore [attr-defined] except ImportError: from datetime import datetime, timezone UTC = timezone.utc import json from typing import TYPE_CHECKING import choreographer.channels._wire as wire import logistro import numpy as np import pytest from choreographer.channels import register_custom_encoder if TYPE_CHECKING: from typing import Any # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _timestamp = datetime(1970, 1, 1, tzinfo=UTC) data = [1, 2.00, 3, float("nan"), float("inf"), float("-inf"), _timestamp] expected_message = b'[1, 2.0, 3, null, null, null, "1970-01-01T00:00:00+00:00"]' converted_type = [int, float, int, type(None), type(None), type(None), str] _logger = logistro.getLogger(__name__) async def test_custom_encoder(): class NonsenseEncoder(json.JSONEncoder): def iterencode( self, o: Any, _one_shot: bool = False, # noqa: FBT001, FBT002 ) -> Any: _ = o yield "Test Passed." def encode(self, o: Any) -> Any: _ = o return "Test Passed." register_custom_encoder(NonsenseEncoder) message = wire.serialize(data) assert message == b"Test Passed." register_custom_encoder(None) message = wire.serialize(data) assert message == expected_message @pytest.mark.asyncio async def test_de_serialize(): _logger.info("testing...") message = wire.serialize(data) assert message == expected_message obj = wire.deserialize(message.decode()) assert len(obj) == len(converted_type) for o, t in zip(obj, converted_type): assert isinstance(o, t) message_np = wire.serialize(np.array(data)) assert message_np == expected_message obj_np = wire.deserialize(message_np.decode()) assert len(obj_np) == len(converted_type) for o, t in zip(obj_np, converted_type): assert isinstance(o, t) choreographer-1.2.1/tests/test_session.py000066400000000000000000000023321510421612300205740ustar00rootroot00000000000000import warnings import logistro import pytest import pytest_asyncio from choreographer import errors # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _logger = logistro.getLogger(__name__) @pytest_asyncio.fixture(scope="function", loop_scope="function") async def session(browser): _logger.info("testing...") with warnings.catch_warnings(): warnings.simplefilter("ignore", errors.ExperimentalFeatureWarning) session_browser = await browser.create_session() yield session_browser await browser.close_session(session_browser) @pytest.mark.asyncio async def test_session_send_command(session): _logger.info("testing...") # Test valid request with correct command response = await session.send_command(command="Target.getTargets") assert "result" in response and "targetInfos" in response["result"] # noqa: PT018 I like this assertion # Test invalid method name should return error response = await session.send_command(command="dkadklqwmd") assert "error" in response # Test int method should return error with pytest.raises( errors.MessageTypeError, ): await session.send_command(command=12345) choreographer-1.2.1/tests/test_tab.py000066400000000000000000000053661510421612300176710ustar00rootroot00000000000000import asyncio import logistro import pytest from choreographer import errors from choreographer.protocol import devtools_async # allows to create a browser pool for tests pytestmark = pytest.mark.asyncio(loop_scope="function") _logger = logistro.getLogger(__name__) # this ignores extra stuff in received- only that we at least have what is expected def check_response_dictionary(response_received, response_expected): for k, v in response_expected.items(): if isinstance(v, dict): check_response_dictionary(v, v) assert ( response_received.get( k, float("NaN"), ) == v ), f"Expected: {response_expected}\nReceived: {response_received}" @pytest.mark.asyncio async def test_create_and_close_session(browser): _logger.info("testing...") tab = await browser.create_tab("") session = await tab.create_session() assert isinstance(session, devtools_async.Session) await tab.close_session(session) assert session.session_id not in tab.sessions @pytest.mark.asyncio async def test_tab_send_command(browser): _logger.info("testing...") tab = await browser.create_tab("") # Test valid request with correct command response = await tab.send_command(command="Page.enable") check_response_dictionary(response, {"result": {}}) # Test invalid method name should return error response = await tab.send_command(command="dkadklqwmd") assert "error" in response # Test int method should return error with pytest.raises( errors.MessageTypeError, ): await tab.send_command(command=12345) @pytest.mark.asyncio async def test_subscribe_once(browser): _logger.info("testing...") tab = await browser.create_tab("") subscription_result = tab.subscribe_once("Page.*") _ = await tab.send_command("Page.enable") _ = await tab.send_command("Page.reload") _ = await subscription_result assert not subscription_result.exception() @pytest.mark.asyncio async def test_subscribe_and_unsubscribe(browser): _logger.info("testing...") tab = await browser.create_tab("") counter = 0 old_counter = counter async def count_event(_r): nonlocal counter counter += 1 tab.subscribe("Page.*", count_event) assert "Page.*" in next(iter(tab.sessions.values())).subscriptions await tab.send_command("Page.enable") await tab.send_command("Page.reload") await asyncio.sleep(0.5) assert counter > old_counter tab.unsubscribe("Page.*") old_counter = counter assert "Page.*" not in next(iter(tab.sessions.values())).subscriptions await tab.send_command("Page.enable") await tab.send_command("Page.reload") assert old_counter == counter choreographer-1.2.1/uv.lock000066400000000000000000005461711510421612300156600ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.8" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", "python_full_version < '3.9'", ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] [[package]] name = "backports-asyncio-runner" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] [[package]] name = "choreographer" source = { editable = "." } dependencies = [ { name = "logistro" }, { name = "simplejson" }, ] [package.dev-dependencies] dev = [ { name = "async-timeout" }, { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "mypy", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "numpy", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "poethepoet", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "poethepoet", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pyright" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-xdist", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-xdist", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "types-simplejson", version = "3.19.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "types-simplejson", version = "3.20.0.20250822", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.metadata] requires-dist = [ { name = "logistro", specifier = ">=2.0.1" }, { name = "simplejson", specifier = ">=3.19.3" }, ] [package.metadata.requires-dev] dev = [ { name = "async-timeout" }, { name = "mypy", specifier = ">=1.14.1" }, { name = "numpy", marker = "python_full_version < '3.11'" }, { name = "numpy", marker = "python_full_version >= '3.11'", specifier = ">=2.3.3" }, { name = "poethepoet", specifier = ">=0.30.0" }, { name = "pyright", specifier = ">=1.1.406" }, { name = "pytest" }, { name = "pytest-asyncio", marker = "python_full_version < '3.14'" }, { name = "pytest-asyncio", marker = "python_full_version >= '3.14'", specifier = ">=1.2.0" }, { name = "pytest-xdist" }, { name = "types-simplejson", specifier = ">=3.19.0.20241221" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "execnet" version = "2.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.9.*'", "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "logistro" version = "2.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, ] [[package]] name = "mypy" version = "1.14.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, ] [[package]] name = "mypy" version = "1.18.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, { name = "pathspec", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "numpy" version = "1.24.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/a4/9b/027bec52c633f6556dba6b722d9a0befb40498b9ceddd29cbe67a45a127c/numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", size = 10911229, upload-time = "2023-06-26T13:39:33.218Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/80/6cdfb3e275d95155a34659163b83c09e3a3ff9f1456880bec6cc63d71083/numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", size = 19789140, upload-time = "2023-06-26T13:22:33.184Z" }, { url = "https://files.pythonhosted.org/packages/64/5f/3f01d753e2175cfade1013eea08db99ba1ee4bdb147ebcf3623b75d12aa7/numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", size = 13854297, upload-time = "2023-06-26T13:22:59.541Z" }, { url = "https://files.pythonhosted.org/packages/5a/b3/2f9c21d799fa07053ffa151faccdceeb69beec5a010576b8991f614021f7/numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", size = 13995611, upload-time = "2023-06-26T13:23:22.167Z" }, { url = "https://files.pythonhosted.org/packages/10/be/ae5bf4737cb79ba437879915791f6f26d92583c738d7d960ad94e5c36adf/numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", size = 17282357, upload-time = "2023-06-26T13:23:51.446Z" }, { url = "https://files.pythonhosted.org/packages/c0/64/908c1087be6285f40e4b3e79454552a701664a079321cff519d8c7051d06/numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", size = 12429222, upload-time = "2023-06-26T13:24:13.849Z" }, { url = "https://files.pythonhosted.org/packages/22/55/3d5a7c1142e0d9329ad27cece17933b0e2ab4e54ddc5c1861fbfeb3f7693/numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", size = 14841514, upload-time = "2023-06-26T13:24:38.129Z" }, { url = "https://files.pythonhosted.org/packages/a9/cc/5ed2280a27e5dab12994c884f1f4d8c3bd4d885d02ae9e52a9d213a6a5e2/numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", size = 19775508, upload-time = "2023-06-26T13:25:08.882Z" }, { url = "https://files.pythonhosted.org/packages/c0/bc/77635c657a3668cf652806210b8662e1aff84b818a55ba88257abf6637a8/numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", size = 13840033, upload-time = "2023-06-26T13:25:33.417Z" }, { url = "https://files.pythonhosted.org/packages/a7/4c/96cdaa34f54c05e97c1c50f39f98d608f96f0677a6589e64e53104e22904/numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", size = 13991951, upload-time = "2023-06-26T13:25:55.725Z" }, { url = "https://files.pythonhosted.org/packages/22/97/dfb1a31bb46686f09e68ea6ac5c63fdee0d22d7b23b8f3f7ea07712869ef/numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", size = 17278923, upload-time = "2023-06-26T13:26:25.658Z" }, { url = "https://files.pythonhosted.org/packages/35/e2/76a11e54139654a324d107da1d98f99e7aa2a7ef97cfd7c631fba7dbde71/numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", size = 12422446, upload-time = "2023-06-26T13:26:49.302Z" }, { url = "https://files.pythonhosted.org/packages/d8/ec/ebef2f7d7c28503f958f0f8b992e7ce606fb74f9e891199329d5f5f87404/numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", size = 14834466, upload-time = "2023-06-26T13:27:16.029Z" }, { url = "https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", size = 19780722, upload-time = "2023-06-26T13:27:49.573Z" }, { url = "https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", size = 13843102, upload-time = "2023-06-26T13:28:12.288Z" }, { url = "https://files.pythonhosted.org/packages/25/6f/2586a50ad72e8dbb1d8381f837008a0321a3516dfd7cb57fc8cf7e4bb06b/numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", size = 14039616, upload-time = "2023-06-26T13:28:35.659Z" }, { url = "https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", size = 17316263, upload-time = "2023-06-26T13:29:09.272Z" }, { url = "https://files.pythonhosted.org/packages/d1/57/8d328f0b91c733aa9aa7ee540dbc49b58796c862b4fbcb1146c701e888da/numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", size = 12455660, upload-time = "2023-06-26T13:29:33.434Z" }, { url = "https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", size = 14868112, upload-time = "2023-06-26T13:29:58.385Z" }, { url = "https://files.pythonhosted.org/packages/9a/cd/d5b0402b801c8a8b56b04c1e85c6165efab298d2f0ab741c2406516ede3a/numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", size = 19816549, upload-time = "2023-06-26T13:30:36.976Z" }, { url = "https://files.pythonhosted.org/packages/14/27/638aaa446f39113a3ed38b37a66243e21b38110d021bfcb940c383e120f2/numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", size = 13879950, upload-time = "2023-06-26T13:31:01.787Z" }, { url = "https://files.pythonhosted.org/packages/8f/27/91894916e50627476cff1a4e4363ab6179d01077d71b9afed41d9e1f18bf/numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9", size = 14030228, upload-time = "2023-06-26T13:31:26.696Z" }, { url = "https://files.pythonhosted.org/packages/7a/7c/d7b2a0417af6428440c0ad7cb9799073e507b1a465f827d058b826236964/numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", size = 17311170, upload-time = "2023-06-26T13:31:56.615Z" }, { url = "https://files.pythonhosted.org/packages/18/9d/e02ace5d7dfccee796c37b995c63322674daf88ae2f4a4724c5dd0afcc91/numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", size = 12454918, upload-time = "2023-06-26T13:32:16.8Z" }, { url = "https://files.pythonhosted.org/packages/63/38/6cc19d6b8bfa1d1a459daf2b3fe325453153ca7019976274b6f33d8b5663/numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", size = 14867441, upload-time = "2023-06-26T13:32:40.521Z" }, { url = "https://files.pythonhosted.org/packages/a4/fd/8dff40e25e937c94257455c237b9b6bf5a30d42dd1cc11555533be099492/numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", size = 19156590, upload-time = "2023-06-26T13:33:10.36Z" }, { url = "https://files.pythonhosted.org/packages/42/e7/4bf953c6e05df90c6d351af69966384fed8e988d0e8c54dad7103b59f3ba/numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", size = 16705744, upload-time = "2023-06-26T13:33:36.703Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/9106005eb477d022b60b3817ed5937a43dad8fd1f20b0610ea8a32fcb407/numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", size = 14734290, upload-time = "2023-06-26T13:34:05.409Z" }, ] [[package]] name = "numpy" version = "2.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] [[package]] name = "numpy" version = "2.3.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", ] sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 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 = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "poethepoet" version = "0.30.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "pastel", marker = "python_full_version < '3.9'" }, { name = "pyyaml", marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/07/dfaed168414cf1e10f5c90860cdc29ffd871df80be81f3d7abd0451a4508/poethepoet-0.30.0.tar.gz", hash = "sha256:9f7ccda2d6525616ce989ca8ef973739fd668f50bef0b9d3631421d504d9ae4a", size = 60139, upload-time = "2024-11-09T22:16:58.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/98/12bff83ac39ba78ba4736c2f217bab294187de5d71ffbfeb3e126c230704/poethepoet-0.30.0-py3-none-any.whl", hash = "sha256:bf875741407a98da9e96f2f2d0b2c4c34f56d89939a7f53a4b6b3a64b546ec4e", size = 78036, upload-time = "2024-11-09T22:16:56.712Z" }, ] [[package]] name = "poethepoet" version = "0.37.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ { name = "pastel", marker = "python_full_version >= '3.9'" }, { name = "pyyaml", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/f2/273fe54a78dc5c6c8dd63db71f5a6ceb95e4648516b5aeaeff4bde804e44/poethepoet-0.37.0.tar.gz", hash = "sha256:73edf458707c674a079baa46802e21455bda3a7f82a408e58c31b9f4fe8e933d", size = 68570, upload-time = "2025-08-11T18:00:29.103Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/1b/5337af1a6a478d25a3e3c56b9b4b42b0a160314e02f4a0498d5322c8dac4/poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22", size = 90062, upload-time = "2025-08-11T18:00:27.595Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyright" version = "1.1.407" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "packaging", marker = "python_full_version < '3.9'" }, { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "packaging", marker = "python_full_version >= '3.9'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pygments", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-asyncio" version = "0.24.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, ] [[package]] name = "pytest-asyncio" version = "1.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] name = "pytest-xdist" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "execnet", marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060, upload-time = "2024-04-28T19:29:54.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108, upload-time = "2024-04-28T19:29:52.813Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] dependencies = [ { name = "execnet", marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" }, { url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" }, { url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" }, { url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" }, { url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" }, { url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" }, { url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" }, { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] name = "simplejson" version = "3.20.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039, upload-time = "2025-09-26T16:27:29.283Z" }, { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894, upload-time = "2025-09-26T16:27:30.378Z" }, { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116, upload-time = "2025-09-26T16:27:31.42Z" }, { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827, upload-time = "2025-09-26T16:27:32.486Z" }, { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772, upload-time = "2025-09-26T16:27:34.036Z" }, { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497, upload-time = "2025-09-26T16:27:35.211Z" }, { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172, upload-time = "2025-09-26T16:27:36.44Z" }, { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272, upload-time = "2025-09-26T16:27:37.605Z" }, { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468, upload-time = "2025-09-26T16:27:39.015Z" }, { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700, upload-time = "2025-09-26T16:27:40.171Z" }, { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323, upload-time = "2025-09-26T16:27:41.324Z" }, { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377, upload-time = "2025-09-26T16:27:42.533Z" }, { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081, upload-time = "2025-09-26T16:27:43.945Z" }, { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, { url = "https://files.pythonhosted.org/packages/2a/13/f290da83da1083051b1665e2508a70821fc1a62c4b6d73f5c7baadcba26c/simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127", size = 92074, upload-time = "2025-09-26T16:29:00.409Z" }, { url = "https://files.pythonhosted.org/packages/99/d9/d23e9f96762224870af95adafcd5d4426f5285b046ed331b034c6d5a8554/simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f", size = 74761, upload-time = "2025-09-26T16:29:01.574Z" }, { url = "https://files.pythonhosted.org/packages/48/5a/92bc0c1da0e805d4894ffe15a76af733e276d27eede5361c8be1c028e692/simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90", size = 75047, upload-time = "2025-09-26T16:29:02.704Z" }, { url = "https://files.pythonhosted.org/packages/bc/42/1ae6f9735d8fe47a638c5a342b835a2108ae7d7f79e7f83224d72c87cc81/simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84", size = 136635, upload-time = "2025-09-26T16:29:03.894Z" }, { url = "https://files.pythonhosted.org/packages/13/eb/7e087b061d6f94e6ba41c2e589267b9349fd3abb27ce59080c1c89fe9785/simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad", size = 146012, upload-time = "2025-09-26T16:29:05.189Z" }, { url = "https://files.pythonhosted.org/packages/08/a1/69a6e4ec69b585724cc9bee2d7f725c155d3ab8f9d3925b67c709a6e5a19/simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246", size = 133135, upload-time = "2025-09-26T16:29:06.475Z" }, { url = "https://files.pythonhosted.org/packages/6b/92/a75df930e2ff29e37654b65fa6eebef53812fa7258a9df9c7ddbf60610d7/simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce", size = 136834, upload-time = "2025-09-26T16:29:07.663Z" }, { url = "https://files.pythonhosted.org/packages/86/6f/2de88bea65e0fdb91cc55624bd77e2eaa5c3acccc59287b058b376acc9a2/simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15", size = 137298, upload-time = "2025-09-26T16:29:10.122Z" }, { url = "https://files.pythonhosted.org/packages/44/2b/dd9ec681115aa65620d57c88eb741bd7e7bc303ac6247554d854ee5168e6/simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4", size = 138287, upload-time = "2025-09-26T16:29:11.42Z" }, { url = "https://files.pythonhosted.org/packages/92/ee/8f45174d2988ec5242ab3c9229693ed6b839a4eb77ee42d7c470cc5846ab/simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13", size = 147295, upload-time = "2025-09-26T16:29:12.723Z" }, { url = "https://files.pythonhosted.org/packages/a5/ac/ab88e99111307eba64bcfbef45e8aa57240a19e019c2dc29269806d2f4a0/simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea", size = 138857, upload-time = "2025-09-26T16:29:13.927Z" }, { url = "https://files.pythonhosted.org/packages/2e/55/58f29500ee6f6bd78bfff4a0893cf9f050d3caf315f175e6263b6e8c0764/simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01", size = 74281, upload-time = "2025-09-26T16:29:15.119Z" }, { url = "https://files.pythonhosted.org/packages/69/c0/1ffd3fe5be619474f955fb8c1ec0a875a6abf3381a32c904c2061c646ba5/simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6", size = 75976, upload-time = "2025-09-26T16:29:16.226Z" }, { url = "https://files.pythonhosted.org/packages/b8/2d/7c4968c60ddc8b504b77301cc80d6e75cd0269b81a779b01d66d8f36dcb8/simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047", size = 94039, upload-time = "2025-09-26T16:29:17.406Z" }, { url = "https://files.pythonhosted.org/packages/e8/e4/d96b56fb87f245240b514c1fe552e76c17e09f0faa1f61137b2296f81529/simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562", size = 75893, upload-time = "2025-09-26T16:29:18.534Z" }, { url = "https://files.pythonhosted.org/packages/09/4f/be411eeb52ab21d6d4c00722b632dd2bd430c01a47dfed3c15ef5ad7ee6e/simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a", size = 76104, upload-time = "2025-09-26T16:29:19.66Z" }, { url = "https://files.pythonhosted.org/packages/66/6f/3bd0007b64881a90a058c59a4869b1b4f130ddb86a726f884fafc67e5ef7/simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881", size = 138261, upload-time = "2025-09-26T16:29:20.822Z" }, { url = "https://files.pythonhosted.org/packages/15/5d/b6d0b71508e503c759a0a7563cb2c28716ec8af9828ca9f5b59023011406/simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee", size = 146397, upload-time = "2025-09-26T16:29:22.363Z" }, { url = "https://files.pythonhosted.org/packages/19/24/40b3e5a3ca5e6f80cc1c639fcd5565ae087e72e8656dea780f02302ddc97/simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b", size = 134020, upload-time = "2025-09-26T16:29:23.615Z" }, { url = "https://files.pythonhosted.org/packages/b9/8c/8fc2c2734ac9e514124635b25ca8f7e347db1ded4a30417ee41e78e6d61c/simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a", size = 137598, upload-time = "2025-09-26T16:29:24.835Z" }, { url = "https://files.pythonhosted.org/packages/2e/d9/15036d7f43c6208fb0fbc827f9f897c1f577fba02aeb7a8a223581da4925/simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671", size = 139770, upload-time = "2025-09-26T16:29:26.244Z" }, { url = "https://files.pythonhosted.org/packages/73/cc/18374fb9dfcb4827b692ca5a33bdb607384ca06cdb645e0b863022dae8a3/simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f", size = 139884, upload-time = "2025-09-26T16:29:28.51Z" }, { url = "https://files.pythonhosted.org/packages/5c/a2/1526d4152806670124dd499ff831726a92bd7e029e8349c4affa78ea8845/simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7", size = 148166, upload-time = "2025-09-26T16:29:29.789Z" }, { url = "https://files.pythonhosted.org/packages/a4/77/fc16d41b5f67a2591c9b6ff7b0f6aed2b2aed1b6912bb346b61279697638/simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba", size = 140778, upload-time = "2025-09-26T16:29:31.025Z" }, { url = "https://files.pythonhosted.org/packages/4a/97/a26ef6b7387349623c042f329df70a4f3baf3a365fe6d1154d73da1dcf5a/simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472", size = 74339, upload-time = "2025-09-26T16:29:32.648Z" }, { url = "https://files.pythonhosted.org/packages/dc/b7/94c6049a99e3c04eed2064e91295370b7429e2361188e35a78df562312e0/simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502", size = 76067, upload-time = "2025-09-26T16:29:34.184Z" }, { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, ] [[package]] name = "tomli" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] name = "types-simplejson" version = "3.19.0.20241221" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/a7/f0/3d4dd216dc527a52ab564cbecd2fd8b3c8b96722348745f8f5cb9ab59801/types_simplejson-3.19.0.20241221.tar.gz", hash = "sha256:114af9db0f49ad15755d2b6ad8e6fd04b5a493815e2fc1e011729d4650defc70", size = 9688, upload-time = "2024-12-21T02:40:56.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/2f/4a5fcab9225bec1d075d543d00f005fe83685ddc1bd395ab1b382b9553db/types_simplejson-3.19.0.20241221-py3-none-any.whl", hash = "sha256:179dfaef8c357156c781fa47cfdfcd953a7953fc375dfe9ab19a20054a828980", size = 10285, upload-time = "2024-12-21T02:40:55.706Z" }, ] [[package]] name = "types-simplejson" version = "3.20.0.20250822" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.11' and python_full_version < '3.14'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ]