pax_global_header00006660000000000000000000000064152576140250014521gustar00rootroot0000000000000052 comment=b16bc062e5b92867a086cb6cc82c3b666a6bc4f9 cogent3-scinexus-b16bc06/000077500000000000000000000000001525761402500153275ustar00rootroot00000000000000cogent3-scinexus-b16bc06/.claude-plugin/000077500000000000000000000000001525761402500201365ustar00rootroot00000000000000cogent3-scinexus-b16bc06/.claude-plugin/marketplace.json000066400000000000000000000012251525761402500233210ustar00rootroot00000000000000{ "name": "scinexus", "owner": { "name": "The scinexus authors", "url": "https://github.com/cogent3/scinexus" }, "description": "The scinexus agent skill, for building applications with scinexus", "plugins": [ { "name": "scinexus", "source": "./", "description": "Writing composable, type-checked data pipelines with scinexus: define_app, NotCompleted, data stores, apply_to, parallel backends", "homepage": "https://scinexus.readthedocs.io", "repository": "https://github.com/cogent3/scinexus", "license": "BSD-3-Clause", "keywords": ["scinexus", "pipelines", "scientific-computing"] } ] } cogent3-scinexus-b16bc06/.claude-plugin/plugin.json000066400000000000000000000011061525761402500223250ustar00rootroot00000000000000{ "name": "scinexus", "displayName": "scinexus", "version": "2026.10.2", "description": "Writing composable, type-checked data pipelines with scinexus: define_app, NotCompleted, data stores, apply_to, parallel backends", "author": { "name": "The scinexus authors", "url": "https://github.com/cogent3/scinexus" }, "homepage": "https://scinexus.readthedocs.io", "repository": "https://github.com/cogent3/scinexus", "license": "BSD-3-Clause", "keywords": [ "scinexus", "pipelines", "scientific-computing" ], "skills": [ "./skills/" ] }cogent3-scinexus-b16bc06/.github/000077500000000000000000000000001525761402500166675ustar00rootroot00000000000000cogent3-scinexus-b16bc06/.github/dependabot.yml000066400000000000000000000007221525761402500215200ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: weekly time: "19:00" open-pull-requests-limit: 10 cooldown: default-days: 7 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" time: "19:00" open-pull-requests-limit: 10 cooldown: default-days: 7 groups: codeql-action: patterns: - "github/codeql-action*" cogent3-scinexus-b16bc06/.github/workflows/000077500000000000000000000000001525761402500207245ustar00rootroot00000000000000cogent3-scinexus-b16bc06/.github/workflows/ci.yml000066400000000000000000000071571525761402500220540ustar00rootroot00000000000000name: CI on: push: pull_request: # NOTE: # if changing python versions, also update versions in # - release.yml # - noxfile.py jobs: tests: name: "Python ${{ matrix.python-version }} (${{ matrix.os }})" runs-on: ${{ matrix.os }} strategy: # without this a failure on an exploratory free-threaded build cancels # the supported cells, which then never upload coverage fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.11", "3.14", "3.14t"] steps: - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 with: fetch-depth: 0 # Setup env - uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0 with: python-version: "${{ matrix.python-version }}" # nox is invoked with --force-python python, so it takes whatever is on # PATH. A GIL-enabled build there would pass silently as a duplicate of # the 3.14 cell rather than testing anything new. - name: "Confirm the interpreter is free-threaded" if: endsWith(matrix.python-version, 't') shell: bash run: python -c "import sys; print(sys.version); assert not sys._is_gil_enabled()" - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Install MPI (Ubuntu) if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y openmpi-bin libopenmpi-dev - name: "Run nox for ${{ matrix.python-version }}" shell: bash run: | lname="snx-${{matrix.os}}-${{matrix.python-version}}.lcov" xname="snx-${{matrix.os}}-${{matrix.python-version}}.xml" cov="lcov -o$lname xml -o$xname" uv run --group dev nox --force-python python -s testcov -- $cov - name: Coveralls Parallel uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 env: # macOS installs the coverage-reporter via a third-party Homebrew # tap; Homebrew's new tap-trust enforcement refuses it otherwise. HOMEBREW_NO_REQUIRE_TAP_TRUST: 1 with: parallel: true github-token: ${{ secrets.github_token }} flag-name: run-${{matrix.python-version}}-${{matrix.os}} file: "snx-${{matrix.os}}-${{matrix.python-version}}.lcov" type_check: name: Type Check runs-on: ${{ matrix.os }} strategy: matrix: python-version: ["3.14"] os: [ubuntu-latest] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0 with: python-version: "${{ matrix.python-version }}" - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: "Run Type Checking for ${{ matrix.python-version }}" run: | uv run --group dev nox --force-python python -s type_check finish: name: "Finish Coveralls" needs: tests runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 with: github-token: ${{ secrets.github_token }} parallel-finished: truecogent3-scinexus-b16bc06/.github/workflows/codeql.yml000066400000000000000000000017411525761402500227210ustar00rootroot00000000000000name: "CodeQL" on: push: branches-ignore: - master pull_request: branches-ignore: - master schedule: - cron: '39 20 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: category: "/language:${{matrix.language}}" cogent3-scinexus-b16bc06/.github/workflows/docs.yml000066400000000000000000000045641525761402500224100ustar00rootroot00000000000000name: Build Docs concurrency: group: docs-build-${{ github.ref }} cancel-in-progress: true on: workflow_dispatch: release: types: [published] push: branches: - main paths: - 'docs/**' - 'overrides/**' - 'skills/**' - 'scripts/publish_skills.py' - '.readthedocs.yaml' - 'zensical.toml' - 'rtd_get_docs.py' permissions: contents: read jobs: build-docs: if: github.repository == 'cogent3/SciNexus' runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 submodules: recursive - uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0 with: python-version: "3.14" - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Build Documentation run: | # update executable components uv run --group dev nox -s cogdocs # build actual docs uv run --group dev --group doc zensical build --clean # agent skills, which zensical cannot place: it does not copy a # hidden directory out of docs/ uv run python scripts/publish_skills.py site working-directory: ${{ github.workspace }} - name: Upload Documentation Artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: scinexus-docs-html path: site # without this the .well-known directory is dropped from the # artifact and every published skill URL becomes a 404 include-hidden-files: true trigger_rtd: if: github.repository == 'cogent3/SciNexus' runs-on: ubuntu-latest needs: build-docs steps: - name: Trigger Read the Docs build env: RTDS_WEBHOOK_URL: ${{ secrets.RTDS_WEBHOOK_URL }} RTDS_WEBHOOK_TOKEN: ${{ secrets.RTDS_WEBHOOK_TOKEN }} BRANCH: ${{ github.ref_name }} run: | curl -sS --fail-with-body -X POST \ --data-urlencode "token=${RTDS_WEBHOOK_TOKEN}" \ --data-urlencode "branches=${BRANCH}" \ "${RTDS_WEBHOOK_URL}" cogent3-scinexus-b16bc06/.github/workflows/linters.yml000066400000000000000000000017521525761402500231340ustar00rootroot00000000000000name: Lint code using ruff on: push: pull_request: jobs: linters: runs-on: ubuntu-latest if: github.repository != 'cogent3/SciNexus' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Format code using ruff run: uv run --group dev nox -s fmt - name: Commit changes uses: EndBug/add-and-commit@cc9c08ba6c8df3b93a8f2db63e89b98368ae2ae8 # v11.1.1 with: author_name: ${{ github.actor }} author_email: ${{ github.actor }}@users.noreply.github.com message: "STY: pre-commit linting with ruff" add: "." cogent3-scinexus-b16bc06/.github/workflows/release.yml000066400000000000000000000115521525761402500230730ustar00rootroot00000000000000name: Release on: [workflow_dispatch] jobs: test: name: "Test on Python ${{ matrix.python-version }} (${{ matrix.os }})" runs-on: ${{ matrix.os }} strategy: # without this a failure on an exploratory free-threaded build cancels # the supported cells, hiding whether they would have passed fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 with: fetch-depth: 0 # Setup env - uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0 with: python-version: "${{ matrix.python-version }}" # nox is invoked with --force-python python, so it takes whatever is on # PATH. A GIL-enabled build there would pass silently as a duplicate of # the 3.14 cell rather than testing anything new. - name: "Confirm the interpreter is free-threaded" if: endsWith(matrix.python-version, 't') shell: bash run: python -c "import sys; print(sys.version); assert not sys._is_gil_enabled()" - name: Install MPI (Ubuntu) if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y openmpi-bin libopenmpi-dev - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: "Run nox for ${{ matrix.python-version }}" shell: bash run: | uv run --group dev nox -db uv --force-python python -s test uv run --group dev nox -db uv --force-python python -s test_types # the doc group pins cogent3[extra], which pulls kaleido and so orjson, # and orjson publishes no free-threaded wheels - name: "Run doc tests for ${{ matrix.python-version }}" if: ${{ !endsWith(matrix.python-version, 't') }} shell: bash run: | uv run --group dev nox -db uv --force-python python -s test_docs - name: "Run MPI tests" if: startsWith(matrix.os, 'ubuntu') shell: bash run: | uv run --group dev nox -db uv --force-python python -s testmpi docbuild: name: "Test the docs" runs-on: ubuntu-latest steps: - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 with: fetch-depth: 0 - uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0 with: python-version: "3.14" - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: "test the docs code" run: | uv run --group dev nox -s cogdocs uv run --group dev --group doc zensical build build: name: Build wheel and sdist needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Build sdist and wheel run: uv build - name: Upload sdist and wheel uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: snx-wheel-sdist path: | ./dist/*.whl ./dist/*.tar.gz release_test: name: Release to Test PyPI needs: [build, docbuild] environment: release_test runs-on: ubuntu-latest permissions: id-token: write steps: - name: Download sdist and wheel uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: snx-wheel-sdist path: ./dist - name: Publish package distributions to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ release: name: Release to PyPI needs: release_test environment: release runs-on: ubuntu-latest permissions: id-token: write steps: - name: Download sdist and wheel uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: snx-wheel-sdist path: ./dist - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 cogent3-scinexus-b16bc06/.gitignore000066400000000000000000000011521525761402500173160ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .nox *.nox nosetests.xml tests/draw_results .pytest_cache *.pytest_cache .mypy_cache tests/junit-*.xml tests/coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject *.vscode # vi .*.swp #Ignore vscode AI rules .github/instructions/codacy.instructions.md CLAUDE.md .idea/* .DS_Store *.wpr *.wpu .cache/* .claude/settings.local.json __pycache__ _build .codacy working sitecogent3-scinexus-b16bc06/.hgignore000066400000000000000000000006271525761402500171370ustar00rootroot00000000000000syntax:glob .svn *.pyc *.pyo *.so *.o *.DS_Store *.tmproj *.rej *.orig *.wpr *.pdf _build/* build *htmlcov* *.idea *.coverage* *egg-info* *.wpu .cache* *taskpaper *.ipynb *.ipynb_checkpoints* *.sublime* *.patch *.pytest_cache *.tox *.nox *.vscode *.code-workspace coverage.xml __pycache__ junit-*.xml dist/* working/* lcov*.info .ruff_cache/* .devcontainer/* venv* .venv* .mypy* CLAUDE.md site/* docs/data/*cogent3-scinexus-b16bc06/.readthedocs.yaml000066400000000000000000000011751525761402500205620ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file for MkDocs projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.14" commands: # Install the required dependencies - pip install requests # Run the script to download and extract the pre-built docs - python rtd_get_docs.py - echo "Documentation downloaded and extracted" # Disable the default build processes since we're using pre-built docs sphinx: configuration: null python: install: [] cogent3-scinexus-b16bc06/LICENSE000066400000000000000000000027261525761402500163430ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2026, cogent3 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cogent3-scinexus-b16bc06/README.md000066400000000000000000000134211525761402500166070ustar00rootroot00000000000000

scinexus

[![Coverage Status](https://coveralls.io/repos/github/cogent3/scinexus/badge.svg?branch=main)](https://coveralls.io/github/cogent3/scinexus?branch=main) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/fd8810efd4f142069bd84144e14350b4)](https://app.codacy.com/gh/cogent3/scinexus/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![CI](https://github.com/cogent3/scinexus/actions/workflows/ci.yml/badge.svg)](https://github.com/cogent3/scinexus/actions/workflows/ci.yml) ![PyPI Downloads](https://img.shields.io/pypi/dm/scinexus) *`scinexus` is a framework for rapid development of data processing applications. It enables interoperability between objects through defined data types, allowing development of scientific domain app ecosystems. Just as `attrs` and `dataclasses` use type hints to simplify data type definition, `scinexus` uses them to simplify writing best-practice scientific algorithms.* Many scientific problems require repeating calculations across many files or database records. Such tasks suit data-level parallelism, but writing robust, maintainable code for them is often tedious and quickly becomes complex. As the Unix philosophy articulates, writing algorithms that do one thing well and can be composed together through piping data of known type is a *Very Good Thing*â„¢. **`scinexus` encourages this design pattern and eliminates the boilerplate.** We leverage the Python type annotation system to govern the compatibility (composability) of different applications. This enables in-process composition of your applications with validation of the consistency of the pipeline and the consistency of the data being run through it. **`scinexus` is designed for scientific reproducibility.** Scientific computations should record all conditions needed to reproduce an analysis. `scinexus` reduces the effort by intercepting all arguments (including defaults) used in app construction and logging the resulting app state. ## Examples Developers can choose inheriting from a base class or use the `scinexus.define_app` decorator to make composable apps. The following examples show simple composition
Loading files so missing data does not cause a crash ```python from scinexus import define_app @define_app(app_type="loader") def read_json(path: str) -> dict: import json with open(path) as f: return json.load(f) @define_app def validate(data: dict, required_field: str) -> dict: if required_field not in data: # this becomes a NotCompleted sentinel object # your run doesn't crash! raise ValueError(f"missing {required_field!r} field") return data app = read_json() + validate(required_field="name") ``` You can apply `app` to a single file path as `app(filepath)`, or operate in parallel (and show a progress bar) on a sequence of file paths as ```python results = list(app.as_completed(["some_file_path.json", "some_other_file_path.json"], parallel=True, show_progress=True) ```
A contrived numerical example ```python from scinexus import define_app @define_app def normalise(values: list[float]) -> list[float]: lo, hi = min(values), max(values) return [(v - lo) / (hi - lo) for v in values] @define_app def threshold(values: list[float]) -> list[bool]: return [v > 0.5 for v in values] app = normalise() + threshold() app([1.0, 5.0, 3.0, 9.0]) ```
A configurable app ```python from scinexus import define_app @define_app(app_type="loader") def load_csv(path: str) -> list[dict]: import csv with open(path) as f: return list(csv.DictReader(f)) @define_app class summarise: def __init__(self, column: str) -> None: """column contains the values to produce summary stats for""" self.column = column def main(self, rows: list[dict]) -> dict[str, float]: vals = [float(r[self.column]) for r in rows] return {"mean": sum(vals) / len(vals), "min": min(vals), "max": max(vals)} app = load_csv() + summarise(column="price") ```
## Features - Type checking at composition time - Durable computing -- failures recorded as `NotCompleted` records, not exceptions - Data-level parallel execution with pluggable backends (stdlib, loky, MPI, or custom) - Progress bars (`tqdm` or `rich`) - Automated logging and citation tracking - Checkpointing via data stores (directory, SQLite) ## Installation ```bash pip install scinexus ``` ## The `scinexus` origin story The app framework and utility functions in `scinexus` incubated inside [cogent3](https://github.com/cogent3/cogent3) from March 2019, accumulating over seven years of development, testing, and real-world use in computational genomics before being extracted into a standalone package. The design is mature and has underpinned analyses in published studies. The extraction into `scinexus` makes the infrastructure available to any scientific Python project, free of the `cogent3` dependency. See the [changelog](changelog.md) for a detailed list of changes from the cogent3 app infrastructure. We acknowledge here that many members of the `cogent3` community contributed to the code that now lives here, including [@GavinHuttley](https://github.com/GavinHuttley), [@rmcar17](https://github.com/rmcar17), [@Nick-Foto](https://github.com/Nick-Foto), [@KatherineCaley](https://github.com/KatherineCaley), [@fredjaya](https://github.com/fredjaya), and [@khiron](https://github.com/khiron). cogent3-scinexus-b16bc06/changelog.d/000077500000000000000000000000001525761402500175005ustar00rootroot00000000000000cogent3-scinexus-b16bc06/changelog.d/README.md000066400000000000000000000036331525761402500207640ustar00rootroot00000000000000# Using `scriv` ## Adding changelog entries This directory is used to store changelog fragments for the next release of the project. Each file should contain a small changelog fragment that will be added to the full changelog when the release is made. The file is created using ``` scriv create --edit ``` to create a change entry This will create a file with the correct name and format and commented out sample categories, and load it into your `git` editor (the one specified by ```git config --global core.editor``` ) Uncomment the category of change you are making and add a short description of the change as a markdown bullet point. For example: Contributors * khiron Enhancements * Added a new feature that allows users to do Y Deprecations * Removed deprecated feature Z Bug fixes * Fixed a bug that caused the project to crash when a user did X by doing Y instead Documentation * Documented feature Z Check the file in with your changes. --- ## Building a changelog To build the changelog for the next release, run ``` scriv collect ``` This will create a file called `CHANGELOG.md` in the root of the project. This file will contain the full changelog for the project, including all the fragments that have been added to the changelog.d directory and remove those fragments. Note this requires that the `__init__.py` file's `__version__` variable is updated from the last time you ran collect, otherwise you will get a warning ``` Entry 'Changes since release 0.0.1' already uses version '0.0.1'. ``` ... and the fragments will not be collected. When the `__version__` variable in the project is updated the next time you run collect, the fragments will be collected and the changelog will be updated. You can override this to create a new collection of all available fragments that are not aligned with a `__version__` with ``` scriv collect --version "description of a milestone not yet used" ``` cogent3-scinexus-b16bc06/changelog.d/templates/000077500000000000000000000000001525761402500214765ustar00rootroot00000000000000cogent3-scinexus-b16bc06/changelog.d/templates/new.md.j2000066400000000000000000000003541525761402500231250ustar00rootroot00000000000000 {% for cat in config.categories -%} {% endfor -%} cogent3-scinexus-b16bc06/changelog.d/templates/title.md.j2000066400000000000000000000000401525761402500234450ustar00rootroot00000000000000Changes in release "{{version}}"cogent3-scinexus-b16bc06/changelog.md000066400000000000000000000156251525761402500176110ustar00rootroot00000000000000# Changelog ## Changes in release "2026.10.2" ### API - A writer's own `id_from_source` now names the records it writes. Before, `__call__`, `apply_to` and `as_completed` always used the global `get_id_from_source()` and ignored the writer's own setting. A custom extractor must be picklable under `parallel=True`, and `apply_to` uses it to work out which records are already done. ### Contributors - @GavinHuttley ### Enhancements - Apps now record which constructor arguments were passed explicitly, separately from the ones that took their defaults. - When a SQLite store's lock is refused, the message now says who holds it: this thread, another thread in this process, or another process. For the first two, it suggests reusing or closing the open store instead of `unlock(force=True)`. ### Bug fixes - Member ids are spelled with `/` on every platform, including Windows. This covers SQLite log ids too. - Passing `id_from_source` to `as_completed` or `apply_to` no longer leaks into later calls. Re-composing an applied writer no longer raises `TypeError`. - Calling `lock()` on a store that already holds its lock no longer fails. ### Documentation - Added a vendor-neutral scinexus agent skill (`skills/scinexus/SKILL.md`) with reference files on pipelines, IO and extending. - The docs site now publishes the skill at the standard `.well-known/agent-skills/` paths. - Improved the wording of the parallel-usage docs and the developer docs. ## Differences from cogent3 Changes from the original cogent3 app infrastructure. ### New Features - Standalone package extracted from `cogent3.app` — no cogent3 dependency required. - Generic base classes `AppBase[T, R]`, `ComposableApp[T, R]`, and `WriterApp[T, R]` that apps can inherit from directly as an alternative to the `@define_app` decorator. Type checkers can resolve types through class inheritance without a plugin. - mypy plugin (`scinexus._mypy_plugin`) for correct type inference of `@define_app` decorated classes. Synthesises the `__call__` return type as `R | NotCompleted`. - `check_data_type` attribute on apps — a settable property to toggle runtime input type checking on or off. Disabling can speed up execution and simplify debugging. - `NotCompletedType` enum (`ERROR`, `FAIL`, `BUG`) for categorising failure types, replacing bare strings. - `set_summary_display()` / `get_summary_display()` — a module-level registry allowing downstream packages (e.g. cogent3) to register custom display functions for data store summary methods (`describe`, `summary_logs`, `summary_not_completed`, `summary_citations`, `validate`). - `citations` and `bib` properties on apps for tracking software citations via the `citeable` library. Citations propagate through composed pipelines. - Pluggable parallel backends -- choose between stdlib multiprocessing, loky, or MPI via `set_parallel_backend()`. - Pluggable progress bars -- use `tqdm` or `rich` via the `Progress` protocol and `set_default_progress()`. - `set_id_from_source()` / `get_id_from_source()` -- register a custom function for extracting storage identifiers from data. - `apply_to()` accepts `logger=False` to disable log file creation. ### Enhancements - App composition (`+`) now makes shallow copies of the right-hand operand. Composed pipelines no longer share mutable state. - Composition-time type compatibility checking via `check_type_compatibility()` — catches type mismatches when apps are composed with `+`, before any data is processed. - Data store summary methods (`describe`, `summary_logs`, etc.) return `list[dict]` or `dict` instead of cogent3 `Table` objects. Custom display can be restored via `set_summary_display()`. - All modules pass mypy strict type checking. - `StrOrBytes` type alias replaced with `str | bytes` throughout. - Inline `assert` statements replaced with explicit `ValueError` / `TypeError` raises. - Type-hint-related imports moved under `TYPE_CHECKING` for lighter runtime import overhead. - `max_workers` below 1 is refused by the local backends with a message naming the value, where it previously reached the executor and surfaced that library's own error. `max_workers=0` was a silent synonym for `None` and is now an error, and a `bool` raises `TypeError` where `True` previously asked for one worker, so `None` is the only way to ask for one worker per CPU. The MPI backend is unchanged and still reads `0` as one worker. - `chunksize` follows the same rule as `max_workers`: below 1 raises `ValueError` and a `bool` raises `TypeError`, where `0` and `False` previously meant "no preference" and `True` asked for one item per chunk. Every call that accepts the argument now checks it, including the ones that go on to ignore it — only `imap` on the process and MPI backends chunks the work by it, since `as_completed` submits one task per item and a thread pool has no per-item transport cost to amortise. - A writable `DataStoreSqlite` is created only by the master process, matching `DataStoreDirectory`, which has always returned early off the master. Construction off the master still succeeds, and the first read or write that reaches a database which does not exist, or a file carrying no schema, now raises `OSError` instead of creating the database and its tables. So an app that opens a writable store of its own inside a worker no longer works, as it already did not for a directory store. Using a store the master made is unchanged, and an in-memory store is exempt: it has no file to race over, so refusing would remove it outright for no gain. - `open_()` takes a bare `"r"` or `"w"` as text for every suffix, matching builtin `open` rather than `gzip`, `bz2` and `lzma`, which take a bare mode as binary. Previously `open_(path, "w")` on a `.gz` gave a binary handle and `open_(path, "r")` raised. Use `"rb"` or `"wb"` for bytes. This also applies to `atomic_write`, whose mode defaults to `"w"`. - A writer constructed with an `id_from_source` argument now names records with it. Calling the app, `apply_to()` and `as_completed()` all consult it before the function registered through `set_id_from_source()`, so a writer that takes its own callable is no longer overruled by the framework. Passing `id_from_source=` to `apply_to()` or `as_completed()`, or `identifier=` to the call, still wins over both. Only a writer is consulted, since `id_from_source` means whatever its author chose on any other app type. The user provided function must be picklable to run under `parallel=True`. - `id_from_source` passed to `apply_to()` or `as_completed()` is a setting on the app rather than a one-off. It applies to later calls of either method until another is given, and on a writer to calling the app as well, and it outranks the `id_from_source` a writer was constructed with. ### Deprecated - `ComposableApp.disconnect()` — discontinued, will be removed in version 2026.9. No longer required since composition uses shallow copies. cogent3-scinexus-b16bc06/docs/000077500000000000000000000000001525761402500162575ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/conftest.py000066400000000000000000000003141525761402500204540ustar00rootroot00000000000000from scinexus.typing import register_type_namespace try: from cogent3.app.typing import _get_resolution_namespace register_type_namespace(_get_resolution_namespace) except ImportError: pass cogent3-scinexus-b16bc06/docs/explanation/000077500000000000000000000000001525761402500206015ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/explanation/app-lifecycle.md000066400000000000000000000022021525761402500236340ustar00rootroot00000000000000# The app lifecycle !!! abstract "" The different app types, their base classes, and how `define_app` transforms a user defined class or function into a composable app. ## Types of apps ### Loaders These are responsible for loading data and are composable. They inherit from `LoaderApp`. ```python from scinexus import LoaderApp ``` ### Writers These are responsible for writing data and are composable. They inherit from `WriterApp`. ```python from scinexus import WriterApp ``` ### Generic Generic apps do other operations on data and are composable. They inherit from `ComposableApp` ```python from scinexus import ComposableApp ``` ### Non-composable Non-composable apps cannot be combined with other apps into pipelines. ```python from scinexus import NonComposableApp ``` !!! info You can create your app by inheriting from one of the above base classes. Or you can use the `define_app` decorator. Using the decorator is the fastest way to turn something you already have into a composable app. Under the hood, the decorator is basically injecting the base classes described above into the inheritance of your own classes. cogent3-scinexus-b16bc06/docs/explanation/customisation-hooks.md000066400000000000000000000101511525761402500251430ustar00rootroot00000000000000# Customisation hooks !!! abstract "" How `scinexus` uses module-level registry functions to let downstream packages customise summary display and identifier extraction without subclassing. ## The pattern `scinexus` uses module-level registry functions to let downstream packages customise behaviour without subclassing or monkey-patching. Each hook follows the same shape: - **`set_*(func)`** registers a callable (or `None` to clear) - **`get_*()`** returns the currently registered callable (or the default) This keeps `scinexus` free of dependencies on downstream packages while still allowing them to integrate deeply. ## `set_summary_display` — transforming summary output Data store summary properties (`.describe`, `.summary_logs`, `.summary_not_completed`, `.summary_citations`, `.validate()`) collect their data as plain Python dicts or lists of dicts. By default these are returned as-is. A downstream package can register a display function that transforms these raw structures into richer objects. The function must accept `(data, *, name)` where `data` is the raw dict or list and `name` is the summary method name (e.g. `"describe"`). ### How `cogent3` uses this When `cogent3.app` is imported, it registers a function that converts summaries into `cogent3.core.table.Table` objects: ```python { notest } from scinexus.data_store import set_summary_display from cogent3.core.table import Table def _summary_to_table(data, *, name): if isinstance(data, dict): title = data.pop("title", name) rows = [[k, v] for k, v in data.items()] return Table( header=["Condition", "Value"], data=rows, title=title, ) if isinstance(data, list): if not data: return Table(header=[], data=[], title=name) header = list(data[0].keys()) rows = [list(row.values()) for row in data] return Table(header=header, data=rows, title=name) return data set_summary_display(_summary_to_table) ``` After this registration, every call to `dstore.describe` or `dstore.summary_not_completed` returns a `Table` with a rich notebook repr, rather than a plain dict. ## `set_id_from_source` — customising unique ID extraction When `apply_to()` or `as_completed()` processes a data store, each result needs a unique identifier so the writer can store it and skip already-processed inputs on subsequent runs. By default, `scinexus` extracts this ID using `get_unique_id`, which strips format suffixes from file names: ``` "gene_001.fasta.gz" → "gene_001" ``` If your data uses a different naming convention — for example, IDs embedded in the file content or in a metadata field — you can register a custom extractor: ```python { notest } from scinexus.data_store import set_id_from_source def my_id_extractor(data): """Extract ID from a metadata dict.""" return data.info.source.split("/")[-1].split("_")[0] set_id_from_source(my_id_extractor) ``` The registered function is consulted by: - `WriterApp.apply_to()` — to derive output record keys - `AppBase.as_completed()` — to identify results - `NotCompleted` — to normalise the `source=` attribute on error records Pass `None` to restore the default: ```python { notest } set_id_from_source(None) # back to get_unique_id ``` Per-call overrides via the `id_from_source` keyword on `apply_to()` and `as_completed()` still take precedence over the registered function. ## The default ID pipeline: `get_data_source` → `get_unique_id` The default extractor, `get_unique_id`, works in two steps: 1. **`get_data_source(data)`** extracts a source string from the input. This is a singledispatch function that handles: - `str` / `Path` → the file name - `dict` → looks for `data["info"]["source"]` or `data["source"]` - `DataMemberABC` → the member's `unique_id` - Any object with a `.source` attribute → recurses on that attribute 2. **`get_unique_id(name)`** strips format suffixes (e.g. `.fasta`, `.gz`) from the source string returned by `get_data_source`. Together they turn inputs like `DataMember(unique_id="gene_001.fasta.gz")` into the key `"gene_001"`. cogent3-scinexus-b16bc06/docs/explanation/data-store-model.md000066400000000000000000000062321525761402500242670ustar00rootroot00000000000000# Data store model !!! abstract "" How data stores map inputs to outputs via unique IDs, how checkpointing works to skip already-processed items, the three backends (directory, zip, SQLite) and when to use each, and how citations and logs are stored alongside results. ## Data stores -- collections of data records If you download [raw.zip](../data/raw.zip) and unzip it, you will see it contains 1,035 files ending with a `.fa` filename suffix. (It also contains a tab delimited file and a log file, which we ignore for now.) The directory `raw` is a "data store" and the `.fa` files are "members" of it. In summary, a data store is a collection of members of the same "type". This means we can apply the same application to every member. ### Types of data store | Class Name | Supported Operations | Supported Data Types | Identifying Suffix | |---|---|---|---| | `DataStoreDirectory` | read / write / append | text | None | | `ReadOnlyDataStoreZipped` | read | text | `.zip` | | `DataStoreSqlite` | read, write, append | text or bytes | `.sqlitedb` | !!! note The `ReadOnlyDataStoreZipped` is just a compressed `DataStoreDirectory`. ### The structure of data stores If a directory was not created by `scinexus` as a `DataStoreDirectory` then it has only the structure that existed previously. If a data store was created by `scinexus`, either as a directory or as a `sqlitedb`, then it contains four types of data: completed records, *not* completed records, log files and md5 files. In a `DataStoreDirectory`, these are organised using the file system. The completed members are valid data records (as distinct from not completed) and are at the top level. The remaining types are in subdirectories. ``` demo_dstore ├── logs ├── md5 ├── not_completed └── ... ``` `logs/` stores `scitrack` log files produced by `scinexus` writer apps. `md5/` stores plain text files with the md5 sum of a corresponding data member which are used to check the integrity of the data store. A checksum file is named for the record it belongs to and for which kind of record that is: `md5/id_0.cmplt` for a completed record and `md5/id_0.ncmplt` for a not completed one. !!! note Earlier versions kept both kinds under `md5/id_0.txt`, and reading such a store still works, since `md5()` falls back to that name. `validate()` counts them under `md5_legacy`, and `migrate_checksums()` renames the ones it can attribute: ```python { notest } dstore = open_data_store("results", suffix="fasta", mode="w") dstore.migrate_checksums() # {'migrated': 12, 'ambiguous': ['id_3'], 'orphaned': ['id_9'], 'superseded': []} ``` `migrate_checksums()` needs `mode="w"`, because it rewrites files already in the store. A file is reported rather than renamed in three cases: `ambiguous` when both kinds of record carry its name, since it holds whichever of them wrote last and that was never recorded; `orphaned` when no record carries it; and `superseded` when that record already has a checksum under the current name, which was written for it by this version and so is the one to trust. The `DataStoreSqlite` stores the same information, just in SQL tables. cogent3-scinexus-b16bc06/docs/explanation/flow.md000066400000000000000000000043651525761402500221020ustar00rootroot00000000000000# Execution flow of a composed app !!! abstract "" How data flows through a composed pipeline, step by step. Consider two apps composed into a pipeline: ```python { notest } from scinexus import define_app @define_app(app_type="loader") def read_json(path: str) -> dict: import json with open(path) as f: return json.load(f) @define_app def validate(data: dict, required_field: str) -> dict: if required_field not in data: raise ValueError(f"missing {required_field!r} field") return data app = read_json() + validate(required_field="name") ``` Composing with `+` creates a new app where `validate` is the outermost app and `read_json` is stored as its `.input` attribute. When you call `app(filepath)`, execution begins at the outermost app and works inward. ## The execution flow when you call `app(filepath)` ```mermaid flowchart TD entry["Executes scinexus __call__(val)"] --> none{val is None?} none -- yes --> nc_none[create and return NotCompleted ERROR, recording current app as origin] none -- no --> nc{val is NotCompleted?} nc -- yes --> nc_return[returns same NotCompleted] nc -- no --> has_input{has an input app?} has_input -- yes --> call_input["call input(val), which enters the top of this chart"] call_input --> input_nc{result is NotCompleted?} input_nc -- yes --> nc_input[return same NotCompleted] input_nc -- no --> type_check has_input -- no --> type_check{val type is valid for self} type_check -- fail --> nc_type[create and return NotCompleted ERROR] type_check -- pass --> main["main(val)"] main -- exception --> nc_main[NotCompleted ERROR] main -- success --> result["return result (which may be NotCompleted FAIL)"] classDef errorNode fill:#fde0c8,stroke:#333 classDef successNode fill:#c8e0fd,stroke:#333 class nc_none,nc_return,nc_input,nc_type,nc_main errorNode class result successNode ``` This is the same sequence for every composed app, regardless of pipeline length. Each app in the chain runs the same `__call__` checks, so `NotCompleted` propagation and exception handling are consistent throughout. See [Runtime type checking](type-system.md#runtime-type-checking) for details on how type validation works and how to disable it. cogent3-scinexus-b16bc06/docs/explanation/index.md000066400000000000000000000014711525761402500222350ustar00rootroot00000000000000# Explanation Background and design rationale behind `scinexus`. - [Why composable apps?](why-composable-apps.md) -- the problem `scinexus` solves and how it compares to alternatives - [The app lifecycle](app-lifecycle.md) -- base classes and `define_app` - [Type system](type-system.md) -- how composition-time type checking works - [Execution flow](flow.md) -- step-by-step data flow through a composed pipeline - [NotCompleted design](not-completed-design.md) -- why a sentinel pattern instead of exceptions - [Source tracking](source-tracking.md) -- how `source_proxy` tracks data provenance through pipelines - [Customisation hooks](customisation-hooks.md) -- `set_summary_display` and `set_id_from_source` registry functions - [Data store model](data-store-model.md) -- unique IDs, checkpointing, and backend choices cogent3-scinexus-b16bc06/docs/explanation/mpi-backend.md000066400000000000000000000115621525761402500233020ustar00rootroot00000000000000# How the MPI backend differs !!! abstract "" Why MPI is not simply a fourth process pool: the worker count is decided outside Python by `mpiexec`, rank 0 is the master and does no work, `max_workers` cannot change any of it, and `UNIVERSE_SIZE` counts slots rather than ranks. The `"multiprocess"`, `"threads"` and `"loky"` backends all assume the running process decides how much parallelism to use by looking at the machine it is on. `max_workers=4` means four workers, and leaving it out means one per CPU. MPI breaks that assumption. The processes were created before Python started, by `mpiexec` or by a cluster scheduler, and nothing inside the program can add to them or give them back. Almost everything else on this page follows from that. ## The launcher decides With a local backend the chain is short. You ask for workers, and the pool starts them: ```python { notest } from scinexus import parallel results = parallel.map(process, items, max_workers=4) # four workers ``` Under MPI that same argument is a request about an allocation that already exists and cannot be changed. The number that decides your parallelism is the one you gave the launcher: ```bash mpiexec -n 8 python -m mpi4py.futures my_script.py ``` So under MPI, `mpiexec -n` is the single place the worker count is in Python set so set `use_mpi=True` but leave `max_workers` out. ## Rank 0 is the master and does no work Every MPI process is a *rank*. Rank 0 is the master: it holds the data, hands out tasks and collects results, and does not process items itself. A job of 8 ranks therefore has **7 workers**, not 8. This has no equivalent in the local backends, where a pool of 4 workers on a 4 core machine puts all 4 to work while the parent simply waits. Setting `max_workers=$PBS_NCPUS` on a cluster is wrong because it asks for one more worker than the job can ever contain. It also means `-n 2` leaves you with a single worker, which is serial execution but using two processes. With `if_serial` defaulting to `"raise"`, `scinexus` will kill a job with one worker You can override this by setting `if_serial="ignore"`. !!! info `-n 3` is the smallest launch with more than one worker. ## Why the ranks must exist before the program starts `scinexus` drives every backend through `concurrent.futures`, and for MPI that means you MUST launch your script using: ```bash mpiexec -n 4 python -m mpi4py.futures my_script.py ``` !!! warning `scinexus` is not written for the any other pattern, such as using `mpi4py` itself to spawn workers on demand while the program runs. ## `max_workers` cannot change the worker count Because the pool is fixed before your code runs, `max_workers` has no way to grow or shrink it. `scinexus` therefore treats the argument as a statement about an allocation rather than a request for one: - **Left out**, you get the pool the job was launched with. This is the right choice almost always. - **Equal to the pool size**, it is accepted quietly, since it agrees with reality. - **Anything else**, you get a warning saying so, in both directions. Asking for fewer used to run on the whole pool in silence, which is the more surprising of the two. - **Below one, or not a whole number**, it is refused, exactly as the local backends refuse it. ## `UNIVERSE_SIZE` counts slots, not ranks MPI exposes an attribute called `UNIVERSE_SIZE`, which is easy to mistake for the number of processes. It is the number of *slots* the job may occupy, which usually means cores on the machine or slots granted by the scheduler. The two numbers are independent, and neither is the worker count. Measured on a six core machine: | launch | `UNIVERSE_SIZE` | ranks | workers | |---|---|---|---| | `mpiexec -n 4 -m mpi4py.futures` | 6 | 4 | 3 | | `mpiexec --oversubscribe -n 8 -m mpi4py.futures` | 6 | 8 | 7 | | `mpiexec -n 1 -m mpi4py.futures` | 6 | 1 | 1 | Running more ranks than there are slots is **oversubscription**, which `mpiexec --oversubscribe` permits deliberately. It is a legitimate thing to do, for testing on a small machine or for work that spends its time waiting rather than computing. The third row is the other end, and it is a launch that will stop rather than run: one worker is serial, so `if_serial` refuses it by default. !!! note `get_size()` reports the last column, the workers you will actually get. It does not report `UNIVERSE_SIZE` and it does not report the rank count. ## What carries over unchanged Two things behave as they do for the process backends rather than being MPI peculiarities. Arguments and results are pickled, so closures and lambdas are refused just as they are under `"multiprocess"`. And the main logic must sit behind an `if __name__ == "__main__":` guard, for the same reason it must under spawn based multiprocessing. For the practical steps -- installing mpi4py, setting the backend, writing and launching the script -- see [Run in parallel](../howto/run-in-parallel.md). cogent3-scinexus-b16bc06/docs/explanation/not-completed-design.md000066400000000000000000000112551525761402500251500ustar00rootroot00000000000000# `NotCompleted` design !!! abstract "" Why `scinexus` uses a sentinel object instead of exceptions for handling failures in batch processing. ## The problem with exceptions in pipelines When applying an algorithm to hundreds or thousands of data records, some records will inevitably fail — bad data, missing fields, violated preconditions. If failures raise exceptions, you face an unpleasant choice: - **Let it crash.** You lose all progress and must restart from scratch. - **Wrap everything in try/except.** Your pipeline logic becomes cluttered with error-handling boilerplate, and you must decide at every step what to catch and what to re-raise. Neither approach scales well. You want failures to be recorded and the pipeline to continue processing the remaining records. ## The sentinel pattern `NotCompleted` is `scinexus`'s answer: a sentinel return value that signals "this record could not be processed" without raising an exception. It carries structured information about the failure: - **`.type`** — `FALSE` (a condition was not met) or `ERROR` (an unexpected exception occurred) - **`.origin`** — which app produced the failure - **`.source`** — which input data failed - **`.message`** — a human-readable explanation Because `NotCompleted` is a regular return value, it flows through the same code paths as successful results. ## Why it subclasses `int` and is falsy `NotCompleted` subclasses `int` with a value of `0`, making it evaluate to `False` in boolean contexts. This means you can check for failure with a simple truthiness test: ```python { notest } result = my_app(data) if not result: print(f"Failed: {result.message}") ``` Subclassing `int` rather than defining `__bool__` alone ensures consistent behaviour with Python's truth-testing protocol across all contexts (including NumPy arrays and other libraries that inspect types). ## Automatic propagation through pipelines When apps are composed with `+`, the resulting pipeline checks each intermediate result. If any step returns a `NotCompleted`, subsequent steps are skipped and the `NotCompleted` is returned as the final result. This means: - A single failure does not corrupt downstream steps. - The failure's `.origin` accurately records where the problem occurred, not where it was finally caught. - No try/except scaffolding is needed in pipeline code. ```python { linenums="1" notest } import cogent3 as c3 aln = c3.get_dataset("primate-brca1") select_seqs = c3.get_app("take_named_seqs", "Mouse", "Human") min_length = c3.get_app("min_length", 300) app = select_seqs + min_length result = app(aln) print(result) # NotCompleted(type=FAIL, origin=take_named_seqs, source="brca1", message="named # seq(s) {'Mouse'} not in ('FlyingLem', 'TreeShrew', 'Galago', 'HowlerMon', # 'Rhesus', 'Orangutan', 'Gorilla', 'Chimpanzee', 'Human')") ``` ## The one case that still raises !!! warning "A writer that cannot name a record raises rather than returning `NotCompleted`" Everything above describes failures of the *data*, and each one stays traceable to the record that caused it — that is what `.source` is for. A result that cannot be named breaks the guarantee rather than exercising it: there is nothing to attribute it to, so returning a `NotCompleted` would put an entry in the audit trail that no one can tie back to an input. `scinexus` raises `ValueError` instead, at the call that made the mistake. This applies to failures too. An unattributable `NotCompleted` reaching a writer raises rather than propagating, for the same reason. It happens only on a one-off call, `app(data)`, where neither the input nor the result carries an identity and no `identifier=` was passed. `apply_to()` always derives one from the data store, so it never hits this. ## Recording failures in data stores When a pipeline is run via `apply_to()` on a data store, `NotCompleted` results are passed to the writer, which is expected to store them in a separate area (the `not_completed/` subdirectory or SQL table). This gives you a complete audit trail: you can inspect which records failed, which app was responsible, and why — all without interrupting the processing of successful records. A writer's `main()` always receives `NotCompleted` values, so it must handle them — see [Handle failures](../howto/handle-failures.md). See [Handle failures](../howto/handle-failures.md) for usage examples. cogent3-scinexus-b16bc06/docs/explanation/source-tracking.md000066400000000000000000000075621525761402500242350ustar00rootroot00000000000000# Source tracking !!! abstract "" How `source_proxy` preserves the link between input identity and output when data is transformed through a pipeline. ## The problem When you call `apply_to()` or `as_completed()` on a data store, each member is fed through the pipeline independently. The pipeline may transform the data into something completely different — a new object with no reference back to the input that produced it. But the writer at the end of the pipeline needs to know *which input* produced *which output* so it can assign the correct unique ID in the output data store. For example, if a loader reads `"gene_001.fa"` and the pipeline returns a translated protein sequence, the writer needs to store that result under the key `"gene_001"`. Without a mechanism to carry the input identity forward, this link is lost. ## How `source_proxy` solves it `source_proxy` is a transparent wrapper that carries two extra pieces of state alongside the wrapped object: - **`.source`** — the original input (or its identifier), preserved across transformations - **`.uuid`** — a unique identifier for this proxy instance, used for hashing When `as_completed()` or `apply_to()` processes a data store, a member that does not have identifying information[^1] is wrapped in a `source_proxy` before entering the pipeline. Because `source_proxy` delegates attribute access to the wrapped object via `__getattr__`, downstream apps see the original object and do not need to know about the proxy. ```python { notest } from scinexus.composable import source_proxy proxy = source_proxy(some_data) proxy.source # the original input proxy.uuid # unique identifier for this proxy proxy.any_attr # delegates to some_data.any_attr ``` ## How `propagate_source` preserves the link `propagate_source` wraps the whole composed pipeline rather than each step. A composed app pulls its input through the chain internally, so this runs once per input record, and when the pipeline returns the result is re-associated with the original source: 1. If the result carries a reference to its own origin[^1], the proxy is **unwrapped** — the result stands on its own. 2. Otherwise[^2] the proxy's wrapped object is **updated** to the new result via `set_obj()`, and the proxy, still carrying the original `.source`, is returned. [^1]: a `.source` attribute for an object, or a `"source"` key, or `info["source"]`, for a `dict` [^2]: For example, a `str` or `bytes` have no `.source` and take the second path. This means the source identity survives an arbitrary number of pipeline steps, even when intermediate apps return entirely new objects. ## Why this matters for writers `WriterApp.apply_to()` uses the source to derive unique IDs for output records. This enables **append-only semantics**: on a subsequent run against the same data store, records that already exist in the output are skipped. The unique ID comes from the original input's identity, extracted by whatever `get_id_from_source()` returns — by default `get_unique_id()`, which strips format suffixes, so an input `gene_001.fa` is stored under `gene_001`. Without source tracking, the writer would have no way to determine whether a result corresponds to an input that has already been processed. ## Failures carry the same identity When an app raises an exception, the framework turns it into a `NotCompleted` recording which input failed. That identity is taken on entry to the app, before the pipeline's intermediate result replaces it, so a failure part way down a chain still names the input rather than whatever value was being processed when it broke. An app may also return a `NotCompleted` itself. If it supplies no source, the proxy's identity is filled in on the way out, so the failure is stored under the same unique ID a successful result would have had. If it supplies one, that is left alone — an app that knows a different file is at fault can say so. cogent3-scinexus-b16bc06/docs/explanation/type-system.md000066400000000000000000000055341525761402500234350ustar00rootroot00000000000000# Type system !!! abstract "" Why types are checked at composition time rather than call time, how `check_type_compatibility` works, handling of Union types and forward references, the role of `register_type_namespace`, and the relationship to `typeguard` for runtime checking. ## Composability rules There are rules around app composition, starting with app types. Loaders and writers are special cases. If included, a loader must always be first, e.g. ```python { notest } app = a_loader + a_generic ``` If included, a writer must always be last, e.g. ```python { notest } app = a_generic + a_writer ``` Changing the order for either of the above will result in a `TypeError`. The next constraint on app composition are the input and output types of the apps involved. Specifically, apps define the type of input they work on and the type of output they produce. For two apps to be composed, the output (or return) type of app on the left (e.g. `a_loader`) must overlap with the input type of the app on the right (e.g. `a_generic`). If they don't match, a `TypeError` is raised. ## Built-in type protocols and aliases SciNexus defines two type-level constructs used across the framework: - `SerialisableType` -- a `Protocol` that any object with a `to_rich_dict()` method satisfies. Database writer apps rely on this to serialise results before storing them in a data store. - `IdentifierType` -- a type alias (`str | Path | DataMemberABC`) representing the accepted ways to identify a member of a data store. Loader apps accept this as input. See the [API reference](../reference/utilities.md#type-system) for details. ## Runtime type checking In addition to checking type compatibility when apps are composed, scinexus validates input data at runtime before each call to `main()`. This uses `typeguard.check_type` to verify that the data matches the app's declared input type. On a mismatch, a `NotCompleted` is returned with a message naming the received and expected types. !!! important "Why this matters" Without runtime type checking, passing the wrong data type to an app still fails — but the error occurs inside the users `main()` and can be confusing. For example, a message like `'NoneType' object has no attribute 'blah'` gives little indication that the real problem is a type mismatch from an upstream app. With runtime checking enabled, scinexus catches this before entering `main()` and reports the mismatch clearly. ### Disabling type checking with `check_data_type` Runtime type checking is enabled by default. For mature pipelines where type correctness has been established, you can disable it to remove the small overhead of the `typeguard` check: ```python { notest } app = read_json() + validate(required_field="name") app.check_data_type = False ``` Setting `check_data_type` on the outermost app propagates the setting to all apps in the pipeline. cogent3-scinexus-b16bc06/docs/explanation/why-composable-apps.md000066400000000000000000000073711525761402500250250ustar00rootroot00000000000000# Why composable apps? !!! abstract "" The design philosophy behind `scinexus` and why composing single-purpose apps leads to more robust, reproducible scientific computation. ## Make your algorithms more robust As the robustness of POSIX based operating systems (think Linux, Mac OS, Unix) can attest, writing algorithms that stitch together multiple single purpose applications is a *Very Good Thing*â„¢. This is most elegantly expressed as a part of the Unix design philosophy. ???+ quote Write programs that do one thing and do it well. Write programs to work together. — Doug McIlroy **`scinexus` encourages this design pattern.** We leverage the Python type annotation system to govern the compatibility (composability) of different applications. This enables in-process composition of your applications with validation of the consistency of the pipeline and the consistency of the data being run through it. We can expand on this slightly for the problem of scientific computation by considering the critical benchmark of satisfying the conditions for reproducible computation, i.e. the obligation to track all of the properties affecting the execution of your algorithm. Examples of this are the operating system, the language version, the seed used for the random number generator, etc. **`scinexus` does this for you.** For example, we intercept all arguments (including default values) passed to the construction of apps and record them so that the app state is logged. If you, the developer, also leverage the capabilities of the [`scitrack`](https://pypi.org/project/scitrack/) logging package (which `scinexus` has as a dependency), you can capture extra information such as versions of packages that your application depends on. We provide an [example](../howto/log-and-cite.md#leveraging-scitrack-for-reproducibility) of using `scitrack` for these cases. ## Improve the accessibility of your work for end users Apps are ready-made functions that users can run on their data without needing technical expertise. They’re easy for non-programmers to use, and can be linked together into pipelines. This lets users process one or millions of records at once—without writing loops, conditionals, or other structural code. ## When to use apps Here are three examples of when to use apps. ### To make a callable class You want a class to be callable, optionally with its input and output types checked at runtime, without writing the boilerplate yourself. ### To make a composite data transform You want to chain several steps into a single transformation and reuse that combination across a code base. For example, converting an object to a JSON string and then compressing it involves two separate functions, plus possible configuration options for compression. By writing each step as an app, you can compose them into a single instance, assign it to a module-level variable. Importing and using that instance wherever it is needed ensures consistency in transformation behaviour. !!! tip "Composite apps can themselves be used as attributes of other apps — apps can have apps as attributes 🤯🚀" ### To make a robust multi-step pipeline You have a composite transformation that you want to apply to many data records. Written conventionally, this would require a loop that calls each step in turn, traps any exceptions, handles their consequences, and passes successful results on to the next step. The `apply_to()` and `as_completed()` methods on composed apps handle this pattern for you. They iterate over the input records, run each one through the pipeline, and collect the results — so you do not need to write the loop, the exception handling, or the conditional checks between steps. And as an extra bonus, you can turn on parallelisation using a single argument. cogent3-scinexus-b16bc06/docs/howto/000077500000000000000000000000001525761402500174175ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/howto/customise-display-and-ids.md000066400000000000000000000146361525761402500247460ustar00rootroot00000000000000# Customise display and IDs !!! abstract "" How to use `set_summary_display` to transform data store summary output into richer objects, and `set_id_from_source` to control how unique identifiers are extracted from data. ## Summary display default By default, summary properties like `.describe` return Python primitive types like `dict` and `list`. ```python { linenums="1" notest } from scinexus import open_data_store dstore = open_data_store("data/raw.zip", suffix="fa", mode="r") print(type(dstore.describe), "", dstore.describe, sep="\n") {'completed': 1035, 'not_completed': 0, 'logs': 0} ``` ## Customising summary display You can register a customised display function for your project. For `cogent3`, it converts them into `cogent3` `Table` objects: ```python { linenums="1" notest } from scinexus.data_store import set_summary_display from cogent3.core.table import Table def summary_to_table(data, *, name): if isinstance(data, dict): title = data.pop("title", name) rows = [[k, v] for k, v in data.items()] return Table(header=["Condition", "Value"], data=rows, title=title) if isinstance(data, list): if not data: return Table(header=[], data=[], title=name) header = list(data[0].keys()) rows = [list(row.values()) for row in data] return Table(header=header, data=rows, title=name) return data set_summary_display(summary_to_table) ``` This results in the following: ```python { linenums="1" notest } describe ====================== Condition Value ---------------------- completed 1035 not_completed 0 logs 0 ---------------------- ``` !!! note `cogent3` registers this transformation automatically when you `import cogent3.app`, so you get `Table` output without any setup in cogent3 projects. ### Unsetting the display function Reset the display function and revert to the default `scinexus` behaviour as follows: ```python { notest } set_summary_display(None) ``` ## Default unique ID extraction Being able to extract unique identifiers for individual data objects is fundamental to the ability of scinexus to track provenance of individual results. Because of its roots from `cogent3`, the `scinexus` default `get_unique_id` function extracts this information from a `.source` attribute. That function, strips format suffixes from file names to derive unique keys for data store records. ```python { linenums="1" notest } from scinexus import get_id_from_source func = get_id_from_source() print(func("gene_001.fasta.gz"), func("sample.txt")) # gene_001 sample ``` ## Customising unique ID extraction Register a custom extractor when your naming convention differs: ```python { linenums="1" notest } from scinexus.data_store import set_id_from_source, get_id_from_source def extract_ensembl_id(data): name = str(data) if name.startswith("ENSG"): return name.split(".")[0] return name set_id_from_source(extract_ensembl_id) func = get_id_from_source() # Now the registered function is used as the default print(func("ENSG00000157184.fa"), func("gene_001.fasta.gz")) # ENSG00000157184 gene_001.fasta.gz ``` ## Reset to default ```python { linenums="1" notest } from scinexus.data_store import set_id_from_source set_id_from_source(None) ``` ## Over-riding the default per-call You can also override per-call without affecting the global default: ```python { notest } result = app.apply_to(dstore, id_from_source=extract_ensembl_id) ``` cogent3-scinexus-b16bc06/docs/howto/extend-type-checking.md000066400000000000000000000106751525761402500237710ustar00rootroot00000000000000# Extend type checking !!! abstract "" How to use `register_type_namespace` to make forward references from downstream packages resolvable at composition time, enabling third-party types in app pipelines. ## The problem When you compose apps with `+`, `scinexus` checks that the output type of the left app is compatible with the input type of the right app. Type hints are often written as forward references — strings like `"Alignment"` or `"PhyloNode"` — to avoid circular imports. At composition time `scinexus` must resolve these strings to actual classes, but it only knows about its own types by default. If your package defines custom types used in app hints, `scinexus` cannot resolve them without help. ## The solution `register_type_namespace` lets a downstream package register a **lazy namespace provider** — a zero-argument callable that returns a `dict[str, type]`. When `scinexus` encounters an unresolved forward reference, it queries each registered provider in order until it finds a match. ```python { notest } from scinexus.typing import register_type_namespace register_type_namespace(my_provider) ``` The provider is called lazily each time a name needs resolving, so the package can defer heavy imports. Providers are responsible for their own caching. Registration is idempotent: re-registering the same callable is a no-op. ???- example "How `cogent3` does it" ```python { linenums="1" notest } from scinexus.typing import register_type_namespace _resolution_ns = None def _get_resolution_namespace(): global _resolution_ns if _resolution_ns is not None: return _resolution_ns from cogent3.core.alignment import Alignment, SequenceCollection # (1)! from cogent3.core.tree import PhyloNode # ... other imports ... _resolution_ns = { "Alignment": Alignment, "SequenceCollection": SequenceCollection, "PhyloNode": PhyloNode, # (3)! # ... other types ... } return _resolution_ns register_type_namespace(_get_resolution_namespace) # (2)! ``` 1. `cogent3` defines many types (`Alignment`, `PhyloNode`, `Table`, etc.) that are used as forward references in app type hints. 2. In `cogent3/app/typing.py`, a resolution namespace is built lazily and registered with `scinexus` 3. With this registration, any `scinexus` app that uses `"PhyloNode"` as a type hint will resolve correctly at composition time without the user importing `PhyloNode` explicitly. ## Registering your own package's types Follow the same pattern: define a lazy provider function that imports and caches your types, then register it at module level. ```python { notest } from scinexus.typing import register_type_namespace _ns = None def _get_my_types(): global _ns if _ns is not None: return _ns from my_package.core import MyDataType, MyResultType _ns = { "MyDataType": MyDataType, "MyResultType": MyResultType, } return _ns register_type_namespace(_get_my_types) ``` Place this in a module that is imported early (e.g. your package's `typing.py` or `__init__.py`). Once registered, apps using `"MyDataType"` as a forward reference will resolve correctly when composed with other apps. cogent3-scinexus-b16bc06/docs/howto/handle-failures.md000066400000000000000000000165051525761402500230130ustar00rootroot00000000000000# Handle failures !!! abstract "" How to create `NotCompleted` values, check their truthiness, inspect their attributes, and control propagation with `skip_not_completed=False`. ## `NotCompleted` FALSE type A FALSE type is returned when a condition is not met. For example, below we create an app that selects 2 specific sequences from an alignment. Applying this to a data set where a "Mouse" sequence does not exist produces a FALSE type. ```python { linenums="1" notest } import cogent3 as c3 aln = c3.get_dataset("primate-brca1") select_seqs = c3.get_app("take_named_seqs", "Mouse", "Human") result = select_seqs(aln) assert result == False print(result) # NotCompleted(type=FAIL, origin=take_named_seqs, source="brca1", message="named # seq(s) {'Mouse'} not in ('FlyingLem', 'TreeShrew', 'Galago', 'HowlerMon', # 'Rhesus', 'Orangutan', 'Gorilla', 'Chimpanzee', 'Human')") ``` ## Inspecting `NotCompleted` attributes The `NotCompleted` instance has attributes identifying what data failed: ```python { notest } result.source ``` Where the failure occurred: ```python { notest } result.origin ``` And the reason for the failure: ```python { notest } result.message ``` The `.type` attribute is the `NotCompletedType` enum value (e.g. `NotCompletedType.FALSE`, `NotCompletedType.ERROR`, or `NotCompletedType.BUG`). ## `NotCompleted` ERROR type An ERROR type is returned if an unexpected condition occurs, such as an exception raised during execution. Here we illustrate this by trying to open a file with an incorrect path. ???- example "Example" ```python { linenums="1" notest } import cogent3 as c3 reader = c3.get_app("load_aligned", moltype="dna") result = reader("primate_brca1.fasta") print(result) # NotCompleted(type=ERROR, origin=load_aligned, source="primate_brca1", # message="Traceback (most recent call last): File # "/Users/gavin/repos/SciNexus/src/scinexus/composable.py", line 545, in __call__ # result = self.main(val, *args, **kwargs) [...] ``` ## Composed functions propagate `NotCompleted` results If you have a composed function with multiple steps and a failure occurs, the resulting `NotCompleted` is returned without any of the subsequent steps being applied. For example, we make a composed app from both of the above apps: ???- example "Example" ```python { linenums="1" notest } import cogent3 as c3 reader = c3.get_app("load_aligned", moltype="dna") select_seqs = c3.get_app("take_named_seqs", "Mouse", "Human") app = reader + select_seqs result = app("data/primate_brca1.fasta") print(result) # NotCompleted(type=FAIL, origin=take_named_seqs, source="primate_brca1", # message="named seq(s) {'Mouse'} not in ('FlyingLem', 'TreeShrew', 'Galago', # 'HowlerMon', 'Rhesus', 'Orangutan', 'Gorilla', 'Chimpanzee', 'Human')") ``` The failure originated in `select_seqs` (an instance of `take_named_seqs`), and `reader` ran successfully — but the `NotCompleted` propagated through the rest of the pipeline. ???- example "Example" ```python { linenums="1" notest } import cogent3 as c3 reader = c3.get_app("load_aligned", moltype="dna") select_seqs = c3.get_app("take_named_seqs", "Mouse", "Human") app = reader + select_seqs result = app("primate_brca1.fasta") print(result) # NotCompleted(type=ERROR, origin=load_aligned, source="primate_brca1", [...] ``` Here the failure originated in `reader` (bad path), and `select_seqs` was never called. ## Creating `NotCompleted` in your own apps You can return a `NotCompleted` from your own app to signal that a particular input cannot be processed: ```python { linenums="1" notest } from scinexus import define_app, NotCompleted, NotCompletedType @define_app def require_min_length(val: str, min_length: int = 10) -> str: if len(val) < min_length: return NotCompleted( NotCompletedType.FALSE, "require_min_length", val, message=f"too short: {len(val)} < {min_length}", ) return val ``` ## Receiving `NotCompleted` with `skip_not_completed=False` By default, apps skip `NotCompleted` inputs — they propagate without calling `main()`. If your app needs to see `NotCompleted` values, set `skip_not_completed=False`: ```python { linenums="1" notest } from scinexus import define_app, NotCompleted @define_app(skip_not_completed=False) def log_failures(val: str) -> str: if isinstance(val, NotCompleted): print(f"Failure: {val.message}") return val return val ``` !!! note "Writer apps get this for free" A writer never skips `NotCompleted`, whatever you pass for `skip_not_completed`. `apply_to()` invokes a writer's `main()` directly rather than through the machinery that does the skipping, so the flag could not be honoured there. It is forced off for writers so that a one-off `app(data)` call behaves the same way. The practical consequence is that a writer's `main()` must handle `NotCompleted` itself, typically by sending it to `write_not_completed()`. Its first parameter annotation does not constrain what arrives. cogent3-scinexus-b16bc06/docs/howto/index.md000066400000000000000000000022261525761402500210520ustar00rootroot00000000000000# How-to guides Task-oriented recipes for common `scinexus` operations, each self-contained. - [Write a function app](write-a-function-app.md) -- use `@define_app` on a function - [Write a class app](write-a-class-app.md) -- use `@define_app` on a class with `main()` - [Handle failures](handle-failures.md) -- create and inspect `NotCompleted` values - [Use data stores](use-data-stores.md) -- open, read, and write data stores - [Read and write files](read-and-write-files.md) -- IO with compression, atomic writes, streaming - [Run in parallel](run-in-parallel.md) -- parallel execution with pluggable backends - [Track progress](track-progress.md) -- progress bars with `tqdm` or `rich` - [Log and cite](log-and-cite.md) -- `scitrack` logging and citation tracking - [Customise display and IDs](customise-display-and-ids.md) -- `set_summary_display` and `set_id_from_source` - [Extend type checking](extend-type-checking.md) -- register type namespaces for downstream packages - [Use with a coding agent](use-with-coding-agents.md) -- install the `scinexus` agent skill - [Migrate from cogent3](migrate-from-cogent3.md) -- update code from `cogent3.app` to `scinexus` cogent3-scinexus-b16bc06/docs/howto/log-and-cite.md000066400000000000000000000177441525761402500222210ustar00rootroot00000000000000# Log and cite !!! abstract "" How to use `scitrack` logging in apps, control logging in `apply_to`, and access citation records from composed pipelines. ## Leveraging `scitrack` for reproducibility We reproduce here one of the examples from [scitrack](https://github.com/HuttleyLab/scitrack). ???- example "Using `scitrack` in a `click` app" ```python linenums="1" import click from scitrack import CachingLogger LOGGER = CachingLogger() @click.command() @click.option("-i", "--infile", type=click.Path(exists=True)) @click.option("-t", "--test", is_flag=True, help="Run test.") def main(infile, test): # capture the local variables, at this point just provided arguments LOGGER.log_args() # (1)! LOGGER.log_versions("numpy") # (2)! LOGGER.input_file(infile) # (3)! LOGGER.log_file_path = "some_path.log" # (4)! if __name__ == "__main__": main() ``` 1. :man_raising_hand: A single statement and you have captured all the input arguments and their values, including defaults! 2. This captures the version numbers of the packages our application depends on. 3. This logs the path to `infile` and its md5sum. 4. Until you assign the path where you want the file written, this content has been cached. ## Controlling logging in `apply_to` By default, `apply_to` creates a `CachingLogger` that records the composable function, package versions, output paths, MD5 checksums of every result, and total elapsed time. The log is then written into the output data store. This is the recommended setting for production analyses because it gives you a complete, self-contained record of what ran and what it produced. ```python { notest } result = process.apply_to(dstore) # logger=True by default ``` You can also pass your own `CachingLogger` instance if you want to configure it beforehand or reuse one across multiple calls. ```python { notest } from scitrack import CachingLogger LOGGER = CachingLogger() LOGGER.log_args() result = process.apply_to(dstore, logger=LOGGER) ``` ### Disabling logging Set `logger=False` to skip logging entirely. ```python { notest } result = process.apply_to(dstore, logger=False) ``` This is useful when: - **Your project is small** and a full provenance log is unnecessary. - **Logging is handled externally**, for example by a workflow manager or your own `CachingLogger` that wraps several `apply_to` calls. - **You want to avoid the overhead** of computing an MD5 checksum for every result object, which can be noticeable for large or numerous outputs. ## Make it easy for your work to be cited Correctly attributing the authors of algorithms and software is a requirement of good scientific practice. `scinexus` makes this easy by letting app authors declare citations that are automatically tracked through composed pipelines. Use the `cite` parameter of `define_app` (or the base classes) to attach a citation. The `citeable` library provides several classes for this purpose. ???- example "Adding a citation to your app" ```python { linenums="1" notest } from citeable import Software from scinexus import define_app from cogent3.app.typing import AlignedSeqsType from cogent3 import get_app my_cite = Software( author=["Doe, J", "Smith, A"], title="My Sequence Filter", year=2025, url="https://example.com/my-filter", version="0.1.0", ) @define_app(cite=my_cite) # (1)! def strict_filter(val: AlignedSeqsType) -> AlignedSeqsType: """Remove sequences shorter than the alignment.""" return val.omit_bad_seqs() app = strict_filter() loader = get_app("load_aligned", moltype="dna", format_name="fasta") pipeline = loader + strict_filter() print(pipeline.citations) # (2)! print(f"\n{pipeline.bib}") # (3)! # (Software( author=['Doe, J', 'Smith, A'], title='My Sequence Filter', # year=2025, version='0.1.0', url='https://example.com/my-filter', [...] ``` 1. Use the `cite` parameter of `define_app` to attach a citation 2. The `.citations` property returns citations as a tuple. When apps are composed into a pipeline, `.citations` collects unique citations from all apps in the chain. 3. The `.bib` gives the BibTeX string. ## Extracting citations from a data store When a composed pipeline is run via `apply_to()`, citations are automatically saved in the output data store. ???- example "Citations in data stores" ```python { linenums="1" notest } from citeable import Software from scinexus import define_app, open_data_store from cogent3.app.typing import AlignedSeqsType from cogent3 import get_app my_cite = Software( author=["Doe, J"], title="My Sequence Filter", year=2025, ) @define_app(cite=my_cite) def strict_filter(val: AlignedSeqsType) -> AlignedSeqsType: return val.omit_bad_seqs() in_dstore = open_data_store("data/raw.zip", suffix="fa", limit=5) out_dstore = open_data_store("cited_results", suffix="fa", mode="w") loader = get_app("load_aligned", moltype="dna", format_name="fasta") writer = get_app("write_seqs", data_store=out_dstore, format_name="fasta") process = loader + strict_filter() + writer result = process.apply_to(in_dstore) result.summary_citations # (1)! result.write_bib("my_analysis.bib") # (2)! ``` 1. Because we are using `cogent3`, the property returns a `cogent3` `Table` of all citations stored in the data store. 2. You can export to a BibTeX file. !!! note `ReadOnlyDataStoreZipped` supports reading stored citations but not writing them. cogent3-scinexus-b16bc06/docs/howto/migrate-from-cogent3.md000066400000000000000000000056361525761402500237040ustar00rootroot00000000000000# Migrate from cogent3 !!! abstract "" A guide for users moving from `cogent3.app` to `scinexus`. ## Update imports Replace `cogent3.app` imports with their `scinexus` equivalents: ```python { notest } # before from cogent3.app.composable import define_app, NotCompleted # after from scinexus import define_app, NotCompleted ``` ## Summary methods return plain Python objects Data store summary methods (`describe`, `summary_logs`, `summary_not_completed`, `summary_citations`, `validate`) now return `list[dict]` or `dict` instead of cogent3 `Table` objects. To restore `Table`-based display, register a converter with `set_summary_display()`: ```python { notest } from cogent3.core.table import Table from scinexus.data_store import set_summary_display def _summary_to_table(data, *, name): if isinstance(data, dict): rows = [[k, v] for k, v in data.items()] return Table(header=["Condition", "Value"], data=rows, title=name) if isinstance(data, list) and data: header = list(data[0].keys()) rows = [list(row.values()) for row in data] return Table(header=header, data=rows, title=name) return data set_summary_display(_summary_to_table) ``` ## NotCompletedType is now an enum Failure types are categorised using the `NotCompletedType` enum rather than bare strings: ```python { notest } from scinexus.composable import NotCompletedType # before nc.type == "ERROR" # after nc.type == NotCompletedType.ERROR ``` The three values are `ERROR`, `FAIL`, and `BUG`. ## App composition uses shallow copies Composing apps with `+` now creates a shallow copy of the right-hand operand. Composed pipelines no longer share mutable state, so `ComposableApp.disconnect()` is no longer needed and is deprecated. ## New features These capabilities are new in `scinexus` and were not available in `cogent3.app`: - **`check_data_type` property** -- toggle runtime input type checking on or off. See [Runtime type checking](../explanation/type-system.md#runtime-type-checking). - **Better IDE integration through static typing support** -- `AppBase[T, R]`, `ComposableApp[T, R]`, and `WriterApp[T, R]` can be inherited from directly as an alternative to `@define_app`. - **Pluggable parallel backends** -- choose between stdlib multiprocessing, stdlib threads, loky, or MPI backends. See [Run in parallel](run-in-parallel.md). - **Pluggable progress bars** -- use `tqdm` or `rich` for progress display. See [Track progress](track-progress.md). - **Custom identifier extraction** -- register a custom function for extracting storage identifiers from data via `set_id_from_source()`. See [Customise display and IDs](customise-display-and-ids.md). - **Logging can be disabled** -- pass `logger=False` to `apply_to()` to skip log file creation. ## Full changelog See the [changelog](https://github.com/cogent3/scinexus/blob/main/changelog.md) for a complete list of changes from the cogent3 app infrastructure. cogent3-scinexus-b16bc06/docs/howto/read-and-write-files.md000066400000000000000000000135501525761402500236500ustar00rootroot00000000000000# Read and write files !!! abstract "" How to use `open_()` for reading and writing files with automatic compression detection (gzip, bzip2, lzma, zip), `atomic_write` for safe file writes that clean up on failure, `iter_splitlines` and `iter_line_blocks` for streaming large files, and `is_url`/`open_url` for working with URLs. ## Writing a compressed file `open_()` detects the compression format from the file suffix and handles it automatically. Writing a gzip-compressed text file is identical to writing a plain text file — just use a `.gz` suffix. ```python { linenums="1" notest } from scinexus import open_ with open_("data/sample.txt.gz", "wt") as f: f.write("Hello, compressed world!\n") f.write("Line two of the file.\n") ``` ## Reading a compressed file Reading works the same way — `open_()` detects the `.gz` suffix and decompresses transparently. ```python { linenums="1" notest } from scinexus import open_ with open_("data/sample.txt.gz") as f: print(f.read()) # Hello, compressed world! Line two of the file. ``` Supported compression formats are gzip (`.gz`), bzip2 (`.bz2`), lzma (`.xz`, `.lzma`), and zip (`.zip`). ## Reading a URL `open_()` also handles URLs. Use `is_url()` to check whether a path is a URL before opening it. ???- example "Checking and reading a URL" ```python linenums="1" from scinexus.io_util import is_url from scinexus import open_ url = "https://github.com/user-attachments/files/26728407/raw.zip" print(is_url(url)) # (1)! with open_(url, "rb") as f: # (2)! header = f.read(20) print(header) ``` 1. `is_url()` returns `True` for `http`, `https`, and `file` scheme URLs. 2. `open_()` detects the URL and delegates to `open_url()`. Only read mode is supported for URLs. ## Efficiently reading large files Reading an entire large file into memory or iterating line by line with Python's built-in `readline()` can be inefficient. The built-in approach makes a system call for every line, which becomes a bottleneck for files with millions of lines. `scinexus` provides two functions that read data in large chunks and then split into lines, greatly reducing I/O overhead. ### `iter_splitlines` `iter_splitlines(path, chunk_size=1_000_000, *, as_bytes=False)` reads a file in chunks (default 1 MB) and yields individual lines. It correctly handles lines that span chunk boundaries. An empty last line is never yielded, so a file ending on a newline gives the same lines as one that does not. ```python { notest } from scinexus.io_util import iter_splitlines for line in iter_splitlines("large_file.txt"): process(line) ``` Pass `as_bytes=True` to open the file in binary mode and get `bytes` lines, skipping the decoding step. The argument is keyword-only. ```python { notest } from scinexus.io_util import iter_splitlines for line in iter_splitlines("large_file.txt", as_bytes=True): process(line) # line is bytes, e.g. b"first line" ``` The two modes do not always split a file into the same number of lines. In text mode the file is read with universal newlines, so `\r`, `\n` and `\r\n` all become line breaks, and `str.splitlines()` breaks on a further eight characters including vertical tab and form feed. In binary mode there is no translation and `bytes.splitlines()` breaks only on `\r`, `\n` and `\r\n`: ```python { notest } from pathlib import Path from scinexus.io_util import iter_splitlines Path("pages.txt").write_bytes(b"alpha\x0cbeta\n") list(iter_splitlines("pages.txt")) # ['alpha', 'beta'] list(iter_splitlines("pages.txt", as_bytes=True)) # [b'alpha\x0cbeta'] ``` For compressed files `as_bytes=True` gives the decompressed bytes, not the raw compressed ones. ### `iter_line_blocks` `iter_line_blocks(path, num_lines=1000, chunk_size=5_000_000, *, as_bytes=False)` builds on `iter_splitlines` — it accumulates lines into lists of `num_lines` and yields each list. This is useful when downstream processing works on batches (e.g. FASTA records where each record spans a fixed number of lines). ```python { notest } from scinexus.io_util import iter_line_blocks for block in iter_line_blocks("large_file.txt", num_lines=1000): process_batch(block) # block is a list of up to 1000 strings ``` `iter_line_blocks` passes `as_bytes` straight through, so each block is a list of `bytes` and everything said above about the two modes applies here too. `num_lines` is unaffected, blocks are still at most `num_lines` long. ```python { notest } for block in iter_line_blocks("large_file.txt", num_lines=1000, as_bytes=True): process_batch(block) # block is a list of up to 1000 bytes objects ``` Use `iter_splitlines` when you need one line at a time. Use `iter_line_blocks` when your processing naturally operates on batches of lines. cogent3-scinexus-b16bc06/docs/howto/run-in-parallel.md000066400000000000000000000242551525761402500227530ustar00rootroot00000000000000# Run in parallel !!! abstract "" How to choose your preferred parallelisation backend, how to parallelise any function using `parallel.map`, `parallel.imap`, and `parallel.as_completed` as standalone utilities, and how to enable parallel execution in app pipelines with `parallel=True` and `par_kw`. ## Data level parallelism `scinexus` supports parallel computation for the common case where the same calculation needs to be applied to many independent data items. The master splits the work among available CPU cores, each worker processes its share, and results are collected. Whether a worker is a process or a thread depends on the backend, and on a free-threaded build the default is threads. !!! warning Parallelism is not always faster. You should see a performance gain when the computation time per task significantly exceeds the overhead of distributing work. If individual tasks are very fast, the overhead of distributing them can dominate — for the process backends that overhead includes pickling every argument and result and sending it between processes. If individual output files are small, storing results in a single file (e.g. a `.sqlitedb` database) is more efficient than writing many small files. ## Choosing a parallel backend `scinexus` supports four parallel backends. Two of them use only the Python standard library and require no extra installs. | Backend | Install | Best for | |---|---|---| | `"multiprocess"` | included | scripts, CI, environments where you control dependencies | | `"threads"` | included | free-threaded (no-GIL) builds, where it is the default | | `"loky"` | `pip install "scinexus[loky]"` | Jupyter notebooks, interactive sessions, long-running pools | | `"mpi"` | `pip install "scinexus[mpi]"` | HPC clusters with multiple nodes | Set the backend once, typically at the top of your script or notebook: ```python { notest } import scinexus scinexus.set_parallel_backend("loky") ``` !!! note The `"loky"` backend uses [loky](https://loky.readthedocs.io/) which provides reusable process pools and robust pickling via `cloudpickle`. This makes it the recommended choice for Jupyter notebooks, where the stdlib `ProcessPoolExecutor` can fail to serialise closures and lambda functions. ### If you do not choose, one is chosen for you With no call to `set_parallel_backend`, the backend is `"threads"` when the GIL is not in force **and** the caller is not itself a pool worker, and `"multiprocess"` in every other case. On a standard CPython build you therefore get processes, and on a free-threaded (no-GIL) build you get threads. Two things besides the build can put the GIL back in force: setting `PYTHON_GIL=1`, and importing an extension module that does not declare free-threading support. The second can happen partway through a run, so the choice is revisited on every call to `imap`, `map` or `as_completed`. Specifying the backend using `set_parallel_backend` is how you opt out of threads on a free-threaded build. ```python { notest } import scinexus scinexus.set_parallel_backend("multiprocess") # processes, whatever the build ``` !!! note "What pinning does not cover" The setting applies to primary process alone, so a worker does not inherit it and chooses again for itself. Passing `use_mpi=True` to `imap`, `map` or `as_completed` uses MPI for that call whatever you pinned. And `get_rank()`, `get_size()` and `is_master_process()` report on the context actually running the current thread rather than on the default you set. ### What crosses to a worker The process backends — `"multiprocess"`, `"loky"` and `"mpi"` — pickle the function and its arguments and send them to another process. Each worker gets its own copy of your app, so mutating `self` inside `main()` does not propagate, and closures and lambdas are refused[^1]. [^1]: `"loky"` is the most forgiving, since it pickles via `cloudpickle` The `"threads"` backend sends nothing. Workers share the app instance and everything reachable from it, so closures and lambdas are accepted, but a `main()` that mutates `self` or writes module-level state — the `numpy.random` global generator, say — is a data race here where it was harmless under the process backends. ### Getting a specific backend without changing the default If your code requires a particular backend, pass the ``backend`` argument to ``get_parallel_backend``. This returns an instance of the requested backend without changing the global default, so other packages that depend on the current setting are unaffected: ```python { notest } from scinexus import get_parallel_backend backend = get_parallel_backend(backend="loky") ``` ## Parallel computation on a single computer ### Using `app.apply_to()` If you have a composed app **with** a writer, use `apply_to()` with the `parallel` and `par_kw` keyword arguments: ```python { notest } result = app.apply_to(dstore, parallel=True, par_kw=dict(max_workers=4)) ``` ### Using `app.as_completed()` If you have a composed app **without** a writer, use `as_completed()`. This returns a generator, so wrap it with `list()` or iterate over it: ```python { notest } results = list(app.as_completed(dstore, parallel=True, par_kw=dict(max_workers=4))) ``` ### Using `scinexus.parallel` directly For parallelising any function (not just apps), use the functions in `scinexus.parallel`. #### `parallel.as_completed` -- results in completion order Returns results as they finish. The order may differ from the input order. It also tends to balance work better across compute nodes than `imap` or `map`. ```python { notest } from scinexus import parallel result = list(parallel.as_completed(is_prime, PRIMES, max_workers=4)) ``` The first argument is the function to call, the second is the iterable of inputs. Each input element is passed as a single argument to the function, and `as_completed` submits one task per item on every backend. If you want the work batched, use `imap` on a process or MPI backend, which chunks by `chunksize`. !!! note If you don't specify `max_workers`, all available CPUs are used. Under MPI it instead means the workers the job was launched with, which is not something `max_workers` can change. `"threads"` `chunksize` but ignores it. #### `parallel.imap` -- preserving input order (generator) Returns results in the same order as the input, yielding one at a time: ```python { notest } from scinexus import parallel for result in parallel.imap(process_item, items, max_workers=4): handle(result) ``` #### `parallel.map` -- preserving input order (list) Same as `imap` but returns a list: ```python { notest } from scinexus import parallel results = parallel.map(process_item, items, max_workers=4) ``` ### Complete example ```python { notest } import math from scinexus import parallel def is_prime(n): if n % 2 == 0: return False sqrt_n = int(math.floor(math.sqrt(n))) for i in range(3, sqrt_n + 1, 2): if n % i == 0: return False return True PRIMES = [ 112272535095293, 112582705942171, 115280095190773, 115797848077099, 117450548693743, 993960000099397, ] if __name__ == "__main__": results = parallel.map(is_prime, PRIMES, max_workers=4) for number, prime in zip(PRIMES, results): print(f"{number} is prime: {prime}") ``` ## Parallel computation on multiple computers (MPI) On systems with multiple nodes (e.g. an HPC cluster), use MPI via the [mpi4py](https://mpi4py.readthedocs.io/) library. You need to install an MPI implementation (e.g. [OpenMPI](https://www.open-mpi.org/)) and the `mpi4py` Python package ```bash pip install mpi4py ``` or installing `scinexus` [with `mpi` extra](../install.md#optional-extras). Set the backend to MPI: ```python { notest } import scinexus scinexus.set_parallel_backend("mpi") ``` Or pass `use_mpi=True` to any of the parallel functions: ```python { notest } from scinexus import parallel results = parallel.map(is_prime, PRIMES, use_mpi=True) ``` Or with app pipelines: ```python { notest } result = app.apply_to(dstore, parallel=True, par_kw=dict(use_mpi=True)) ``` To run an MPI script, invoke it via `mpiexec`: ```bash mpiexec -n $PBS_NCPUS python3 -m mpi4py.futures my_script.py ``` !!! warning "Do not set `max_workers` under MPI" Note that `max_workers` is absent from the calls above. The worker count is fixed by `mpiexec -n` before your program starts, so `max_workers` cannot change it and passing one that disagrees only earns a warning. Rank 0 is the master and does no work, so `-n $PBS_NCPUS` gives you `$PBS_NCPUS - 1` workers. See [How the MPI backend differs](../explanation/mpi-backend.md). !!! note You can use MPI for parallel execution on a single computer too. This can be useful for testing your code locally before migrating to a larger system. ### MPI script structure MPI scripts must guard the main logic behind `if __name__ == "__main__":`: ```python { notest } from scinexus import parallel def process(data): ... if __name__ == "__main__": results = parallel.map(process, my_data, use_mpi=True) ``` ## Custom backends You can integrate any parallel engine by subclassing `Parallel`: ```python { notest } from scinexus.parallel import Parallel, set_parallel_backend class DaskBackend(Parallel): def __init__(self, client): self._client = client def imap(self, f, s, max_workers=None, **kwargs): futures = self._client.map(f, list(s)) yield from self._client.gather(futures) def as_completed(self, f, s, max_workers=None, **kwargs): from dask.distributed import as_completed futures = self._client.map(f, list(s)) for future in as_completed(futures): yield future.result() def is_master_process(self): from dask.distributed import get_worker try: get_worker() return False except ValueError: return True def get_rank(self): return 0 def get_size(self): return sum(self._client.nthreads().values()) set_parallel_backend(DaskBackend(client)) ``` !!! warning "The above is a hypothetical wrapping of dask!" The example for adding [dask](https://www.dask.org) as a parallelisation backend is not tested! Also note that this would require installing `"dask[distributed]"`. cogent3-scinexus-b16bc06/docs/howto/track-progress.md000066400000000000000000000144621525761402500227160ustar00rootroot00000000000000# Track progress !!! abstract "" How to choose your preferred progress bar backend and customise progress bars. `scinexus` defaults to using the [tqdm](https://pypi.org/project/tqdm/) for progress bars. These behave well across terminal and notebook environments. We also support using [rich](https://pypi.org/project/rich/) for its progress bars. A single API for different progress backends. ## Choosing the progress bar backend Use `set_progress_backend` to switch between backends. The default is `tqdm`. ```python { notest } import scinexus scinexus.set_progress_backend("rich") # switch to rich scinexus.set_progress_backend("tqdm") # switch back to tqdm scinexus.set_progress_backend(None) # reset to default (tqdm) ``` ## Getting a progress bar Use `get_progress` to obtain a `Progress` instance. Passing `show_progress=True` returns the current default backend. ```python { notest } import scinexus pbar = scinexus.get_progress(show_progress=True) for item in pbar(range(100), msg="Processing"): pass # your work here ``` You can pass keyword arguments to configure the default backend: ```python { notest } import scinexus pbar = scinexus.get_progress(show_progress=True, colour="blue", leave=True) ``` You can also pass a `Progress` instance directly: ```python { notest } from scinexus.progress import RichProgress pbar = scinexus.get_progress(show_progress=RichProgress()) ``` !!! note If you call `get_progress(show_progress=False)`, it returns `NoProgress`, which silently passes through the iterable. ## Nesting progress bars === "Using `tqdm` (default)" Create nested progress bars using `child()`. Each bar can have its own description via the `msg` keyword. Create the child once before the loop — it automatically resets to zero on each subsequent call. ```python { notest } import scinexus pbar = scinexus.get_progress(show_progress=True) child = pbar.child() for batch in pbar(range(3), msg="Outer loop"): for item in child(range(10), msg=f"Inner batch {batch}"): pass # your work here ``` === "Using `rich`" The same nesting pattern works with the `rich` backend: ```python { notest } import scinexus scinexus.set_progress_backend("rich") pbar = scinexus.get_progress(show_progress=True) child = pbar.child() for batch in pbar(range(3), msg="Outer loop"): for item in child(range(10), msg=f"Inner batch {batch}"): pass # your work here ``` `rich` children share the same `rich.progress.Progress` display instance, so all bars render together in a single live display. The outer bar tracks the top-level iteration. Each call to `child()` creates a new `Progress` at the next cursor position, so inner bars appear below the outer one. The child bar is reused across iterations — on the second and subsequent calls, the bar resets to zero instead of creating a new one. #### Push-based sub-contexts When you need to report fractional progress rather than iterating, use `context()`: ```python { notest } import scinexus pbar = scinexus.get_progress(show_progress=True) child = pbar.child() for batch in pbar(range(3), msg="Processing"): with child.context(msg=f"Batch {batch}") as ctx: for i in range(100): ctx.update(progress=i / 100, msg=f"Step {i}") ``` The context maps progress values from `[0.0, 1.0]` to the configured `[start, end]` range and is cleaned up automatically when the `with` block exits. ## Cleaning up Both `Progress` and `ProgressContext` support the context manager protocol. Using a progress bar as a context manager ensures that `close()` is called automatically, which finalises the display and moves the cursor past the bars. Without cleanup, leftover bars can leave the terminal cursor in the wrong position. === "Using `tqdm` (default)" ```python import scinexus with scinexus.get_progress(show_progress=True) as pbar: # (1)! child = pbar.child() for batch in pbar(range(3), msg="Outer"): for item in child(range(10), msg=f"Batch {batch}"): pass ``` 1. `close()` is called automatically and the cursor position is restored. === "Using `rich`" ```python import scinexus scinexus.set_progress_backend("rich") with scinexus.get_progress(show_progress=True) as pbar: # (1)! child = pbar.child() for batch in pbar(range(3), msg="Outer"): for item in child(range(10), msg=f"Batch {batch}"): pass ``` 1. `close()` is called automatically and the cursor position is restored. ???- tip "No context manager? No problem!" ```python import scinexus pbar = scinexus.get_progress(show_progress=True) child = pbar.child() for batch in pbar(range(3), msg="Outer"): for item in child(range(10), msg=f"Batch {batch}"): pass pbar.close() # (1)! ``` 1. Call `close()` explicitly when you are done !!! note Calling `close()` on a `Progress` instance also closes all of its children. For standalone `ProgressContext` objects (from `context()`), use the `with` statement as shown in the [push-based sub-contexts](#push-based-sub-contexts) section. ## Customising appearance ### Persisting bars after completion By default, `tqdm` keeps the outermost bar visible after completion but clears nested bars. `rich` removes all bars. Use `leave` to control this: ```python { notest } from scinexus.progress import TqdmProgress, RichProgress # Keep all tqdm bars visible after completion pbar = TqdmProgress(leave=True) # Keep all rich bars visible after completion pbar = RichProgress(leave=True) ``` You can also set `leave` independently on child bars: ```python { notest } from scinexus.progress import TqdmProgress pbar = TqdmProgress(leave=True) child = pbar.child(leave=False) # child bars disappear, outer persists for batch in pbar(range(3), msg="Outer"): for item in child(range(10), msg=f"Batch {batch}"): pass ``` ### Setting bar colour Both backends support a `colour` parameter. For `tqdm`, this sets the bar colour directly. For `rich`, it styles the bar column when the display is auto-created. ```python { notest } from scinexus.progress import TqdmProgress, RichProgress pbar = TqdmProgress(colour="green") pbar = RichProgress(colour="cyan") ``` Colour is inherited by child bars. cogent3-scinexus-b16bc06/docs/howto/use-data-stores.md000066400000000000000000000301041525761402500227570ustar00rootroot00000000000000# Use data stores !!! abstract "" How to use `open_data_store` in read, write, and append modes with directory, zip, and SQLite backends, iterate over members, and inspect `.completed`, `.not_completed`, and `.summary_`. ## How do I use a data store? A data store is just a "container". To open a data store you use the `open_data_store()` function. To load the data for a member of a data store you need an appropriately selected loader type of app. ## Supported operations on a data store All data store classes can be iterated over, indexed, checked for membership. These operations return a `DataMember` object. In addition to providing access to members, the data store classes have convenience methods for describing their contents and providing summaries of log files that are included and of the `NotCompleted` members (see not completed). ## Opening a data store Use the `open_data_store()` function, illustrated below. Use the mode argument to identify whether to open as read only (`mode="r"`), write (`mode="w"`) or append(`mode="a"`). ### Opening a read only data store We open the zipped directory described above, defining the filenames ending in ``.fa`` as the data store members. All files within the directory become members of the data store (unless we use the ``limit`` argument). ```python { linenums="1" notest } from scinexus import open_data_store dstore = open_data_store("data/raw.zip", suffix="fa", mode="r") # (1)! print(dstore) dstore.describe # (2)! m = dstore[0] # (3)! for m in dstore[:5]: # (4)! print(m) m.read()[:20] # (5)! # 1035x member # ReadOnlyDataStoreZipped(source='/Users/gavin/repos/SciNexus/docs/data/raw.zip', # members=[DataMember(data_store=/Users/gavin/repos/SciNexus/docs/data/raw.zip, # unique_id=ENSG00000157184.fa), # DataMember(data_store=/Users/gavin/repos/SciNexus/docs/data/raw.zip, # unique_id=ENSG00000131791.fa)]...) ENSG00000157184.fa ENSG00000131791.fa # ENSG00000127054.fa ENSG00000067704.fa ENSG00000182004.fa ``` 1. Open a data store. 2. The `.describe` property summarises the contents. 3. You can index like any Python sequence. 4. Or loop over members. 5. And read data from a member. !!! note For a `DataStoreSqlite` member, the default data storage format is bytes. So reading the content of an individual record is best done using the `load_db` app. ### Making a writeable data store The creation of a writeable data store is specified with `mode="w"`, or (to append) `mode="a"`. In the former case, any existing records are overwritten. In the latter case, existing records are ignored. In a directory store, the `suffix` you open with names every *completed* record it writes. The identifier you pass to `write()` supplies the stem only, so `"brca1"`, `"brca1.fa"` and `"brca1.genbank"` all become `brca1.fasta` in a store opened with `suffix="fasta"`. Not-completed records, logs and checksums are kept in their own subdirectories under their own extensions, and the store's suffix does not apply to them. !!! warning "A compression suffix is refused, not replaced" Compression is the one part of the name that says how to read the record back, so a directory store will not quietly swap it. If the identifier names a compression the store does not write, `write()` raises `ValueError` rather than storing the record under a different name. This part is specific to directory stores, since a SQLite store has no suffix to contradict. The rule that an identifier must name a record — a non-blank, non-hidden stem — applies to both. ```python { notest } dstore = open_data_store("results", suffix="fasta", mode="w") dstore.write(unique_id="brca1.fasta.gz", data=seqs) # ValueError: identifier 'brca1.fasta.gz' names .gz, but a record # stored as .fasta carries no compression ``` Open the store with the compression in its suffix instead, and its completed records are gzipped: ```python { notest } dstore = open_data_store("results", suffix="fasta.gz", mode="w") dstore.write(unique_id="brca1", data=seqs) # brca1.fasta.gz, gzipped ``` Not-completed records are always plain `.json`, whatever the store's own suffix is, so a compressed identifier is refused there too — including in a `suffix="fasta.gz"` store, where `write()` accepts `"brca1.fasta.gz"` and `write_not_completed()` does not. ## `DataStoreSqlite` stores serialised data When you specify a Sqlitedb data store as your output (by using `open_data_store()`) you write multiple records into a single file making distribution easier. !!! warning The process which creates a Sqlitedb "locks" the file. If that process exits unnaturally (e.g. the run that was producing it was interrupted) then the file may remain in a locked state. If the db is in this state, `scinexus` will not modify it unless you explicitly unlock it. ### Closing a Sqlitedb data store The lock is released by `close()`, and only by `close()`. That is what gives a lock you find on a file its meaning: it says the session that took it did not finish. So call `close()` once you have finished with a writable store, after reading whatever you need from it. ```python { notest } out_dstore = open_data_store("results.sqlitedb", mode="w") # ... write to it, then read your summaries from it ... out_dstore.close() ``` A store that is garbage collected without being closed warns you and names the file, because it has left a lock behind that the next run will refuse to write over. Closing a data store ends access to it – trying to read from one afterwards raises an exception rather than returning stale answers. Directory and zip data stores take no such lock, and closing one does nothing. `close()` is defined on every data store so that code holding whatever `open_data_store()` returned can close it without first asking which kind it got. This is represented in the display as shown below. ```python { linenums="1" notest } {"completed": 175, "not_completed": 0, "logs": 1, "title": "Unlocked db store."} ``` To unlock, open the store in a writable mode and execute the following: ```python { notest } dstore = open_data_store("data/demo-locked.sqlitedb", mode="a") dstore.unlock(force=True) ``` The store has to be writable, since unlocking writes to it (a read only store ignores the call). `force=True` is needed when the lock was taken by another process, which is the usual case for a store left behind by an interrupted run. Overriding it is meant to be a deliberate act, because the records in such a store were never confirmed complete. ## Interrogating run logs If you use the `apply_to()` method, a `scitrack` logfile will be stored in the data store. This includes useful information regarding the run conditions that produced the contents of the data store. ```python { linenums="1" notest } # [{'time': '2019-07-24 14:42:56', 'name': 'logs/load_unaligned-progressive_align- # write_db-pid8650.log', 'python_version': '3.7.3', 'who': 'gavin', 'command': # '/Users/gavin/miniconda3/envs/c3dev/lib/python3.7/site- # packages/ipykernel_launcher.py -f [...] ``` Log files can be accessed via a special attribute. ```python { linenums="1" notest } # [DataMember(data_store=/Users/gavin/repos/SciNexus/docs/data/demo- # locked.sqlitedb, unique_id=logs/load_unaligned-progressive_align-write_db- # pid8650.log)] ``` Each element in that list is a `DataMember` which you can use to get the data contents. The following ```python { notest } print(dstore.logs[0].read()[:225]) ``` Produces ```python { linenums="1" notest } # 2019-07-24 14:42:56 Eratosthenes.local:8650 INFO system_details : # system=Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; # root:xnu-4903.261.4~2/RELEASE_X86_64 2019-07-24 14:42:56 # Eratosthenes.local:8650 INFO python ``` ## Citations – giving credit to package developers When apps declare citations, those citations are automatically saved alongside your results when you use `apply_to()`. ```python { linenums="1" notest } import pathlib import shutil from citeable import Software from scinexus import define_app, open_data_store from cogent3 import get_app from cogent3.app.typing import AlignedSeqsType my_cite = Software( author=["Doe, J"], title="My Sequence Filter", year=2025, ) @define_app(cite=my_cite) def strict_filter(val: AlignedSeqsType) -> AlignedSeqsType: return val.omit_bad_seqs() in_dstore = open_data_store("data/raw.zip", suffix="fa", limit=5) out_dstore = open_data_store("cited_results", suffix="fa", mode="w") loader = get_app("load_aligned", moltype="dna", format_name="fasta") writer = get_app("write_seqs", data_store=out_dstore, format_name="fasta") process = loader + strict_filter() + writer result = process.apply_to(in_dstore) result.write_bib("my_analysis.bib") print(pathlib.Path("my_analysis.bib").read_text()) # @software{cogent3, author = {Huttley, Gavin and Caley, Katherine and # Fotovat, Nabi and Ma, Stephen Ka-Wah and Koh, Moses and Morris, Richard and # McArthur, Robert and McDonald, Daniel and Jaya, Fred and Maxwell, Peter and # Martini, James and La, Thomas and Lang, Yapeng}, title = {{cogent3}: [...] ``` The `summary_citations` property returns a table of all citations stored in the data store (line 24). Export to BibTeX with `write_bib()` (line 26). !!! note `ReadOnlyDataStoreZipped` supports reading stored citations but not writing them. cogent3-scinexus-b16bc06/docs/howto/use-with-coding-agents.md000066400000000000000000000077221525761402500242360ustar00rootroot00000000000000# Use scinexus with a coding agent !!! abstract "" Install the `scinexus` agent skill, so that a coding assistant knows how the framework fits together before it writes any code for you. Works with any agent that reads skills, not one in particular. ## What the skill is A directory of markdown: a `SKILL.md` carrying a worked loader, generic app and writer pipeline plus the rules that decide whether a first attempt works, and four files beside it that an agent opens only when a task reaches them -- the [llms.txt](../llms.txt) API summary, the file helpers, pipelines and data stores, and the hooks for a package built on `scinexus`. It tells an assistant the things an API reference cannot: that a writer's `main` always receives `NotCompleted` values, that `mode="w"` creates a data store rather than emptying it, that `from __future__ import annotations` stops `define_app` working. ## Install it from this site This site publishes the skill at the location the [agent skills standard](https://posit-dev.github.io/great-docs/user-guide/agent-skills.html) specifies, so a tool that follows that standard needs only the address this page is served from: ```bash npx skills add https://scinexus.readthedocs.io/en/latest/ ``` That reads the manifest at [`.well-known/agent-skills/index.json`](../.well-known/agent-skills/index.json) and installs what it lists. That link is relative, so following it from wherever you are reading this page gives the address to use, whatever prefix this site is served under. Where the files then land is the installing tool's business, and each assistant reads a different place: | agent | directory | | --- | --- | | Claude Code | `.claude/skills/scinexus/` | | Codex | `.codex/skills/scinexus/` | | Cursor | `.cursor/skills/scinexus/` | | GitHub Copilot | `.github/skills/scinexus/` | | Windsurf | `.windsurf/skills/scinexus/` | | OpenCode | `.opencode/skills/scinexus/` | ## Install it by hand Nothing above is required. The files are plain markdown at a fixed address, so fetching them yourself works just as well -- substitute the directory your assistant reads: ```bash mkdir -p .claude/skills/scinexus base=https://scinexus.readthedocs.io/en/latest/.well-known/agent-skills/scinexus for f in SKILL.md io.md pipelines.md extending.md llms.txt; do curl -sSfo ".claude/skills/scinexus/$f" "$base/$f" done ``` An agent that can fetch a URL itself needs even less: point it at `https://scinexus.readthedocs.io/en/latest/skill.md` and ask it to follow the file names inside. ## Install it as a Claude Code plugin Claude Code can also install the skill from the repository, which keeps it up to date with the rest of the plugin machinery: ``` /plugin marketplace add cogent3/scinexus /plugin install scinexus@scinexus ``` Worth knowing how that finds anything, since neither command names a skill. The first clones `github.com/cogent3/scinexus` and reads `.claude-plugin/marketplace.json` at its root, which lists one plugin named `scinexus` whose `source` is the repository root. The second reads `@`, so `scinexus@scinexus` is the plugin named `scinexus` from the marketplace named `scinexus`. Installing it copies that source, and Claude Code then finds skills where the manifest says they are, at `skills//SKILL.md`. Because the source is the repository root, that copies the whole repository -- a couple of megabytes of package, tests and documentation for thirty kilobytes of markdown. The trade buys a single copy of the skill: the same files serve the plugin and this site, so the two cannot disagree. Either of the routes above fetches only the skill. Remove it with: ``` /plugin uninstall scinexus@scinexus /plugin marketplace remove scinexus ``` ## Keeping it current A skill is a copy, and copies go stale. The published files are rebuilt from the repository whenever these docs are, so re-running the install is what refreshes them. If your assistant starts asserting something this documentation contradicts, that is the first thing to check. cogent3-scinexus-b16bc06/docs/howto/write-a-class-app.md000066400000000000000000000072251525761402500232000ustar00rootroot00000000000000# Write a class app !!! abstract "" How to inherit from `scinexus` app base classes, or using the `define_app` decorator, specifying input/output type hints. ## Using inheritance from a base class ```python linenums="1" from collections.abc import Callable from citeable import Software from scinexus import ComposableApp my_cite = Software( author=["Doe, J", "Smith, A"], title="My Sequence Filter", year=2025, url="https://example.com/my-filter", version="0.1.0", ) class my_app( # (1)! ComposableApp[str, str], # (2)! cite=my_cite, # (3)! ): def __init__(self, convert: Callable[[str], str]): self.convert = convert def main(self, val: str) -> str: # (4)! return self.convert(val) ``` 1. We suggest naming your apps using the PEP8 naming style for functions (lowercase separated by underscores) because the instances will be used like functions. 2. We type hint the input / output types with the base class. 3. We assign the citation in the class definition. 4. Your class **must** have a `main()` method with type hints specified for its first argument and its return type. ## Using the `define_app` decorator How to use `@define_app` on a class with a `main()` method, configure it via `__init__` parameters, and control behaviour with the `app_type` parameter. ```python { linenums="1" notest } from collections.abc import Callable from citeable import Software from scinexus import ComposableApp my_cite = Software( author=["Doe, J", "Smith, A"], title="My Sequence Filter", year=2025, url="https://example.com/my-filter", version="0.1.0", ) @define_app(cite=my_cite) # (1)! class my_app: def __init__(self, convert: Callable[[str], str]): self.convert = convert def main(self, val: str) -> str: # (2)! return self.convert(val) ``` 1. The `define_app` decorator is used. You can specify the `app_type` here, which we don't in this case, and assign your citation. 2. Your class **must** have a `main()` method with type hints specified for its first argument and its return type. ### Specifying the app type The `define_app` decorator has a default `app_type` of `"generic"`. This means the app does data transformation and does not load or write data. The supported app types are indicated by the `AppType` enum: ```python { linenums="1" notest } from scinexus.composable import AppType print(list(AppType)) # [, , , ] ``` If your app is not intended to be composed sequentially with other apps, set it to non-composable: ```python from scinexus import define_app, AppType @define_app(app_type=AppType.NON_COMPOSABLE) class my_standalone_app: def main(self, val: str) -> str: return val.upper() ``` ## Handling `NotCompleted` values By default, apps skip `NotCompleted` inputs — they propagate through the pipeline without calling `main()`. If your app needs access to `NotCompleted` instances, set `skip_not_completed=False`: ```python from scinexus import define_app, NotCompleted @define_app(skip_not_completed=False) class my_logger: def main(self, val: str) -> str: if isinstance(val, NotCompleted): # handle the failure ... return val ``` A writer does not need the flag — it is forced off, so a writer's `main()` always receives `NotCompleted` values and must handle them. See [Handle failures](handle-failures.md). cogent3-scinexus-b16bc06/docs/howto/write-a-function-app.md000066400000000000000000000042711525761402500237160ustar00rootroot00000000000000# Write a function app !!! abstract "" How to use `@define_app` on a plain function, specifying input/output type hints and turning function parameters into constructor arguments. ```python { linenums="1" notest } from collections.abc import Callable from citeable import Software from scinexus import define_app, AppType my_cite = Software( author=["Doe, J", "Smith, A"], title="My Sequence Filter", year=2025, url="https://example.com/my-filter", version="0.1.0", ) @define_app( app_type=AppType.GENERIC, # (1)! cite=my_cite, # (2)! ) def my_app(val: str, convert: Callable[[str], str]) -> str: # (3)! return convert(val) app = my_app(str.upper) print(app("hello world")) # HELLO WORLD ``` 1. We specify the `app_type` explicitly here. 2. We assign the citation in the class definition. 3. The function definition has type hints for its first argument and its return type. !!! note Your function can only have one required argument. It can have any number of optional arguments. Pay attention to the order of arguments for the function! Every call to the app provides a new instance of `val`. Whereas `str.upper` is assigned to the variable `convert`. You can think of all of the other arguments as being arguments to a class constructor. Under the hood, `scinexus` caches these and injects them into each call of your function with new values of `val`. cogent3-scinexus-b16bc06/docs/images/000077500000000000000000000000001525761402500175245ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/images/logo-bw.png000066400000000000000000000023111525761402500215750ustar00rootroot00000000000000‰PNG  IHDRä‚íÅ pHYsll\ý.tEXtSoftwarewww.inkscape.org›î<VIDATH‰…Ö{ÈÞeðÏ÷éÝÞwSçœÊDg¥´Eåa­Ði£,g…‡y¬DHl”ž +¡Lrh”a˜¥˜Ùô+ÃéŒrsYJ)uæ!sn+}·¬V©›]ýqß{x!ºàÇó;Þ×}]ßÃõ¤ª@’å8sð$.©ªÇû³%ø0ŽÄîZ¼�ç°wTÕ_’àLŒã |©ªžë ]‚wâxlű¸9É�qþˆÛñ=lªªJ2 oÇRÜ–äIÃQx ÁíIŽ öÄ:¼oÂB,è ¯ÅûªêEÿ'’œƒOb föÌì•5À!x°ª¶÷ÖÀUµ 8;É“Œý�{'9Ëðs zgÆñOÜ……¼„7'YÔw0Ï$™Ó«ºGcM’³Fì“ä¬Ä.¸oÁ|LïIVc_LêdX‹°¸_7àüª¾3ŽËp ï8bäù4üïïÉÆúñ3œ”ª’d_<�;°±¿|¾Ð�–c/œØ]Ý»qwU=Ú«\€âqlÁ)XWU˽ °ßî žQUŸ¯!÷í—u`Wâ�‰Á­Iö€ªzGô–¿Š�ã`½4ø4®ªª‡’üoÕ4"É\œ‡Sq^Á,�¾OõëËqnO¶#É;pfUmIòB’EÊí­‚ñE;ãJ|½ƒú÷]¾ ¿ÓDyP'“$ 1YU[ú÷«ñÑAÇgó°MUõ~•ä‚îsqöÃ"Ü�ßàA|;ð|'Én½ý_Ùèoqè{ô�ŽÆà?Ò@?¼ãSýü:�43ú÷³{e�áÒ)ŸÄì�è´)‰f`o ðqüÇëvkúâÁ†~ Ñ{þ”µ&h*Þ¤õ4®Å×4süj_`Í¢öÂûÑ,ìÀ¾Î©8.É⑵æaã ª¶bÎÐb’Œ×«êÞŽ×OðÙ^Õcø—FÝI §í}3WÕ4)¬I´� Y÷ œ–d®Á†$³ú³«p26k³Ysè74æ�‚«êÏPU/cS/| «†:Ú¤`¥ôÛ°6É7«êÖ$á&�Σ1ûãâá�$ó4̯L²ÛªêO4ü»i@žÞgjnü¡ÎüpÀóÚ€{vñ»5Uõt’h:ùG¯f¼ªÖ'™—dÂgªjCïÒ§’¯Mâíš ÖàQMØË4‡™Äecë¦ø”FÕY“žO²;þ�£«jÇ|ôḪ;Â7Ú­z¸_¿ZU÷%™�ém4œÜÜÖAEãrMŒ™šdJÌÕôµ´Ëä0�y‡hÞø‡Ø9ïÂõÚÌÿµæè'à—aÖkº¶nBûŸ±¤·üû8F#ÍmÎ3ñ #ƒoçã4M�ÓûËß­ª× É{5ñÍÓìgcOzO׎> ÏÒ´3�g±¢ª^ü/K3žæšâ1IEND®B`‚cogent3-scinexus-b16bc06/docs/images/logo-bw.svg000066400000000000000000001151231525761402500216160ustar00rootroot00000000000000 cogent3-scinexus-b16bc06/docs/images/logo-text-bw.svg000066400000000000000000001262141525761402500226030ustar00rootroot00000000000000 cogent3-scinexus-b16bc06/docs/images/logo-text-wb.svg000066400000000000000000001263011525761402500226000ustar00rootroot00000000000000 cogent3-scinexus-b16bc06/docs/images/logo-wb.png000066400000000000000000000021761525761402500216060ustar00rootroot00000000000000‰PNG  IHDRä‚íÅ pHYsll\ý.tEXtSoftwarewww.inkscape.org›î< IDATH‰…Öi¬�cðß9nÝÛ«¥›�¶–ŠV,¥–hKc­%h«öH*"A[b‹6ˆ%„Zb-H,ICÔ´µ+mP»’ ZÔµ”¢(ãÃÌu�%Lòæœç¼ïûÌ3ó_æ4"Bű8ðÎÇuoöÆNX§~û b.Âh`*ŽB;Þ¹ø°­^:[à|�ýp'îÃ�XŒp3–!Ð ›a®*z Cj³ð²”ÄéUÁ¸}eûÏk9è+ئYý]î¯q‰ü^ úh‰OÔ÷%iz×ûýª²E¸Ð_Þ…~M h¯¿%ê-…¸X ï|‡_$vsjó–Ô5 ¯IµF©âeÕ¾Ö¸—J@/ª Ö’5 $»b 6©}ÆþÇMIÅz8²N>Oâu?N«ªáGIÝ.=ÖuÎÁïR Ó[�ÂÂnÖ=‰C¤VfT+Ö®{Wa²$ÌFõ¹Bb¶¥åmø¼žÿ²ªQëC1»»Še’3%ЛÊ9s¥ä¸CÒ¹5`ƒª¦µU½¤,ÅJ|,"öŒˆ‡#¢oyÔõÙODÄåeÃ"bLjx1">Šˆ©±}Dôiñ»IåwgDÄ”òŧ#¢w#"ézD¸}¤÷½)é=^Ž� q»ô·’âWKœ‡H�M—®±Aµq<–´I»»J�|/ÇE{%*�q>Ž/üà9wh—´] {¢t˜.Lk“lë/}k $Á¯øHþ?øIΣÕþ³ëê‹Ëë  j½JÚÚp¬Ù”£ar¸R‚þ­žÑ°¤¾ÿW¬'õ5¾¾£$ØÖRSo7"¢{:>‚›¤¿=/}ž©ŠÞ”ºén]‡üŸ1®*¹{I‡˜%qîÄáøsðuà©¥å(ž…ë%°°ƒßPi?K+éStUùÑR;ø@’ãÓ?ö�° öl¢—IEND®B`‚cogent3-scinexus-b16bc06/docs/images/logo-wb.svg000066400000000000000000001151231525761402500216160ustar00rootroot00000000000000 cogent3-scinexus-b16bc06/docs/index.md000066400000000000000000000074251525761402500177200ustar00rootroot00000000000000
scinexus logo scinexus logo
# About !!! abstract "*Just as `attrs` and `dataclasses` use type hints to simplify data type definition, `scinexus` uses them to simplify writing best-practice scientific algorithms.*" `scinexus` (pronounced 'sigh-nexus') is a Python framework for rapid development of data processing applications. It enables interoperability between apps through defined data types, allowing development of scientific domain app ecosystems (for examples see [cogent3](https://cogent3.org/doc/app/index-app.html) and [piqtree](https://piqtree.readthedocs.io/en/stable/)). Many scientific problems require repeating calculations across many files or database records. Such tasks suit data-level parallelism on multi-core CPUs, but writing robust, maintainable code for them is often tedious and quickly becomes complex. With `scinexus` apps, you can use a functional programming style when developing your application. Combined with `scinexus` app composition, this greatly simplifies your programming logic making it easier to understand and thus easier to explain. And as we know !!! quote If the implementation is easy to explain, it may be a good idea. -- Tim Peters, "Zen of Python" ## What you get - Type checking at composition time - Durable computing[^1] - Greatly simplified data level parallel execution - Automated logging - Automated citation tracking - Checkpointing via data stores - Customisable experience (progress bars[^2], parallelisation[^3], data store representations etc..) [^1]: Failures are automatically recorded as `NotCompleted` records which get propagated and stored in [data stores](explanation/not-completed-design.md). These records record salient details that help you identify the cause of the failure. [^2]: `tqdm` is the default because of its robustness in notebooks, but you can choose `rich`. [^3]: The default follows the interpreter — Python’s standard library `multiprocessing` module, or its `ThreadPoolExecutor` in the master process on a free-threaded build, where threads can run at the same time. If you're using Jupyter Notebooks, however, it's recommended that you use `loky`. This is an [installation option](install.md#optional-extras) and [configuration is easy](howto/run-in-parallel.md#choosing-a-parallel-backend). ## Standalone utilities `scinexus` also provides generally useful utilities for developers of data analysis applications. Utilities for file IO, parallel execution, and progress tracking are usable independently of the app framework. ## Get started - **Install `scinexus`** -- see [Installing from PyPI](install.md) - **Build algorithms** -- see [How to write apps](howto/write-a-function-app.md) - **Build applications for others** -- see [Why composable apps?](explanation/why-composable-apps.md) - **Use existing apps** -- see [Composing apps](tutorials/composing-apps.md) ## The `scinexus` origin story The app infrastructure code was originally developed within [cogent3](https://cogent3.org), where it accumulated over seven years of development, testing, and real-world use in computational genomics before being extracted into `scinexus`. The design is mature and has underpinned analyses in published studies. We acknowledge here that many members of the `cogent3` community contributed to the code that now lives here, including [@GavinHuttley](https://github.com/GavinHuttley), [@rmcar17](https://github.com/rmcar17), [@Nick-Foto](https://github.com/Nick-Foto), [@KatherineCaley](https://github.com/KatherineCaley), [@fredjaya](https://github.com/fredjaya), and [@khiron](https://github.com/khiron). cogent3-scinexus-b16bc06/docs/install.md000066400000000000000000000016731525761402500202560ustar00rootroot00000000000000# Installation !!! abstract "" How to install `scinexus` and its optional extras for parallel execution, progress bars, and MPI support. ## Basic install ```bash pip install scinexus ``` ## Optional extras - `pip install "scinexus[loky]"` -- uses the [loky](https://loky.readthedocs.io/) library for parallel execution. Loky provides reusable process pools that are more robust than the stdlib `ProcessPoolExecutor`, particularly in Jupyter notebooks where standard multiprocessing can fail. Recommended for interactive and notebook-based workflows. - `pip install "scinexus[rich]"` -- also installs the `rich` package for its progress bars ([see using rich](howto/track-progress.md)) - `pip install "scinexus[mpi]"` -- MPI parallel execution via `mpi4py` You can combine extras: ```bash pip install "scinexus[loky,rich]" ``` ## Requirements - Python 3.11+ ## Verify installation ```python import scinexus print(scinexus.__version__) ``` cogent3-scinexus-b16bc06/docs/llms.txt000066400000000000000000000156741525761402500200040ustar00rootroot00000000000000# scinexus > A composable app infrastructure for scientific computing scinexus (pronounced "sigh-nexus") is a Python framework for building composable, type-checked data processing pipelines. What dataclasses and attrs are for structured data, scinexus apps are for structured algorithms. It enables interoperability between apps through defined data types, supporting scientific domain app ecosystems. ## Core Concepts ### define_app The `@define_app` decorator transforms a class (with a `main()` method) or a function into a composable app. Apps are callable and compose with `+`. ```python from scinexus import define_app @define_app class upper: def main(self, data: str) -> str: return data.upper() result = upper()("hello") # "HELLO" ``` Function-based apps are also supported: ```python @define_app def double(val: int) -> int: return val * 2 ``` ### App Types - `GENERIC` (default) -- general-purpose processing step - `LOADER` -- must be first in a composed pipeline - `WRITER` -- must be last in a composed pipeline; writes results to a data store - `NON_COMPOSABLE` -- cannot participate in `+` composition Set via `@define_app(app_type="loader")` etc. ### App Composition Apps compose with `+`. Type compatibility between the return type of the left app and the input type of the right app is checked at composition time. ```python @define_app def add_one(x: int) -> int: return x + 1 @define_app def to_str(x: int) -> str: return str(x) pipeline = add_one() + to_str() pipeline(5) # "6" ``` Ordering rules: LOADER must be first, WRITER must be last, GENERIC can go anywhere in between. ### NotCompleted A sentinel return type for failed computations. It propagates through pipelines without raising exceptions, enabling durable computing. ```python from scinexus import NotCompleted nc = NotCompleted("ERROR", "my_app", "something went wrong", source="input.txt") bool(nc) # False -- so it is falsy ``` If an app's `main()` raises an exception, the framework catches it and returns a `NotCompleted` instance. If an app receives a `NotCompleted` as input (and `skip_not_completed=True`, the default), it passes it through unchanged. A writer is the exception on both counts. `skip_not_completed` is forced off for a writer and `apply_to` calls its `main()` directly, so a writer always receives `NotCompleted` values and must handle them. An exception raised in a writer's `main()` is not caught either: it ends the run. ## Data Stores Data stores provide checkpointed, append-only storage for pipeline results. ```python from scinexus import open_data_store # Directory-based dstore = open_data_store("results/", suffix="json", mode="w") # SQLite-based dstore = open_data_store("results.sqlitedb", mode="w") # Read-only from a zip dstore = open_data_store("results.zip") ``` Key features: - `.describe` -- summary of stored data - `.summary_not_completed` -- summary of failed results - Completed members accessed via `.completed` - Membership testing with `"identifier" in dstore` - Logs and citations are stored alongside data ## Writer Apps and apply_to Writer apps process an entire data store, with logging, checkpointing, and progress tracking: ```python @define_app(app_type="writer") class save_result: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str) -> str: if isinstance(data, NotCompleted): # a writer always receives these, so it must handle them self.data_store.drop_not_completed(unique_id=identifier) self.data_store.write_not_completed( unique_id=identifier, data=data.to_json() ) else: self.data_store.write(unique_id=identifier, data=data) return identifier pipeline = add_one() + to_str() + save_result(dstore) pipeline.apply_to(input_dstore, parallel=True, show_progress=True) ``` ## Parallel Execution Four backends are available: ```python from scinexus import set_parallel_backend, get_parallel_backend set_parallel_backend("multiprocess") # stdlib multiprocessing set_parallel_backend("threads") # stdlib ThreadPoolExecutor set_parallel_backend("loky") # recommended for Jupyter set_parallel_backend("mpi") # for HPC clusters via mpi4py ``` With no call to `set_parallel_backend`, the backend is `"threads"` when the GIL is not in force and the caller is not itself a pool worker, and `"multiprocess"` otherwise. The process backends pickle the app and its arguments, giving each worker its own copy. The `"threads"` backend shares the app instance between workers instead, so a `main()` that mutates `self` or module-level state is a data race there. Use `parallel=True` in `apply_to()` or `as_completed()` to enable parallel execution. Configure workers via `par_kw={"max_workers": 4}`. ## Progress Tracking ```python from scinexus import set_progress_backend set_progress_backend("tqdm") # default set_progress_backend("rich") # requires rich extra ``` Pass `show_progress=True` to `apply_to()` or `as_completed()`. ## Installation ``` pip install scinexus ``` Optional extras: - `pip install scinexus[loky]` -- process pool for Jupyter - `pip install scinexus[rich]` -- rich progress bars - `pip install scinexus[mpi]` -- MPI support via mpi4py Requires Python 3.11+. ## Type System scinexus checks type compatibility when composing apps with `+`. The return type of the left app must overlap with the input type of the right app. Standard typing constructs (`Union`, `Optional`, protocols) are supported. ## Public API Direct imports from `scinexus`: - `define_app` -- decorator for creating apps - `NotCompleted`, `NotCompletedType` -- failure sentinel - `is_app`, `is_app_composable` -- introspection helpers - `AppBase`, `ComposableApp`, `LoaderApp`, `WriterApp`, `NonComposableApp` -- base classes for inheritance-based app definition - `Progress`, `ProgressContext`, `get_progress`, `set_progress_backend` -- progress tracking - `__version__` -- package version Lazy imports from `scinexus`: - `open_data_store` -- create/open data stores - `open_` -- open files with format detection - `set_parallel_backend`, `get_parallel_backend` -- parallel configuration - `set_summary_display`, `get_summary_display` -- customise data store summary output - `set_id_from_source`, `get_id_from_source` -- customise unique identifier extraction ## Documentation - How-to guides: writing function apps, writing class apps, composing apps, using data stores, handling failures, running in parallel, tracking progress, logging and citations, extending the type system, customising display, migrating from cogent3 - Tutorials: composing apps, processing a dataset - Explanations: why composable apps, app lifecycle, type system, data store model, NotCompleted design, source tracking, customisation hooks, control flow ## Links - Documentation: https://scinexus.readthedocs.io - Source: https://github.com/cogent3/scinexus - Bug tracker: https://github.com/cogent3/scinexus/issues cogent3-scinexus-b16bc06/docs/reference/000077500000000000000000000000001525761402500202155ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/reference/app-classes.md000066400000000000000000000007161525761402500227560ustar00rootroot00000000000000# App classes Properties and methods of the app base classes. ::: scinexus.composable.AppBase options: show_root_heading: true ::: scinexus.composable.ComposableApp options: show_root_heading: true ::: scinexus.composable.LoaderApp options: show_root_heading: true ::: scinexus.composable.WriterApp options: show_root_heading: true ::: scinexus.composable.NonComposableApp options: show_root_heading: true cogent3-scinexus-b16bc06/docs/reference/data-stores.md000066400000000000000000000022221525761402500227630ustar00rootroot00000000000000# Data stores API for `open_data_store` and data store backends. ::: scinexus.io.open_data_store options: show_root_heading: true ::: scinexus.data_store.DataStoreABC options: show_root_heading: true ::: scinexus.data_store.DataStoreDirectory options: show_root_heading: true ::: scinexus.data_store.ReadOnlyDataStoreZipped options: show_root_heading: true ::: scinexus.sqlite_data_store.DataStoreSqlite options: show_root_heading: true ::: scinexus.data_store.DataMemberABC options: show_root_heading: true ::: scinexus.data_store.DataMember options: show_root_heading: true ::: scinexus.data_store.set_summary_display options: show_root_heading: true ::: scinexus.data_store.get_summary_display options: show_root_heading: true ::: scinexus.data_store.set_id_from_source options: show_root_heading: true ::: scinexus.data_store.get_id_from_source options: show_root_heading: true ::: scinexus.data_store.get_unique_id options: show_root_heading: true ::: scinexus.data_store.get_data_source options: show_root_heading: true cogent3-scinexus-b16bc06/docs/reference/define-app.md000066400000000000000000000004641525761402500225530ustar00rootroot00000000000000# `define_app` Full signature, parameters, and behaviour of the `define_app` decorator, plus the `AppType` enum that controls which base class is used. ::: scinexus.composable.define_app options: show_root_heading: true ::: scinexus.composable.AppType options: show_root_heading: true cogent3-scinexus-b16bc06/docs/reference/deserialise.md000066400000000000000000000002171525761402500230300ustar00rootroot00000000000000# Deserialisation API for the extensible JSON deserialisation registry. ::: scinexus.deserialise options: show_root_heading: false cogent3-scinexus-b16bc06/docs/reference/index.md000066400000000000000000000016651525761402500216560ustar00rootroot00000000000000# API reference Complete API documentation for all public `scinexus` modules. ## App framework - [App classes](app-classes.md) -- `AppBase`, `ComposableApp`, `WriterApp`, `LoaderApp`, `NonComposableApp` - [define_app](define-app.md) -- the `define_app` decorator and `AppType` enum - [NotCompleted](not-completed.md) -- the `NotCompleted` sentinel and `NotCompletedType` enum - [Source tracking](source-proxy.md) -- `source_proxy` and `propagate_source` for data provenance - [Data stores](data-stores.md) -- `open_data_store` and data store backends ## Standalone utilities - [IO utilities](io-util.md) -- file IO with compression, atomic writes, streaming - [Parallel execution](parallel.md) -- parallel map, imap, as_completed - [Progress](progress.md) -- progress bar ABCs and backends - [Deserialisation](deserialise.md) -- JSON deserialisation registry - [Utilities](utilities.md) -- introspection helpers and type namespace registration cogent3-scinexus-b16bc06/docs/reference/io-util.md000066400000000000000000000002321525761402500221160ustar00rootroot00000000000000# IO utilities API for file IO functions that work independently of the app framework. ::: scinexus.io_util options: show_root_heading: false cogent3-scinexus-b16bc06/docs/reference/not-completed.md000066400000000000000000000004401525761402500233070ustar00rootroot00000000000000# `NotCompleted` The sentinel type for failed computations that propagates through pipelines without raising exceptions. ::: scinexus.composable.NotCompleted options: show_root_heading: true ::: scinexus.composable.NotCompletedType options: show_root_heading: true cogent3-scinexus-b16bc06/docs/reference/parallel.md000066400000000000000000000002541525761402500223340ustar00rootroot00000000000000# Parallel execution API for parallel execution functions that work independently of the app framework. ::: scinexus.parallel options: show_root_heading: false cogent3-scinexus-b16bc06/docs/reference/progress.md000066400000000000000000000002051525761402500224000ustar00rootroot00000000000000# Progress API for progress tracking ABCs and built-in backends. ::: scinexus.progress options: show_root_heading: false cogent3-scinexus-b16bc06/docs/reference/source-proxy.md000066400000000000000000000004001525761402500232100ustar00rootroot00000000000000# Source tracking Internal utilities for tracking data provenance through app pipelines. ::: scinexus.composable.source_proxy options: show_root_heading: true ::: scinexus.composable.propagate_source options: show_root_heading: true cogent3-scinexus-b16bc06/docs/reference/utilities.md000066400000000000000000000013021525761402500225460ustar00rootroot00000000000000# Utilities API for introspection helpers and type namespace registration. ## App introspection ::: scinexus.composable.is_app options: show_root_heading: true ::: scinexus.composable.is_app_composable options: show_root_heading: true ## Type system ::: scinexus.typing.register_type_namespace options: show_root_heading: true ::: scinexus.typing.SerialisableType options: show_root_heading: true ::: scinexus.typing.IdentifierType options: show_root_heading: true ## Introspection helpers ::: scinexus.misc.get_object_provenance options: show_root_heading: true ::: scinexus.misc.in_jupyter options: show_root_heading: true cogent3-scinexus-b16bc06/docs/scripts/000077500000000000000000000000001525761402500177465ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/scripts/cog_utils.py000066400000000000000000000075641525761402500223240ustar00rootroot00000000000000import os import pathlib import re import sys import textwrap import urllib.request import zipfile from io import StringIO import cog ROOT_DIR = pathlib.Path(__file__).parent.parent DOCS_DIR = ROOT_DIR DATA_DIR = ROOT_DIR / "data" DATA_DIR.mkdir(exist_ok=True) DATA_URL = "https://github.com/user-attachments/files/26728407/raw.zip" DECOMPRESS_URL = ( "https://github.com/user-attachments/files/26769518/demo-locked.sqlitedb.zip" ) def count_pattern(text: str) -> int: return len(re.findall(r"# \((\d+)\)!", text)) def _register_cogent3_types() -> None: try: from cogent3.app.typing import _get_resolution_namespace from scinexus.typing import register_type_namespace register_type_namespace(_get_resolution_namespace) except ImportError: pass _register_cogent3_types() def setup_installed() -> None: zip_dest = DATA_DIR / "raw.zip" if not zip_dest.exists(): urllib.request.urlretrieve(DATA_URL, filename=zip_dest) return decompress_dest = DATA_DIR / "demo-locked.sqlitedb.zip" if not decompress_dest.exists(): urllib.request.urlretrieve(DECOMPRESS_URL, filename=decompress_dest) with zipfile.ZipFile(decompress_dest, "r") as zip_ref: zip_ref.extractall(DATA_DIR) return def exec_codeblock( *, src: str, lang: str = "python", width: int = 80, max_lines: int = 10, admonition: str | None = None, use_wrap: bool = True, display_src: bool = True, annotations: list[str] | None = None, ) -> None: """Execute code lines, then emit a fenced code block with source + output. Parameters ---------- lines List of code strings (one per line). lang Language for the emitted code block (default: "python"). width Wrap width for output lines. max_lines Maximum number of lines to emit for output (after wrapping). use_wrap Whether to wrap the output lines. admonition Optional admonition type to wrap the code block in (e.g. "note", "tip", etc.). display_src Whether to include the source code in the emitted code block. annotations Optional list of annotation strings to include as comments in the emitted code block. """ setup_installed() cwd = os.getcwd() # noqa: PTH109 os.chdir(ROOT_DIR) wd = os.getcwd() lines = src.splitlines() if not lines[0]: lines = lines[1:] if not lines[-1]: lines = lines[:-1] src = "\n".join(lines) buf = StringIO() sys.stdout = buf try: ns: dict = {} exec(src, ns) # noqa: S102 except Exception as exc: msg = f"Error executing code block: \n{src}\n{exc}\ncwd={wd}\n" raise Exception(msg) from exc sys.stdout = sys.__stdout__ output = [f'```{lang} {{ linenums="1" notest }}'] output_text = buf.getvalue().strip() if display_src: output.append(src) if use_wrap and output_text: output.extend( [ "", *[ f"# {l}" for l in textwrap.wrap( output_text, width=width, max_lines=max_lines ) ], ] ) elif output_text: output.extend(["", output_text]) output.append("```") if annotations: expect = count_pattern(src) if expect != len(annotations): msg = f"Number of annotations ({len(annotations)}) does not match expected ({expect}) based on pattern count in source." raise ValueError(msg) annotation_text = "\n".join(f"{i}. {a}" for i, a in enumerate(annotations, 1)) output.extend(["", annotation_text]) txt = "\n".join(output) if admonition: txt = textwrap.indent(txt, " ") txt = f"{admonition}\n\n{txt}" cog.outl(txt, dedent=False) os.chdir(cwd) cogent3-scinexus-b16bc06/docs/stylesheets/000077500000000000000000000000001525761402500206335ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/stylesheets/extra.css000066400000000000000000000012011525761402500224620ustar00rootroot00000000000000@import url('https://fonts.bunny.net/css?family=orbitron:400,500,700&display=swap'); .md-header__topic:first-child .md-ellipsis { font-family: 'Orbitron', sans-serif; } [data-md-color-scheme="slate"] .md-logo img, [data-md-color-scheme="slate"] .md-nav__button.md-logo img { content: url('../images/logo-wb.png'); } [data-md-color-scheme="default"] .only-dark { display: none; } [data-md-color-scheme="slate"] .only-light { display: none; } [data-md-color-scheme="slate"] .only-dark { display: inline; } [data-md-color-scheme="slate"] { --md-mermaid-label-fg-color: #000; --md-mermaid-node-bg-color: #fff; }cogent3-scinexus-b16bc06/docs/tutorials/000077500000000000000000000000001525761402500203055ustar00rootroot00000000000000cogent3-scinexus-b16bc06/docs/tutorials/composing-apps.md000066400000000000000000000117401525761402500235710ustar00rootroot00000000000000# Composing apps !!! abstract "" Compose multiple apps into a single pipeline using the `+` operator, see what happens when types don't match, and observe how `NotCompleted` propagates through a pipeline without raising exceptions. ## Why compose? Consider an app that performs a molecular evolutionary analysis (`fit_model`) and another that extracts statistics from the result (`extract_stats`). You could apply them sequentially: ```python { notest } fitted = fit_model(alignment) stats = extract_stats(fitted) ``` Composability simplifies this into a single callable: ```python { notest } app = fit_model + extract_stats stats = app(alignment) ``` You can have many more apps in a composed function than just two. ## A worked example We compose three apps: a loader, a processor, and a writer. ```python { notest } from cogent3 import get_app from scinexus import open_data_store out_dstore = open_data_store(path_to_dir, suffix="fa", mode="w") loader = get_app("load_aligned", format_name="fasta", moltype="dna") cpos3 = get_app("take_codon_positions", 3) writer = get_app("write_seqs", out_dstore, format_name="fasta") ``` ### Using apps sequentially ```python { notest } data = loader("data/primate_brca1.fasta") just3rd = cpos3(data) m = writer(just3rd) ``` ### Composing into a single pipeline ```python { notest } process = loader + cpos3 + writer m = process("data/primate_brca1.fasta") ``` The result is identical, but the composed form is more concise and enables batch processing via `apply_to()`. ## Composability rules ### App type ordering Loaders and writers are special cases. If included, a loader must always be first: ```python { notest } app = a_loader + a_generic ``` If included, a writer must always be last: ```python { notest } app = a_generic + a_writer ``` Changing the order for either will raise a `TypeError`. ### Type compatibility Apps define the type of input they accept and the type of output they produce. For two apps to be composed, the output type of the app on the left must overlap with the input type of the app on the right. If they don't match, a `TypeError` is raised. ## `NotCompleted` propagation If any step in a composed pipeline returns a `NotCompleted`, subsequent steps are skipped and the `NotCompleted` is returned as the final result. ???+ example "Condition not satisfied" ```python { linenums="1" notest } from cogent3 import get_app reader = get_app("load_aligned", format_name="fasta") select_seqs = get_app("take_named_seqs", "Mouse", "Human") app = reader + select_seqs result = app("data/primate_brca1.fasta") print(result) # (1)! # NotCompleted(type=FAIL, origin=take_named_seqs, source="primate_brca1", # message="named seq(s) {'Mouse'} not in ('FlyingLem', 'TreeShrew', 'Galago', # 'HowlerMon', 'Rhesus', 'Orangutan', 'Gorilla', 'Chimpanzee', 'Human')") ``` 1. A successful load but a failed selection — the `NotCompleted` from `select_seqs` is returned ???+ example "Caught an exception" ```python { linenums="1" notest } from cogent3 import get_app reader = get_app("load_aligned", format_name="fasta") select_seqs = get_app("take_named_seqs", "Mouse", "Human") app = reader + select_seqs result = app("primate_brca1.fasta") print(result) # (1)! # NotCompleted(type=ERROR, origin=load_aligned, source="primate_brca1", # message="Traceback (most recent call last): File # "/Users/gavin/repos/SciNexus/src/scinexus/composable.py", line 545, in __call__ # result = self.main(val, *args, **kwargs) # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File # "/Users/gavin/repos/SciNexus/.venv/lib/python3.12/site- # packages/cogent3/app/io.py", line 334, in main return _load_seqs(path, # cogent3.make_aligned_seqs, self._parser, self.moltype) # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File # "/Users/gavin/repos/SciNexus/.venv/lib/python3.12/site- [...] ``` 1. An error during load — `select_seqs` is never called. cogent3-scinexus-b16bc06/docs/tutorials/index.md000066400000000000000000000004031525761402500217330ustar00rootroot00000000000000# Tutorials Step-by-step lessons that take you from zero to productive with `scinexus`. - [Composing apps](composing-apps.md) -- build pipelines with `+` and type safety - [Processing a dataset](processing-a-dataset.md) -- batch processing with data stores cogent3-scinexus-b16bc06/docs/tutorials/processing-a-dataset.md000066400000000000000000000075371525761402500246600ustar00rootroot00000000000000# Processing a dataset !!! abstract "" Use `open_data_store` with a loader, processor, and writer app to batch-process a directory of files `apply_to`, and enable progress bars and parallel execution. Then inspect results. We will translate the DNA sequences in `raw.zip` into amino acid and store them as a sqlite database. We will interrogate the generated data store to get a synopsis of the results. ???+ example "Translating DNA to amino acid" ```python { linenums="1" notest } from scinexus import open_data_store from cogent3 import get_app in_dstore = open_data_store("data/raw.zip", suffix="fa") # (1)! out_dstore = open_data_store("translated.sqlitedb", mode="w") # (2)! load = get_app("load_unaligned", moltype="dna") translate = get_app("translate_seqs") write = get_app("write_db", data_store=out_dstore) app = load + translate + write # (3)! out_dstore = app.apply_to(in_dstore) # (4)! out_dstore.describe # (5)! out_dstore.validate() # (6)! out_dstore.summary_not_completed # (7)! out_dstore.close() # (8)! ``` 1. Open the zipped input data store, selecting `.fa` files as members. 2. Create a writable SQLite output data store. Using a single database file is more efficient than writing many small files. 3. Compose loader, translator, and writer into a single pipeline. 4. Apply the pipeline to every member of the input data store. Results are written to `out_dstore`. 5. Summary showing counts of completed records, not-completed records, and log files. 6. Verify the integrity of all records via MD5 checksums. 7. Summary of why some records could not be processed — e.g. sequences not divisible by 3 or containing stop codons. 8. Release the lock this run holds on the database. A SQLite store left unclosed stays locked, which is how an interrupted run is recognised, so close it once you have finished reading from it. !!! note The `.completed` and `.not_completed` attributes give access to the different types of members, while `.members` gives them all. For example, `len(out_dstore.not_completed)` returns the count of failed records and each element is a `DataMember`. cogent3-scinexus-b16bc06/noxfile.py000066400000000000000000000135621525761402500173540ustar00rootroot00000000000000import os import pathlib import shutil import subprocess import sys import nox # on python >= 3.12 this will improve speed of test coverage a lot if sys.version_info >= (3, 12): os.environ["COVERAGE_CORE"] = "sysmon" # "3.14t" is the free-threaded (no-GIL) build; uv can fetch it as # cpython-3.14.x+freethreaded. It is exploratory here, not yet a support claim. # # 3.14t is the only free-threaded build the dev group installs into. The group # pins cogent3, which requires numba on every version it supports, and numba # requires llvmlite. Both publish free-threaded wheels for 3.14t alone, so on # 3.13t uv falls back to building llvmlite from source and that needs an LLVM # toolchain. The 3.13t interpreter itself installs fine, and test_types would # run there because it installs mypy alone. Adding a free-threaded version # once its wheels exist means this list and the matrices named in the note at # the top of ci.yml. _py_versions = [f"3.{v}" for v in range(11, 15)] + ["3.14t"] nox.options.default_venv_backend = "uv" @nox.session(python=False) def fmt(session: nox.Session) -> None: session.run("ruff", "check", "--fix-only", ".", external=True) session.run("ruff", "format", ".", external=True) @nox.session(python="3.14") def cogdocs(session: nox.Session) -> None: session.install("-e", ".", "--group", "dev") cmnd = ( 'find docs -name "*.md" | xargs ' "uv run --group dev --group doc cog -r -I docs/scripts" ) subprocess.run(cmnd, check=True, shell=True) # noqa: S602 def _mypy(session: nox.Session) -> None: # each session gets its own cache: they install different packages, and a # cache shared with a session where an optional extra was present hides # the import errors the other session exists to catch session.run("mypy", f"--cache-dir=.mypy_cache/{session.name}", "src/scinexus/") @nox.session(python=_py_versions) def type_check(session): session.install("-e", ".", "--group", "dev") _mypy(session) @nox.session(python=_py_versions) def test_types(session): # mypy is installed explicitly rather than via the dev group: this session # deliberately type checks against runtime dependencies only. Without it # nox falls back to whatever mypy is on PATH, on the wrong interpreter. session.install("-e", ".", "mypy") _mypy(session) @nox.session(python=_py_versions) def test(session): session.install("-e", ".", "--group", "dev") session.run("uv", "pip", "list") # doctest modules within scinexus session.chdir("src/scinexus") session.run( "pytest", "-s", "-x", "--doctest-modules", ".", ) session.chdir("../../tests") session.run( "pytest", "-s", "-x", "-m", "not slow and not mpi", *session.posargs, ) @nox.session(python=_py_versions) def testmpi(session): session.install("-e", ".[mpi]", "--group", "dev") session.chdir("tests") py = pathlib.Path(session.bin_paths[0]) / "python" session.run( "mpiexec", "--oversubscribe", "-n", "4", str(py), "-m", "mpi4py.futures", "-m", "pytest", "-s", "-x", "-m", "mpi", *session.posargs, external=True, ) @nox.session(python=_py_versions) def testcov(session): session.install("-e", ".", "--group", "dev") cover_mpi = shutil.which("mpiexec") is not None if cover_mpi: session.install("-e", ".[mpi]") cov_file = str(pathlib.Path.cwd() / ".coverage") session.env["COVERAGE_FILE"] = cov_file session.run("coverage", "erase") base = ["coverage", "run", "--source=scinexus"] # mypy via API wrapper so coverage traces plugin hook execution session.run(*base, "scripts/run_mypy_cov.py", "--no-incremental", "src/scinexus/") # doctests session.chdir("src/scinexus") session.run(*base, "--append", "-m", "pytest", "-s", "-x", "--doctest-modules", ".") # unit tests session.chdir("../../tests") session.run( *base, "--append", "-m", "pytest", "-s", "-x", "-m", "not mpi", ) # MPI tests when mpiexec is available if cover_mpi: py = pathlib.Path(session.bin_paths[0]) / "python" session.run( "mpiexec", "--oversubscribe", "-n", "4", str(py), "-m", "mpi4py.futures", "-m", *base, "--append", "-m", "pytest", "-s", "-x", "-m", "mpi", external=True, ) session.chdir("..") session.run("coverage", "report") i = 0 while i < len(session.posargs): fmt = session.posargs[i] if fmt == "html": session.run("coverage", fmt, external=True) i += 1 continue o_name = session.posargs[i + 1] session.run("coverage", fmt, o_name, external=True) i += 2 @nox.session(python=_py_versions) def test_docs(session): session.install("-e", ".", "--group", "dev", "--group", "doc") session.run("uv", "pip", "list") # doctest modules within scinexus session.chdir("docs") session.run( "pytest", "--markdown-docs", "-m", "markdown-docs", "-x", ".", "--ignore", "scripts", *session.posargs, ) # the skill's runnable markdown examples, which nothing else runs. The # blocks marked notest are fragments and stay unrun, and llms.txt is not # collected at all: pytest-markdown-docs takes .md only. posargs are not # passed on, they name paths relative to docs/ session.chdir("..") session.run( "pytest", "--markdown-docs", "-m", "markdown-docs", "-x", "skills", ) cogent3-scinexus-b16bc06/overrides/000077500000000000000000000000001525761402500173315ustar00rootroot00000000000000cogent3-scinexus-b16bc06/overrides/main.html000066400000000000000000000007041525761402500211440ustar00rootroot00000000000000{% extends "base.html" %} {# The dismiss button keys on a hash of this block's rendered HTML, so a link whose href varies with page depth makes the banner reappear on every directory change. Keep the content identical on every page. #} {% block announce %} scinexus now runs on free-threaded Python 3.14t — the parallel backend defaults to threads there. See the “Run in parallel” how-to. {% endblock %} cogent3-scinexus-b16bc06/pyproject.toml000066400000000000000000000053561525761402500202540ustar00rootroot00000000000000[project] name = "scinexus" dynamic = ["version"] description = "A composable app infrastructure for scientific computing" requires-python = ">=3.11" license = "BSD-3-Clause" readme = "README.md" readme_renderer = "markdown" dependencies = [ "charset_normalizer", "citeable", "scitrack", "tqdm", "typeguard", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Topic :: System :: Distributed Computing", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Free Threading :: 3 - Stable", ] [project.urls] Documentation = "https://scinexus.readthedocs.io" "Bug Tracker" = "https://github.com/cogent3/scinexus/issues" "Source Code" = "https://github.com/cogent3/scinexus/" [project.optional-dependencies] loky = ["loky"] mpi = ["mpi4py"] rich = ["rich"] [dependency-groups] dev = [ "cogent3>=2026.4.20a0", "pytest", "pytest-cov", "pytest-xdist", "mypy", "nox", "numpy", "rich", "ruff", "scriv", ] doc = [ "cogent3[extra]>=2026.4.20a0", "pymdown-extensions>=10.21.2", "zensical>=0.0.59", "mkdocstrings-python>=2.0.3", "cogapp>=3.6.0", "click>=8.3.2", "pytest-markdown-docs", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.version] path = "src/scinexus/_version.py" [tool.hatch.build.targets.wheel] packages = ["src/scinexus"] [tool.pytest.ini_options] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "mpi: marks tests requiring MPI (run with nox -s testmpi)", "internet: marks tests requiring internet access", "free_threaded: marks tests of thread-based parallelism, which run on any build (select with '-m free_threaded')", ] testpaths = ["tests"] [tool.mypy] plugins = ["scinexus._mypy_plugin"] [tool.scriv] format = "md" md_header_level = "2" categories = [ "API", "Contributors", "Enhancements", "Bug fixes", "Documentation", "Deprecations", "Discontinued", ] output_file = "changelog.md" version = "literal: src/scinexus/_version.py: __version__" skip_fragments = "README.*" new_fragment_template = "file: changelog.d/templates/new.md.j2" entry_title_template = "file: changelog.d/templates/title.md.j2" [tool.uv.pip] exclude-newer = "7 days" [tool.uv.pip.exclude-newer-package] # but we allow other cogent3 packages to be more recent citeable = false cogent3-scinexus-b16bc06/rtd_get_docs.py000066400000000000000000000111601525761402500203400ustar00rootroot00000000000000# this file # is directly used by .readthedocs.yaml # it extracts the built docs from the github artefact # created by the build_docs.yml github action import os import pathlib import time import zipfile import requests MAX_WAIT_TIME = 600 # Typically takes 5.5 minutes POLL_INTERVAL = 20 def get_rtd_version_name() -> str: return os.environ.get("READTHEDOCS_VERSION_NAME", "").lower() def get_github_token() -> str: token = os.environ.get("GITHUB_TOKEN") if token is None: token = os.environ.get("GITHUB_TOKEN_PRIVATE") if not token: msg = "GitHub token not found." raise OSError(msg) return token def get_latest_run(workflow_filename: str, headers: dict) -> dict: url = f"https://api.github.com/repos/cogent3/scinexus/actions/workflows/{workflow_filename}/runs" response = requests.get(url, headers=headers, timeout=10) # Check if we got a successful response before trying to parse JSON if not response.ok: msg = f"GitHub API request failed with status {response.status_code}: {response.text[:200]}" raise RuntimeError(msg) try: data = response.json() except requests.exceptions.JSONDecodeError as e: msg = f"Failed to parse JSON response. Status: {response.status_code}, Content: {response.text[:200]}" raise RuntimeError(msg) from e runs = data.get("workflow_runs", []) if not runs: msg = f"No workflow runs found for: '{workflow_filename}'" raise ValueError(msg) return runs[0] def wait_for_run_completion(run: dict, headers: dict) -> dict: run_id = run["id"] run_url = f"https://api.github.com/repos/cogent3/scinexus/actions/runs/{run_id}" waited = 0 while waited < MAX_WAIT_TIME: response = requests.get(run_url, headers=headers, timeout=10) if not response.ok: msg = f"GitHub API request failed with status {response.status_code}: {response.text[:200]}" raise RuntimeError(msg) try: run_status = response.json() except requests.exceptions.JSONDecodeError as e: msg = f"Failed to parse JSON response. Status: {response.status_code}, Content: {response.text[:200]}" raise RuntimeError(msg) from e status = run_status["status"] if status == "completed": conclusion = run_status["conclusion"] if conclusion != "success": msg = f"Latest workflow run failed with conclusion: '{conclusion}'" raise RuntimeError(msg) return run_status time.sleep(POLL_INTERVAL) waited += POLL_INTERVAL msg = "Timed out waiting for workflow run to complete." raise TimeoutError(msg) def download_and_extract_artifact(run: dict, headers: dict) -> None: artifact_name = "scinexus-docs-html" response = download_extract_artifact( run, "artifacts_url", headers, "GitHub API request failed with status " ) try: artifacts_data = response.json() except requests.exceptions.JSONDecodeError as e: msg = f"Failed to parse JSON response. Status: {response.status_code}, Content: {response.text[:200]}" raise RuntimeError(msg) from e artifacts = artifacts_data.get("artifacts", []) artifact = next((a for a in artifacts if a["name"] == artifact_name), None) if artifact is None: msg = f"Artifact '{artifact_name}' not found in the run." raise ValueError(msg) response = download_extract_artifact( artifact, "archive_download_url", headers, "Artifact download failed with status ", ) out = pathlib.Path(f"{artifact_name}.zip") out.write_bytes(response.content) with zipfile.ZipFile(out, "r") as zip_ref: zip_ref.extractall("_readthedocs/html/") out.unlink() def download_extract_artifact(arg0, arg1, headers, arg3): artifacts_url = arg0[arg1] result = requests.get(artifacts_url, headers=headers, timeout=10) if not result.ok: msg = f"{arg3}{result.status_code}: {result.text[:200]}" raise RuntimeError(msg) return result def download_and_extract_docs() -> None: version = get_rtd_version_name() if version not in ("latest", "stable"): msg = f"Unexpected version '{version}' for readthedocs." raise ValueError(msg) workflow_filename = "docs.yml" headers = {"Authorization": f"token {get_github_token()}"} latest_run = get_latest_run(workflow_filename, headers) completed_run = wait_for_run_completion(latest_run, headers) download_and_extract_artifact(completed_run, headers) if __name__ == "__main__": download_and_extract_docs() cogent3-scinexus-b16bc06/ruff.toml000066400000000000000000000070171525761402500171730ustar00rootroot00000000000000exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", "working", ] extend-exclude = ["rtd_get_docs.py"] line-length = 88 indent-width = 4 target-version = "py311" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. select = ["ALL"] # ICN001 not adhering to numpy as np convention due to # name collision, where np is used as num params # N801 CamelCase is not always appropriate # PLR0913 number of arguments sometimes needs to be > 5 # FBT001 and FBT002, positional boolean arguments are allowed! # PT011, I disagree about checking error messages as well # as exception types as it doubles the "cost" of fixing typos ignore = [ "COM812", "EXE002", "FA100", "E501", "D", "N801", "ICN001", "PLR0913", "FBT001", "FBT002", "PT011", "S311", # Allow use of random "S608", # sql injection unlikely "PLR2004", # Magic number usage where appropriate "PLC0415", # Allow delayed imports ] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [lint.per-file-ignores] "tests/**/*.py" = [ "S101", # asserts allowed in tests... "INP001", # __init__.py files are not required... "ANN", "N802", "N803", "S301", # we allow pickling / unpickling in tests "SLF001", # Allow accessing private attrs ] "noxfile.py" = [ "S101", # asserts allowed in tests... "INP001", # __init__.py files are not required... "ANN", "N802", "N803", ] "skills/conftest.py" = [ "INP001", # pytest conftest, not an importable package ] "scripts/publish_skills.py" = [ "INP001", # a script, not an importable package ] "src/scinexus/composable.py" = [ "ANN401", # Allow use of Any ] "src/scinexus/deserialise.py" = [ "ANN401", # Allow use of Any ] "src/scinexus/data_store.py" = [ "ANN401", # Allow use of Any "SLF001", # Allow accessing private attrs ] "src/scinexus/io_util.py" = [ "ANN401", # Allow use of Any ] "src/scinexus/misc.py" = [ "ANN401", # Allow use of Any ] "src/scinexus/io.py" = [ "ANN401", # Allow use of Any for generic serialisation apps ] "src/scinexus/arallel.py" = [ "ANN401", # Allow use of Any for generic serialisation apps ] "src/scinexus/progress.py" = [ "ANN401", # Allow use of Any for generic serialisation apps "SLF001", # Allow accessing private attrs ] "src/scinexus/typing.py" = [ "ANN401", # Allow use of Any ] [format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "lf" docstring-code-format = true # Set the line length limit used when formatting code snippets in # docstrings. # # This only has an effect when the `docstring-code-format` setting is # enabled. docstring-code-line-length = "dynamic" cogent3-scinexus-b16bc06/scripts/000077500000000000000000000000001525761402500170165ustar00rootroot00000000000000cogent3-scinexus-b16bc06/scripts/publish_skills.py000066400000000000000000000133741525761402500224270ustar00rootroot00000000000000"""publish the agent skills in skills/ into a built documentation site Serves them at the paths the great-docs agent-skills standard specifies, so that ``npx skills add `` and anything else following that standard can discover them. https://posit-dev.github.io/great-docs/user-guide/agent-skills.html Run after the site is built, since zensical does not copy a hidden directory out of docs/: uv run python scripts/publish_skills.py site """ import argparse import json import shutil import sys from pathlib import Path REPO = Path(__file__).parent.parent SOURCE = REPO / "skills" WELL_KNOWN = Path(".well-known") / "agent-skills" # the path a consumer predating the agent-skills endpoint probes LEGACY = Path(".well-known") / "skills" / "default" # scaffolding for running the examples, which is not part of the skill EXCLUDED = {"conftest.py", "__pycache__", ".pytest_cache"} def read_frontmatter(path: Path) -> dict[str, str]: """return the YAML frontmatter of a SKILL.md as a dict of strings Understands the subset the standard uses: plain scalars, quoted scalars, and the folded or literal blocks its own example writes the description with. A nested map such as ``metadata`` is reported as a key with an empty value rather than being flattened into bogus top-level keys. """ lines = path.read_text(encoding="utf-8").splitlines() if not lines or lines[0].strip() != "---": msg = f"{path} has no frontmatter, which the standard requires" raise ValueError(msg) try: end = lines.index("---", 1) except ValueError: msg = f"{path} has no closing '---', so its frontmatter never ends" raise ValueError(msg) from None fields: dict[str, str] = {} key: str | None = None for line in lines[1:end]: indented = line.startswith((" ", "\t")) if indented and key is not None: # a continuation of a folded or literal block, or a member of a # nested map. Either way it belongs to the key above it fields[key] = f"{fields[key]} {line.strip()}".strip() continue if ":" not in line: continue key, _, value = line.partition(":") key = key.strip() value = value.strip() # "> " and "| " introduce a block whose content is on the lines below fields[key] = "" if value in {">", "|", ">-", "|-"} else value.strip("\"'") return fields def _publishable(source: Path) -> list[Path]: """the files of a skill, relative to it, in a stable order Walked rather than listed: a companion file may sit in a subdirectory, which is how the reference implementation ships references and scripts. """ found = [] for path in sorted(source.rglob("*")): relative = path.relative_to(source) if EXCLUDED.intersection(relative.parts): continue if path.is_file(): found.append(relative) return found def publish_skill(source: Path, site: Path) -> dict[str, object]: """copy one skill directory into site and return its manifest entry""" front = read_frontmatter(source / "SKILL.md") name = front.get("name", source.name) if name != source.name: msg = f"skill {source.name!r} declares name {name!r}, they must match" raise ValueError(msg) destination = site / WELL_KNOWN / name # removed first, so a file dropped from the skill stops being served shutil.rmtree(destination, ignore_errors=True) published = _publishable(source) for relative in published: target = destination / relative target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source / relative, target) entry: dict[str, object] = { "name": name, "description": front.get("description", ""), # relative to this manifest, so the skill is found wherever the site # is mounted. Read the Docs serves a versioned project under a prefix "files": [relative.as_posix() for relative in published], } for optional in ("license", "compatibility"): if optional in front: entry[optional] = front[optional] return entry def publish(site: Path) -> list[dict[str, object]]: """copy every skill into site and write the discovery manifest""" if not site.is_dir(): msg = f"{site} does not exist, build the site before publishing into it" raise SystemExit(msg) sources = sorted(p for p in SOURCE.iterdir() if (p / "SKILL.md").exists()) if not sources: msg = f"no skills found under {SOURCE}" raise SystemExit(msg) entries = [publish_skill(source, site) for source in sources] manifest = site / WELL_KNOWN / "index.json" manifest.write_text( json.dumps({"skills": entries}, indent=2) + "\n", encoding="utf-8" ) # the single-skill endpoints the standard also names, so that a site with # one skill answers the simplest fetch and the older probe as well if len(sources) == 1: legacy = site / LEGACY legacy.mkdir(parents=True, exist_ok=True) shutil.copy2(sources[0] / "SKILL.md", legacy / "SKILL.md") shutil.copy2(sources[0] / "SKILL.md", site / "skill.md") return entries def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("site", type=Path, help="the built site directory") args = parser.parse_args(argv) try: entries = publish(args.site) except ValueError as invalid: raise SystemExit(str(invalid)) from None for entry in entries: files = entry["files"] count = len(files) if isinstance(files, list) else 0 print(f"published {entry['name']}: {count} files") # noqa: T201 return 0 if __name__ == "__main__": sys.exit(main()) cogent3-scinexus-b16bc06/scripts/run_mypy_cov.py000066400000000000000000000003071525761402500221210ustar00rootroot00000000000000"""Run mypy via API so coverage can trace the plugin.""" import sys import mypy.api result = mypy.api.run(sys.argv[1:]) sys.stdout.write(result[0]) sys.stderr.write(result[1]) sys.exit(result[2]) cogent3-scinexus-b16bc06/skills/000077500000000000000000000000001525761402500166305ustar00rootroot00000000000000cogent3-scinexus-b16bc06/skills/conftest.py000066400000000000000000000015411525761402500210300ustar00rootroot00000000000000"""setup for running the skill's markdown code blocks under pytest-markdown-docs Excluded from what the docs site publishes, see scripts/publish_skills.py. """ import os from collections.abc import Iterator from pathlib import Path import pytest @pytest.fixture(autouse=True) def skill_scratch_dir(tmp_path: Path) -> Iterator[Path]: """run each block in a scratch directory holding the inputs it assumes The skeleton in SKILL.md reads a store at ``raw`` and writes one at ``out``, which the prose beside it states. Preparing that here keeps the block free of setup a reader does not need to see. """ (tmp_path / "raw").mkdir() (tmp_path / "raw" / "alpha.txt").write_text("contents of alpha", encoding="utf-8") previous = Path.cwd() os.chdir(tmp_path) try: yield tmp_path finally: os.chdir(previous) cogent3-scinexus-b16bc06/skills/scinexus/000077500000000000000000000000001525761402500204715ustar00rootroot00000000000000cogent3-scinexus-b16bc06/skills/scinexus/SKILL.md000066400000000000000000000120411525761402500216670ustar00rootroot00000000000000--- name: scinexus description: Build and debug scinexus pipelines and data stores - define_app, NotCompleted, open_data_store, apply_to, as_completed, composable app, loader and writer apps, sqlitedb stores, parallel backends, and the file helpers open_, atomic_write and compression handling. Use whenever code imports scinexus, composes apps with +, reads or writes a scinexus data store, or opens a file through scinexus. license: BSD-3-Clause compatibility: Requires Python >=3.11 and the scinexus package. --- # scinexus `llms.txt` beside this file is the API summary: what exists and what it is called. This file is for what a first attempt gets wrong. Read one of these only when the task reaches it: - `pipelines.md` - data stores, `apply_to` versus `as_completed`, resume, the four parallel backends, progress - `io.md` - `open_`, compression, `atomic_write`, streaming lines and records - `extending.md` - the hooks a package built on scinexus registers ## The shape of a pipeline A loader, a generic app, a writer, applied to a data store. Most wrong-shaped code is a variation on getting this wrong. ```python from scinexus import NotCompleted, define_app, open_data_store from scinexus.data_store import DataMember @define_app(app_type="loader") class load_text: def main(self, member: DataMember) -> str: return member.read() @define_app def shout(text: str, end: str = "!") -> str: return text.upper() + end @define_app(app_type="writer") class save_text: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: if isinstance(data, NotCompleted): self.data_store.drop_not_completed(unique_id=identifier) return self.data_store.write_not_completed( unique_id=identifier, data=data.to_json() ) return self.data_store.write(unique_id=identifier, data=data) in_dstore = open_data_store("raw", suffix="txt") out_dstore = open_data_store("out", suffix="txt", mode="w") app = load_text() + shout(end="?") + save_text(out_dstore) app.apply_to(in_dstore) out_dstore.close() ``` Applied to `raw/alpha.txt` holding `contents of alpha`, that writes `out/alpha.txt` holding `CONTENTS OF ALPHA?`. ## What to get right 1. **The type hints on `main()` are load-bearing.** The first parameter and the return must both be annotated or `define_app` raises. They are not enforced in the same way: `typeguard` checks the incoming value against the first parameter on every call, while the return hint is used only to decide whether `+` is allowed. An app annotated `-> str` that returns `42` returns `42`. `from __future__ import annotations` anywhere in the module turns every hint into a string and `define_app` raises `NotImplementedError`. 2. **A function app takes one data argument, first.** Every later parameter is constructor configuration, so `shout(end="?")` configures and `shout(end="?")("hi")` calls. A class app puts configuration in `__init__` for the same reason. 3. **Return `NotCompleted` for an expected failure rather than raising.** In a loader or a generic app an uncaught exception is caught and becomes a `NotCompleted` of type `ERROR`, so the choice is which kind of record you get rather than whether the run survives. Returning `None` is never right: it becomes a record of type `BUG`. A **writer** has no such net - `apply_to` calls its `main` directly, so an exception there ends the run with no log and no citations written. 4. **A writer's `main` always receives `NotCompleted` values.** `skip_not_completed` is forced off for writers, so the flag cannot turn this off. Handle them the way the skeleton does, which is the shape the `llms.txt` writer example also has. Dropping the stale record first matters on a resume of a directory store, which retries a failed input on the next run and then cannot rewrite its record in `mode="a"`. A SQLite store does the opposite and never retries a failure. See `pipelines.md`. 5. **Nothing already in the store is recomputed, whatever the mode.** `mode="w"` creates the store, it does not empty it, and `apply_to` skips every input whose identifier is already there. So editing an app and re-running it over the same output store writes nothing and reports no error - point it at a new path, or delete the old one. A directory store goes further and refuses even a direct `write()` of a record it holds, returning `None`, which the framework turns into a `BUG` record. 6. **Encoding is chosen at `open_data_store()`, never at `write()`.** The store owns the suffix and the compression, the writer supplies the bare stem. Into a `suffix="fasta"` store, `write(unique_id="brca1.fa")` stores `brca1.fasta`, and `write(unique_id="brca1.fasta.gz")` raises `ValueError` rather than renaming. 7. **A writable SQLite store holds a lock until `close()`.** Opening a second writable handle to the same file is silent, and then the first operation through it - a `write()`, or a read such as `.describe` - raises `OSError` naming the session that holds the lock. There is no context manager: call `close()`. cogent3-scinexus-b16bc06/skills/scinexus/extending.md000066400000000000000000000133521525761402500230040ustar00rootroot00000000000000# Extending scinexus from a package built on it Four registration points and one base class. Each is process-local: a worker process does not inherit a registration, so register at import time of your package rather than inside a function that only the master runs. ## Type names in hints `define_app` resolves the hints on `main()` against the module the app was defined in. A bare string hint is refused outright with `NotImplementedError`, but a forward reference inside a container or a `TypeVar` bound - `list["Genome"]` - is resolved by name, and a name that module does not import gives `TypeError: cannot resolve type name 'Genome'`. `register_type_namespace` supplies a fallback for downstream users, who write hints naming your types without importing them: ```python { notest } from scinexus.typing import register_type_namespace def _my_types() -> dict[str, type]: from mypackage import Genome, Variant return {"Genome": Genome, "Variant": Variant} register_type_namespace(_my_types) ``` The provider is called lazily, each time a name needs resolving, so it can defer a heavy import - and it is responsible for its own caching. Registration is idempotent, providers are consulted in registration order, and the first to supply a name wins. Module globals are always checked first. For compatibility checking itself: a `Protocol` on either side makes the composition-time check pass and leaves the real check to `typeguard` at run time. `scinexus.typing` ships `HasSource`, `HasInfo` and `SerialisableType` (anything with `to_rich_dict`). ## How a record gets its name Every identifier in a store comes from this chain, and each step is replaceable: ``` input object -> get_data_source -> get_unique_id -> identifier ``` `get_data_source` is a `singledispatch`. Out of the box: a `str` or `Path` gives its file name, a `DataMember` gives its `unique_id`, a `dict` gives `info["source"]` or `source`, and anything else gives its `.source` attribute, followed recursively. Add a branch for your own type rather than replacing the function: ```python { notest } from scinexus.data_store import get_data_source @get_data_source.register def _(data: Genome) -> str | None: return data.accession ``` `get_unique_id` then strips format and compression suffixes, so `alpha.fasta.gz` becomes `alpha`. `set_id_from_source(func)` replaces the whole chain with `func(obj) -> str | None`. It is what `apply_to` and `as_completed` use by default, and what `NotCompleted` uses to normalise its `source=` argument. The per-call `id_from_source=` keyword still wins over it. `set_id_from_source(None)` restores the default, and `get_id_from_source()` returns whatever is active. The function travels to every worker of every process backend, so a lambda or a function defined inside another function raises `PicklingError` as soon as a parallel run starts - including under the default backend. Define it at module level. (The docstring on `set_id_from_source` says this is only a concern for `loky` and MPI, which is wrong in both directions.) ## Display of summaries Store summaries return plain data: `.describe` a `dict`, `.summary_not_completed` and the rest a `list[dict]`. `set_summary_display(func)` intercepts all of them, with `func(data, *, name)` where `name` is `"describe"`, `"summary_logs"`, `"summary_not_completed"`, `"summary_citations"` or `"validate"`. ```python { notest } from scinexus import set_summary_display def as_table(data, *, name): from cogent3 import make_table rows = [data] if isinstance(data, dict) else data return make_table(data=rows, title=name) set_summary_display(as_table) ``` That is how a domain package gives back the rendered tables scinexus deliberately does not depend on. `set_summary_display(None)` clears it. ## Other registration points `register_datastore_reader("myfmt")(MyStore)` in `scinexus.io` teaches `open_data_store` a new suffix. It dispatches on the suffix of a *file*: a directory called `results.myfmt` still goes to `DataStoreDirectory`, since `open_data_store` asks `is_dir()` first. `register_deserialiser("mypackage.module.MyClass")(func)` in `scinexus.deserialise` teaches `deserialise_object` how to inflate a `to_rich_dict` payload. Matching is by **substring** against the payload's `"type"` entry, in registration order, first hit wins - so a short key such as `"MyClass"` also captures `"OtherPackage.MyClassExtended"`. Register the fully qualified name. ## A custom parallel backend Subclass `Parallel` and implement five methods: `imap`, `as_completed`, `is_master_process`, `get_rank` and `get_size`. Pass an instance to `set_parallel_backend`, which accepts an instance as readily as one of the four names. ```python { notest } import multiprocessing from scinexus.parallel import Parallel, set_parallel_backend class RayBackend(Parallel): def imap(self, f, s, max_workers=None, **kwargs): ... def as_completed(self, f, s, max_workers=None, **kwargs): ... def get_rank(self): ... def get_size(self): ... def is_master_process(self): # implement this one first, and never let it return None return multiprocessing.parent_process() is None set_parallel_backend(RayBackend()) ``` `is_master_process` is the one with consequences beyond parallelism, and the first to get right. Data stores gate directory and schema creation on it, so a backend that answers falsely in the master creates nothing and the next write fails with a bare `FileNotFoundError` naming no backend at all. One that answers truthfully in a worker has every worker try to create the store. An instance set this way is never replaced. Left to choose for itself, scinexus re-reads the GIL state on every call, because importing an extension that does not declare free-threading support turns the GIL back on mid-process. cogent3-scinexus-b16bc06/skills/scinexus/io.md000066400000000000000000000100141525761402500214160ustar00rootroot00000000000000# Files: open_, atomic_write, streaming These are usable on their own, with no app and no data store. Import them from `scinexus.io_util`, except `open_`, which is also on the package. ## open_ `open_(filename, mode="rt", **kwargs)` opens a path or a URL and handles `.gz`, `.bz2`, `.xz`, `.lzma` and `.zip` by suffix. Two things differ from what the standard library does: - a bare `"r"` or `"w"` means **text**, for a compressed file as much as for a plain one. `gzip`, `bz2` and `lzma` take a bare mode as binary, and `open_` does not. Use `"rb"` / `"wb"` for bytes. This applies to `"r"` and `"w"` alone: `"a"` is still binary for the compressed formats, and neither `"a"` nor `"x"` works on a zip at all - on a text read with no `encoding=` given, the encoding is sniffed from the first hundred bytes with `charset_normalizer` rather than assumed to be the locale default ```python import gzip import tempfile from pathlib import Path from scinexus import open_ path = Path(tempfile.mkdtemp()) / "data.txt.gz" with gzip.open(path, "wt") as out: out.write("one\ntwo\n") with open_(path) as infile: assert infile.read() == "one\ntwo\n" # str, not bytes ``` A URL is detected by `is_url` (`http`, `https` and `file` schemes) and handed to `open_url`, which decompresses by suffix and takes the charset from the response headers. `get_format_suffixes("data.txt.gz")` returns `("txt", "gz")`, the pair `open_` dispatches on. `path_exists(p)` is false for a URL and for anything that is not there. `open_zip` handles a single-member archive: a read of one holding more than one record raises `ValueError`, and a text read falls back to `latin-1`, which decodes any byte. ## atomic_write Writes to a temporary file and renames it into place on success, so a reader never sees a half-written file and a failure leaves nothing behind. ```python import tempfile from pathlib import Path from scinexus.io_util import atomic_write target = Path(tempfile.mkdtemp()) / "result.txt" try: with atomic_write(target) as out: out.write("partial") raise RuntimeError("something went wrong") except RuntimeError: pass assert not target.exists() # and no temporary left either ``` It takes `mode`, `encoding`, `tmpdir`, and `in_zip` for writing a member into a zip archive. Further arguments for the underlying open go in `open_kwargs=` as a dict, so that a name like `tmpdir` cannot bind to the wrong parameter. ## Streaming a large file Three functions, all reading in bounded chunks rather than loading the file. | function | yields | default chunk | | --- | --- | --- | | `iter_splitlines(path, chunk_size, *, as_bytes=False)` | one line at a time | 1 MB | | `iter_line_blocks(path, num_lines=1000, chunk_size, *, as_bytes=False)` | lists of `num_lines` lines | 5 MB | | `iter_record_chunks(*, path, delimiter, chunk_size)` | bytes between delimiters | 5 MB | `chunk_size` counts bytes read in one go, in both modes, and for a compressed file it counts bytes coming out of the decompression - the memory the read costs, not the size on disk. A trailing empty line is never yielded, so a file ending in a newline gives the same lines as one that does not. The bound does not hold for a URL. All three set `chunk_size = None` for one and read the whole response in a single call, so streaming a large remote file this way costs its full size in memory. The two modes of `iter_splitlines` do not always agree on how many lines a file has. `str.splitlines()` breaks on eleven characters, `bytes.splitlines()` on three. For a file holding `b"a\x0bb\nc\n"`: ``` as_bytes=False -> ['a', 'b', 'c'] as_bytes=True -> [b'a\x0bb', b'c'] ``` If the file might hold a vertical tab, a form feed or a Unicode line separator, read it as bytes and decode yourself. `iter_record_chunks` is keyword-only, takes a `bytes` delimiter and yields `bytes`. The first item is whatever precedes the first delimiter, which for a file that starts with one is empty: for a FASTA file split on `b">"` the items are `b""`, `b"a\nACGT\n"`, `b"b\nTTTT\n"`. Filter that first item yourself. cogent3-scinexus-b16bc06/skills/scinexus/llms.txt000066400000000000000000000156741525761402500222160ustar00rootroot00000000000000# scinexus > A composable app infrastructure for scientific computing scinexus (pronounced "sigh-nexus") is a Python framework for building composable, type-checked data processing pipelines. What dataclasses and attrs are for structured data, scinexus apps are for structured algorithms. It enables interoperability between apps through defined data types, supporting scientific domain app ecosystems. ## Core Concepts ### define_app The `@define_app` decorator transforms a class (with a `main()` method) or a function into a composable app. Apps are callable and compose with `+`. ```python from scinexus import define_app @define_app class upper: def main(self, data: str) -> str: return data.upper() result = upper()("hello") # "HELLO" ``` Function-based apps are also supported: ```python @define_app def double(val: int) -> int: return val * 2 ``` ### App Types - `GENERIC` (default) -- general-purpose processing step - `LOADER` -- must be first in a composed pipeline - `WRITER` -- must be last in a composed pipeline; writes results to a data store - `NON_COMPOSABLE` -- cannot participate in `+` composition Set via `@define_app(app_type="loader")` etc. ### App Composition Apps compose with `+`. Type compatibility between the return type of the left app and the input type of the right app is checked at composition time. ```python @define_app def add_one(x: int) -> int: return x + 1 @define_app def to_str(x: int) -> str: return str(x) pipeline = add_one() + to_str() pipeline(5) # "6" ``` Ordering rules: LOADER must be first, WRITER must be last, GENERIC can go anywhere in between. ### NotCompleted A sentinel return type for failed computations. It propagates through pipelines without raising exceptions, enabling durable computing. ```python from scinexus import NotCompleted nc = NotCompleted("ERROR", "my_app", "something went wrong", source="input.txt") bool(nc) # False -- so it is falsy ``` If an app's `main()` raises an exception, the framework catches it and returns a `NotCompleted` instance. If an app receives a `NotCompleted` as input (and `skip_not_completed=True`, the default), it passes it through unchanged. A writer is the exception on both counts. `skip_not_completed` is forced off for a writer and `apply_to` calls its `main()` directly, so a writer always receives `NotCompleted` values and must handle them. An exception raised in a writer's `main()` is not caught either: it ends the run. ## Data Stores Data stores provide checkpointed, append-only storage for pipeline results. ```python from scinexus import open_data_store # Directory-based dstore = open_data_store("results/", suffix="json", mode="w") # SQLite-based dstore = open_data_store("results.sqlitedb", mode="w") # Read-only from a zip dstore = open_data_store("results.zip") ``` Key features: - `.describe` -- summary of stored data - `.summary_not_completed` -- summary of failed results - Completed members accessed via `.completed` - Membership testing with `"identifier" in dstore` - Logs and citations are stored alongside data ## Writer Apps and apply_to Writer apps process an entire data store, with logging, checkpointing, and progress tracking: ```python @define_app(app_type="writer") class save_result: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str) -> str: if isinstance(data, NotCompleted): # a writer always receives these, so it must handle them self.data_store.drop_not_completed(unique_id=identifier) self.data_store.write_not_completed( unique_id=identifier, data=data.to_json() ) else: self.data_store.write(unique_id=identifier, data=data) return identifier pipeline = add_one() + to_str() + save_result(dstore) pipeline.apply_to(input_dstore, parallel=True, show_progress=True) ``` ## Parallel Execution Four backends are available: ```python from scinexus import set_parallel_backend, get_parallel_backend set_parallel_backend("multiprocess") # stdlib multiprocessing set_parallel_backend("threads") # stdlib ThreadPoolExecutor set_parallel_backend("loky") # recommended for Jupyter set_parallel_backend("mpi") # for HPC clusters via mpi4py ``` With no call to `set_parallel_backend`, the backend is `"threads"` when the GIL is not in force and the caller is not itself a pool worker, and `"multiprocess"` otherwise. The process backends pickle the app and its arguments, giving each worker its own copy. The `"threads"` backend shares the app instance between workers instead, so a `main()` that mutates `self` or module-level state is a data race there. Use `parallel=True` in `apply_to()` or `as_completed()` to enable parallel execution. Configure workers via `par_kw={"max_workers": 4}`. ## Progress Tracking ```python from scinexus import set_progress_backend set_progress_backend("tqdm") # default set_progress_backend("rich") # requires rich extra ``` Pass `show_progress=True` to `apply_to()` or `as_completed()`. ## Installation ``` pip install scinexus ``` Optional extras: - `pip install scinexus[loky]` -- process pool for Jupyter - `pip install scinexus[rich]` -- rich progress bars - `pip install scinexus[mpi]` -- MPI support via mpi4py Requires Python 3.11+. ## Type System scinexus checks type compatibility when composing apps with `+`. The return type of the left app must overlap with the input type of the right app. Standard typing constructs (`Union`, `Optional`, protocols) are supported. ## Public API Direct imports from `scinexus`: - `define_app` -- decorator for creating apps - `NotCompleted`, `NotCompletedType` -- failure sentinel - `is_app`, `is_app_composable` -- introspection helpers - `AppBase`, `ComposableApp`, `LoaderApp`, `WriterApp`, `NonComposableApp` -- base classes for inheritance-based app definition - `Progress`, `ProgressContext`, `get_progress`, `set_progress_backend` -- progress tracking - `__version__` -- package version Lazy imports from `scinexus`: - `open_data_store` -- create/open data stores - `open_` -- open files with format detection - `set_parallel_backend`, `get_parallel_backend` -- parallel configuration - `set_summary_display`, `get_summary_display` -- customise data store summary output - `set_id_from_source`, `get_id_from_source` -- customise unique identifier extraction ## Documentation - How-to guides: writing function apps, writing class apps, composing apps, using data stores, handling failures, running in parallel, tracking progress, logging and citations, extending the type system, customising display, migrating from cogent3 - Tutorials: composing apps, processing a dataset - Explanations: why composable apps, app lifecycle, type system, data store model, NotCompleted design, source tracking, customisation hooks, control flow ## Links - Documentation: https://scinexus.readthedocs.io - Source: https://github.com/cogent3/scinexus - Bug tracker: https://github.com/cogent3/scinexus/issues cogent3-scinexus-b16bc06/skills/scinexus/pipelines.md000066400000000000000000000172711525761402500230130ustar00rootroot00000000000000# Pipelines: stores, apply_to, parallel, progress ## Opening a store `open_data_store(path, suffix=None, limit=None, mode="r")` picks the backend from the path. | path | backend | notes | | --- | --- | --- | | a directory | `DataStoreDirectory` | `suffix` required, and cannot be just `"*"` | | a zip archive | `ReadOnlyDataStoreZipped` | `suffix` required here too, and any mode but `"r"` raises `ValueError` | | `*.sqlitedb` | `DataStoreSqlite` | `suffix` is ignored, `limit` is refused unless `mode="r"` | | `":memory:"` | `DataStoreSqlite` | needs `mode="w"`, the read-only case raises `NotImplementedError` | A missing `suffix` on a zip reports `suffix is required for DataStoreDirectory`, naming a class you are not using. It means the zip. `mode` is `"r"`, `"w"` or `"a"`, and none of them empties an existing store. What happens to a record already there depends on the backend and on which kind of record it is: | | directory store | SQLite store | | --- | --- | --- | | `mode="w"`, completed record | the write is skipped, `write()` returns `None`, old content stays | replaced | | `mode="w"`, not-completed record | replaced | replaced | | `mode="a"`, either kind | `OSError` | `OSError` | | `mode="r"` | `OSError` | `OSError` | To recompute a store from scratch, delete it or write to a new path. A directory store keeps completed records at the top level, failures as json under `not_completed/`, logs under `logs/`, checksums under `md5/` and citations in `bibliography.citations`. A SQLite store keeps all of that in one file. Every store answers `len()`, `store[0]`, `"alpha" in store` and iteration. A member has `.unique_id`, `.read()` and `.md5`. The summaries - `.describe`, `.summary_not_completed`, `.summary_logs`, `.summary_citations`, `.validate()` - return a `dict` or a `list[dict]`, plain data rather than a formatted table. `set_summary_display` changes that, see `extending.md`. Member identifiers are spelled with `/` on every platform, so a not-completed member is `not_completed/alpha.json` on Windows too. Never build one by joining with `Path`. ## apply_to and as_completed `apply_to` belongs to a writer and needs a composed input. It returns the output store. ```python { notest } out = (loader() + process() + writer(out_store)).apply_to( in_store, parallel=True, par_kw={"max_workers": 4}, show_progress=True ) ``` By default it also writes a `scitrack` log into the store recording the composed app, the package versions, the output identifiers and their md5 sums. `logger=False` turns that off, and a `CachingLogger` of your own directs it. Citations are separate and unconditional: every app carrying a `cite=` is written to the store whatever `logger` says. Provenance is a property of `apply_to`, so a pipeline driven any other way records none of it. Failures worth knowing about, all raised before any work is done: - two inputs reducing to one identifier, or an input yielding none, raise `ValueError` - a writer with nothing composed in front of it raises `RuntimeError` - an empty `dstore` raises `ValueError` `as_completed` is the same traversal without a writer. It yields in completion order only when `parallel=True`; run serially it preserves the order the inputs came in. ```python { notest } for result in (loader() + process()).as_completed(in_store, parallel=True): ... ``` The surprise there: a result that does not carry its own origin comes back wrapped in a `source_proxy`, so the identity of the input is not lost. The proxy forwards attribute access, `len`, `bool`, `repr`, `str` and `==`, and nothing else - a pipeline returning an `int` yields a proxy that raises `TypeError` on `result + 1`. Its `__hash__` is its own, so `proxy == 16` is `True` while `proxy in {16}` is `False`: putting results straight into a set or a dict does not work. Unwrap with `result.obj`, and read the input it came from with `result.source`. ## Resuming `apply_to` skips any input whose identifier is already in the output store, so an interrupted run resumes by pointing the same pipeline at the same store in `mode="a"`. What counts as "already there" differs by backend, and it decides whether a failure is retried: - a directory store names a failure `not_completed/alpha.json`, which does not match the input identifier `alpha`, so the input is retried. If it fails again the writer must drop the stale record before writing the new one, or the append-only check raises `OSError` - a SQLite store names it `alpha`, which does match, so the input is not retried at all. After fixing whatever caused a failure, `store.drop_not_completed(unique_id="alpha")` or the rerun does nothing ## Parallel `parallel=True` on `apply_to` or `as_completed` runs everything up to the writer in workers. The writer itself always runs in the calling process, so the *output* store is never touched by a worker. The input store does cross: a `DataMember` carries a reference to the store it came from. | backend | what crosses to a worker | watch out for | | --- | --- | --- | | `"multiprocess"` | app and inputs pickled, spawn start method | needs `if __name__ == "__main__":`, no lambdas or closures | | `"threads"` | nothing, the app instance is shared | a `main()` that mutates anything shared | | `"loky"` | as multiprocess, but through cloudpickle | an app defined in `__main__`, which includes a notebook cell, fails to deserialise in the worker. Define apps in an importable module | | `"mpi"` | pickled, to ranks that already exist | launching under `mpiexec`, see below | `par_kw` reaches the backend, and two of its keys do less than they look: - `max_workers` is **clamped**, not refused: `apply_to` and `as_completed` only ever use the pool's `as_completed`, which takes the CPU count as a ceiling. A value of 6000 on a six-core machine runs on six. Only a value below one raises - `chunksize` is validated and then ignored, for the same reason: only `imap` chunks, and nothing in the app machinery calls it - `if_serial` reaches the MPI backend only if you also pass `use_mpi=True`. Under `set_parallel_backend("mpi")` it is dropped and the default `"raise"` applies Anything you pass as `id_from_source` must be picklable under every process backend, so a lambda or a local function raises `PicklingError` before any work starts. Because an input that yields a source-less result is kept alive by its proxy, a worker pickles that input back with the result. For a small result over a large input that is the difference between tens of bytes and megabytes per item. It does not arise for results carrying their own `.source`, which are returned unwrapped. ## MPI MPI is not a fourth pool. The ranks exist before Python starts and nothing in the program can change them. ```bash mpiexec -n 8 python -m mpi4py.futures my_script.py ``` Rank 0 is the master and does no work, so `-n 8` gives seven workers and `-n 3` is the smallest launch with more than one. A single worker is serial execution, which `if_serial` refuses by default with a `RuntimeError` rather than running slowly. Do not set `max_workers`. The count comes from `mpiexec -n`, and a value that disagrees is warned about and ignored. The main logic must sit behind `if __name__ == "__main__":`, as it must under spawn. ## Progress `show_progress` takes `True`, `False` or a `Progress` instance. ```python from scinexus import get_progress, set_progress_backend set_progress_backend("tqdm") progress = get_progress(True) for _ in progress(range(3), msg="working"): pass ``` `parent.child()` gives a nested bar. For work that reports a fraction rather than iterating, `progress.context(msg="phase", start=0.0, end=0.5)` is a context manager with `update(progress=0.3)`. Note that it creates a **separate** bar of its own: `start` and `end` map the fraction onto that slice of that bar, not onto the parent. cogent3-scinexus-b16bc06/src/000077500000000000000000000000001525761402500161165ustar00rootroot00000000000000cogent3-scinexus-b16bc06/src/scinexus/000077500000000000000000000000001525761402500177575ustar00rootroot00000000000000cogent3-scinexus-b16bc06/src/scinexus/__init__.py000066400000000000000000000037141525761402500220750ustar00rootroot00000000000000"""scinexus: composable app infrastructure for scientific computing.""" import typing as _typing from scinexus._version import __version__ from scinexus.composable import ( AppBase, AppType, ComposableApp, LoaderApp, NonComposableApp, NotCompleted, NotCompletedType, WriterApp, define_app, is_app, is_app_composable, ) from scinexus.progress import ( Progress, ProgressContext, get_progress, set_progress_backend, ) if _typing.TYPE_CHECKING: # pragma: no cover from typing import Any __all__ = [ "AppBase", "AppType", "ComposableApp", "LoaderApp", "NonComposableApp", "NotCompleted", "NotCompletedType", "Progress", "ProgressContext", "WriterApp", "__version__", "define_app", "get_id_from_source", "get_parallel_backend", "get_progress", "get_summary_display", "is_app", "is_app_composable", "open_", "open_data_store", "set_id_from_source", "set_parallel_backend", "set_progress_backend", "set_summary_display", ] _LAZY_IMPORTS: dict[str, tuple[str, str]] = { "open_": ("scinexus.io_util", "open_"), "open_data_store": ("scinexus.io", "open_data_store"), "set_parallel_backend": ("scinexus.parallel", "set_parallel_backend"), "get_parallel_backend": ("scinexus.parallel", "get_parallel_backend"), "set_summary_display": ("scinexus.data_store", "set_summary_display"), "get_summary_display": ("scinexus.data_store", "get_summary_display"), "set_id_from_source": ("scinexus.data_store", "set_id_from_source"), "get_id_from_source": ("scinexus.data_store", "get_id_from_source"), } def __getattr__(name: str) -> "Any": if name in _LAZY_IMPORTS: module_path, attr = _LAZY_IMPORTS[name] import importlib mod = importlib.import_module(module_path) return getattr(mod, attr) msg = f"module {__name__!r} has no attribute {name!r}" raise AttributeError(msg) cogent3-scinexus-b16bc06/src/scinexus/_mypy_plugin.py000066400000000000000000000064301525761402500230470ustar00rootroot00000000000000"""Mypy plugin for scinexus.composable.define_app. Reads the ``main()`` method signature on decorated classes and synthesises the correct ``__call__`` return type so that ``reveal_type(app(x))`` works. """ from __future__ import annotations from collections.abc import Callable from mypy.nodes import ARG_POS, ARG_STAR, ARG_STAR2, Argument, Var from mypy.plugin import ClassDefContext, Plugin from mypy.plugins.common import add_method_to_class from mypy.types import AnyType, TypeOfAny, UnionType from mypy.types import Type as MypyType DEFINE_APP_FULLNAME = "scinexus.composable.define_app" def _get_main_return_type(ctx: ClassDefContext) -> MypyType | None: """Extract the return type from the ``main()`` method.""" info = ctx.cls.info main_sym = info.names.get("main") if main_sym is None or main_sym.node is None: return None main_type = main_sym.node.type # type: ignore[attr-defined] if main_type is None: return None return main_type.ret_type def _define_app_hook(ctx: ClassDefContext) -> bool: """Add __call__ and __add__ to @define_app-decorated classes.""" ret = _get_main_return_type(ctx) if ret is None: return True from mypy.types import Instance # Build NotCompleted union type not_completed_type: MypyType = AnyType(TypeOfAny.special_form) nc_info = ctx.api.lookup_fully_qualified_or_none("scinexus.composable.NotCompleted") if nc_info and nc_info.node: not_completed_type = Instance(nc_info.node, []) # type: ignore[arg-type] return_type = UnionType([ret, not_completed_type]) val_arg = Argument( Var("val", AnyType(TypeOfAny.explicit)), AnyType(TypeOfAny.explicit), None, ARG_POS, ) args_arg = Argument( Var("args", AnyType(TypeOfAny.explicit)), AnyType(TypeOfAny.explicit), None, ARG_STAR, ) kwargs_arg = Argument( Var("kwargs", AnyType(TypeOfAny.explicit)), AnyType(TypeOfAny.explicit), None, ARG_STAR2, ) add_method_to_class( ctx.api, ctx.cls, "__call__", [val_arg, args_arg, kwargs_arg], return_type, ) # Add ComposableApp as a base class so instances are recognised as # composable by mypy (the runtime decorator rebuilds the class with # ComposableApp as a base via types.new_class). composable_sym = ctx.api.lookup_fully_qualified_or_none( "scinexus.composable.ComposableApp" ) if composable_sym and composable_sym.node: any_type = AnyType(TypeOfAny.explicit) base_type = Instance(composable_sym.node, [any_type, any_type]) # type: ignore[arg-type] if not any( isinstance(b, Instance) and b.type.fullname == "scinexus.composable.ComposableApp" for b in ctx.cls.info.bases ): ctx.cls.info.bases.append(base_type) ctx.cls.info.mro.insert(1, composable_sym.node) # type: ignore[arg-type] return True class SciNexusPlugin(Plugin): def get_class_decorator_hook_2( self, fullname: str ) -> Callable[[ClassDefContext], bool] | None: return _define_app_hook if fullname == DEFINE_APP_FULLNAME else None def plugin(version: str) -> type[SciNexusPlugin]: # noqa: ARG001 return SciNexusPlugin cogent3-scinexus-b16bc06/src/scinexus/_sync.py000066400000000000000000000024401525761402500214440ustar00rootroot00000000000000"""synchronisation helpers Helpers to facilitate usage in free-threaded CPython. """ import threading from typing import Any class LockMixin: """gives an object a reentrant lock that is kept out of its pickled state Owning classes create the lock in ``__new__`` rather than ``__init__``, so that instances built by ``__new__`` alone still have one. Creating it on first use instead would need a guard of its own, which is the problem this class exists to solve. Notes ----- ``__getstate__`` returning a falsy mapping makes pickle skip ``__setstate__`` entirely. That is harmless here because the lock is restored by ``__new__``, not by ``__setstate__``. """ _cache_lock: threading.RLock @staticmethod def new_lock() -> threading.RLock: """return a lock suitable for guarding lazily cached state Reentrant because the guarded regions re-enter each other: reading a cached list can run through ``__contains__``, which reads the same cached list again. """ return threading.RLock() def __getstate__(self) -> dict[str, Any]: """return ``__dict__`` without the lock, which cannot be pickled""" state = self.__dict__.copy() state.pop("_cache_lock", None) return state cogent3-scinexus-b16bc06/src/scinexus/_version.py000066400000000000000000000000321525761402500221500ustar00rootroot00000000000000__version__ = "2026.10.2" cogent3-scinexus-b16bc06/src/scinexus/composable.py000066400000000000000000001361741525761402500224710ustar00rootroot00000000000000import contextlib import inspect import json import re import sys import textwrap import time import traceback import types import typing from collections.abc import Callable, Iterable, Iterator from copy import copy, deepcopy from enum import Enum from pathlib import Path from typing import Any, ClassVar, Generic, Literal, Self, TypeVar, overload from uuid import uuid4 from citeable import Citation from scitrack import CachingLogger # type: ignore[import-untyped] from typeguard import TypeCheckError, check_type from scinexus import typing as snx_typing from scinexus._version import __version__ from scinexus.deserialise import register_deserialiser from scinexus.misc import docstring_to_summary_rest, get_object_provenance from scinexus.progress import Progress, get_progress from scinexus.typing import ( check_type_compatibility, get_type_display_names, resolve_type_hint, ) from scinexus.warning import deprecated_callable from .data_store import ( DataMember, DataStoreABC, get_id_from_source, ) _builtin_seqs = list, set, tuple T = TypeVar("T") R = TypeVar("R") _T = TypeVar("_T") def _make_logfile_name(process: object) -> str: text = re.split(r"\s+\+\s+", str(process)) parts = [] for part in text: if part.find("(") >= 0: part = part[: part.find("(")] parts.append(part) result = "-".join(parts) uid = str(uuid4()) return f"{result}-{uid[:8]}.log" def _get_origin(origin: typing.Any) -> str: return origin if isinstance(origin, str) else origin.__class__.__name__ class NotCompletedType(Enum): ERROR = "ERROR" FAIL = "FAIL" BUG = "BUG" class NotCompleted(int): """results that failed to complete""" type: NotCompletedType origin: str message: str source: str | None _persistent: tuple[tuple[Any, ...], dict[str, Any]] def __new__( cls, type_: NotCompletedType | str, origin: typing.Any, message: str, source: typing.Any = None, ) -> Self: """ Parameters ---------- type_ the category of failure, e.g. NotCompletedType.ERROR origin where the instance was created, can be an instance message descriptive message, succinct traceback source the data operated on that led to this result. May be a string or an instance with ``.source`` or ``.info.source`` attributes. """ type_ = NotCompletedType(type_) origin = _get_origin(origin) try: source = get_id_from_source()(source) except Exception: # noqa: BLE001 source = None result = int.__new__(cls, False) # noqa: FBT003 result._persistent = (type_.value, origin, message), {"source": source} # noqa: SLF001 result.type = type_ result.origin = origin result.message = message result.source = source return result def __getnewargs_ex__(self) -> tuple[tuple[Any, ...], dict[str, Any]]: return self._persistent[0], self._persistent[1] def __repr__(self) -> str: return str(self) def __str__(self) -> str: name = self.__class__.__name__ source = self.source or "Unknown" return f'{name}(type={self.type.value}, origin={self.origin}, source="{source}", message="{self.message}")' def to_rich_dict(self) -> dict[str, Any]: """returns components for to_json""" return { "type": get_object_provenance(self), "not_completed_construction": { "args": self._persistent[0], "kwargs": self._persistent[1], }, "version": __version__, } def to_json(self) -> str: """returns json string""" return json.dumps(self.to_rich_dict()) class AppType(Enum): LOADER = "loader" WRITER = "writer" GENERIC = "generic" NON_COMPOSABLE = "non_composable" # Aliases to use Enum easily LOADER = AppType.LOADER WRITER = AppType.WRITER GENERIC = AppType.GENERIC NON_COMPOSABLE = AppType.NON_COMPOSABLE def _get_raw_hints( main_func: Callable[..., Any], min_params: int ) -> tuple[object, object]: _no_value = inspect.Parameter.empty params = inspect.signature(main_func) if len(params.parameters) < min_params: msg = f"{main_func.__name__!r} must have at least {min_params} input parameters" raise ValueError( msg, ) # annotation for first parameter other than self, params.parameters is an orderedDict first_param_type = [p.annotation for p in params.parameters.values()][ min_params - 1 ] return_type = params.return_annotation if return_type is _no_value: msg = "must specify type hint for return type" raise TypeError(msg) if first_param_type is _no_value: msg = "must specify type hint for first parameter" raise TypeError(msg) if first_param_type is None: msg = "NoneType invalid type for first parameter" raise TypeError(msg) if return_type is None: msg = "NoneType invalid type for return value" raise TypeError(msg) if isinstance(first_param_type, str): msg = ( "Apps do not yet support string type hints " "(such as those caused by __future__ annotations). " f"Bad type hint: {first_param_type}" ) raise NotImplementedError(msg) if isinstance(return_type, str): msg = ( "Apps do not yet support string type hints " "(such as those caused by __future__ annotations). " f"Bad type hint: {return_type}" ) raise NotImplementedError(msg) return first_param_type, return_type def _get_main_hints(klass: type[Any]) -> tuple[object, object]: """return raw type hints for main method Returns ------- (first_param_type_hint, return_type_hint) """ # Check klass.main exists and is type method main_func = getattr(klass, "main", None) if ( main_func is None or not inspect.isclass(klass) or not inspect.isfunction(main_func) ): msg = f"must define a callable main() method in {klass.__name__!r}" raise ValueError(msg) first_param_type, return_type = _get_raw_hints(main_func, 2) return first_param_type, return_type def _set_hints( main_meth: Callable[..., Any], first_param_type: object, return_type: object ) -> Callable[..., Any]: """adds type hints to main""" main_meth.__annotations__["arg"] = first_param_type main_meth.__annotations__["return"] = return_type return main_meth class source_proxy(Generic[_T]): """wraps an object to track its source through app pipelines""" __slots__ = ("_obj", "_src", "_uuid") def __init__(self, obj: _T) -> None: self._obj = obj self._src = obj self._uuid = uuid4() def __hash__(self) -> int: return hash(self._uuid) @property def obj(self) -> _T: return self._obj def set_obj(self, obj: typing.Any) -> None: self._obj = obj @property def source(self) -> typing.Any: """origin of this object""" return self._src @source.setter def source(self, src: typing.Any) -> None: # need to check whether src is hashable, how to cope if it isn't? # might need to make this instance hashable perhaps using a uuid? self._src = src @property def uuid(self) -> str: """unique identifier for this object""" return str(self._uuid) def __getattr__(self, name: str) -> typing.Any: return getattr(self._obj, name) def __setattr__(self, name: str, value: typing.Any) -> None: if name.startswith("_"): super().__setattr__(name, value) else: setattr(self._obj, name, value) def __bool__(self) -> bool: return bool(self._obj) def __repr__(self) -> str: return self.obj.__repr__() def __str__(self) -> str: return self.obj.__str__() def __eq__(self, other: object) -> bool: return self.obj.__eq__(other) def __len__(self) -> int: return self.obj.__len__() # type: ignore[attr-defined] # pickling induces infinite recursion on python 3.10 # only on Windows, so implementing the following methods explicitly def __getstate__(self) -> tuple[Any, Any, Any]: return self._obj, self._src, self._uuid def __setstate__(self, state: tuple[Any, Any, Any]) -> None: self._obj, self._src, self._uuid = state def _proxy_input(dstore: Iterable[Any]) -> list[source_proxy[Any]]: inputs = [] for e in dstore: if not e: continue # every input is proxied, retaining any existing .source information # in the proxy e = e if isinstance(e, source_proxy) else source_proxy(e) inputs.append(e) return inputs GetIdFuncType = typing.Callable[[source_proxy[Any] | snx_typing.HasSource], str | None] def _has_source(result: typing.Any) -> bool: """whether result carries a reference to its own origin""" if isinstance(result, dict): info = result.get("info") val = info.get("source") if isinstance(info, dict) else None return bool(val or result.get("source")) return bool(getattr(result, "source", None)) class propagate_source: """retains result association with source Notes ----- Returns the unwrapped result if it tracks its own source, otherwise returns the original source_proxy with the .obj updated with result. """ def __init__(self, app: "AppBase[Any, Any]", id_from_source: GetIdFuncType) -> None: self.app = app self.id_from_source = id_from_source def __call__( self, value: source_proxy[Any] | snx_typing.HasSource ) -> snx_typing.HasSource: if not isinstance(value, source_proxy): return self.app(value) result = self.app(value.obj) if isinstance(result, NotCompleted) and not _has_source(result): # rebuilt rather than assigned to, so to_rich_dict() reports the # same source as the instance does result = NotCompleted( result.type, result.origin, result.message, source=self.id_from_source(value), ) elif not _has_source(result): value.set_obj(result) return value return result # Forbidden methods per app kind _FORBIDDEN_BASE = frozenset( { "__call__", "__repr__", "__str__", "__new__", "__copy__", "__eq__", "_validate_data_type", "as_completed", "check_data_type", "_get_citations", "citations", "bib", } ) _FORBIDDEN_COMPOSABLE = _FORBIDDEN_BASE | frozenset( { "__add__", "disconnect", "input", } ) _FORBIDDEN_WRITER = _FORBIDDEN_COMPOSABLE | frozenset( { "apply_to", "set_logger", } ) def _init_subclass_setup( cls: Any, app_type: AppType | Literal["loader", "writer", "generic", "non_composable"], skip_not_completed: bool, cite: Citation | None, ) -> None: """Shared setup logic for __init_subclass__ and define_app.""" app_type = AppType(app_type) if "__slots__" in cls.__dict__: msg = "slots are not currently supported" raise NotImplementedError(msg) if app_type is WRITER: forbidden = _FORBIDDEN_WRITER elif app_type is not NON_COMPOSABLE: forbidden = _FORBIDDEN_COMPOSABLE else: forbidden = _FORBIDDEN_BASE if ( app_type is not NON_COMPOSABLE and "input" in cls.__dict__ and cls.__dict__["input"] is not None ): msg = f"remove 'input' attribute in {cls.__name__!r}, reserved by the app framework" raise TypeError(msg) for meth in forbidden: if meth in cls.__dict__: val = cls.__dict__[meth] if isinstance(val, staticmethod): val = val.__func__ if inspect.isfunction(val) or isinstance(val, property): msg = f"remove {meth!r} in {cls.__name__!r}, reserved by the app framework" raise TypeError(msg) raw_input, raw_return = _get_main_hints(cls) mod = sys.modules.get(cls.__module__) if cls.__module__ else None module_globals = vars(mod) if mod else {} cls._input_type = resolve_type_hint(raw_input, module_globals) cls._return_type = resolve_type_hint(raw_return, module_globals) cls.app_type = app_type # apply_to drives a writer's main() itself rather than going through # __call__, so a writer receives a NotCompleted no matter what it asks # for. Forcing the flag off keeps a direct call saying the same thing. cls._skip_not_completed = False if app_type is WRITER else skip_not_completed main_params = inspect.signature(cls.main).parameters cls._main_takes_identifier = "identifier" in main_params or any( p.kind is inspect.Parameter.VAR_KEYWORD for p in main_params.values() ) cls._check_data_type = True cls._cite = cite cls._id_from_source_override = None if app_type is not LOADER: cls.input = None class AppBase(Generic[T, R]): """Base for all app types. Provides __call__, __repr__, etc. Raises ------ TypeError If a subclass defines any method name reserved by the app framework. The reserved names depend on the app type. All app types: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``. Composable apps (GENERIC, LOADER, WRITER) additionally: ``__add__``, ``disconnect``, ``input``. Writer apps additionally: ``apply_to``, ``set_logger``. """ _is_intermediate_base: bool = False _skip_not_completed: bool _main_takes_identifier: bool _check_data_type: bool _id_from_source_override: GetIdFuncType | None _cite: Citation | None _input_type: type _return_type: type _init_vals: dict[str, Any] _supplied_args: dict[str, Any] app_type: AppType input: Any main: Callable[..., Any] def __init_subclass__( cls, skip_not_completed: bool = True, cite: Citation | None = None, **kwargs: Any, ) -> None: super().__init_subclass__(**kwargs) # Skip setup for intermediate bases and classes built by define_app if "_is_intermediate_base" in cls.__dict__ or getattr( cls, "_define_app_pending", False ): return app_type = getattr(cls, "_default_app_type", GENERIC) _init_subclass_setup(cls, app_type, skip_not_completed, cite) def __new__(cls, *args: Any, **kwargs: Any) -> Self: obj = object.__new__(cls) if hasattr(cls, "_func_sig"): # we have a decorated function, the first parameter in the signature # is not given to constructor, so we create a new signature excluding that one params = cls._func_sig.parameters # type: ignore[attr-defined] init_sig = inspect.Signature(parameters=list(params.values())[1:]) bargs = init_sig.bind_partial(*args, **kwargs) bound_self = None else: init_sig = inspect.signature(cls.__init__) bargs = init_sig.bind_partial(cls, *args, **kwargs) # the first parameter took cls above and is not an argument the # caller gave, whatever the app chose to name it bound_self = next(iter(init_sig.parameters), None) # apply_defaults below makes a defaulted argument indistinguishable # from a supplied one, so record what the call bound first supplied = { name: value for name, value in bargs.arguments.items() if name != bound_self } for name, param in init_sig.parameters.items(): # a name absorbed by **kwargs is nested one level down, and it is # the nested name a caller means when asking what was supplied if param.kind is inspect.Parameter.VAR_KEYWORD and name in supplied: supplied.update(supplied.pop(name)) bargs.apply_defaults() init_vals = bargs.arguments init_vals.pop("self", None) obj._init_vals = init_vals obj._supplied_args = supplied return obj def __copy__(self) -> Self: new = object.__new__(type(self)) new.__dict__.update(self.__dict__) return new def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): return False return all( v is other.__dict__.get(k) for k, v in self.__dict__.items() if k != "input" ) __hash__ = None # type: ignore[assignment] @property def check_data_type(self) -> bool: """toggle whether the type of input data matches the defined compatible types Notes ----- If False, and an invalid data type is passed, the error will still be caught, but in a potentially less informative way. For instance "'NoneType' object has no attribute 'blah'" """ return self._check_data_type @check_data_type.setter def check_data_type(self, value: bool) -> None: self._check_data_type = value head = getattr(self, "input", None) while head is not None: head._check_data_type = value head = getattr(head, "input", None) def _resolve_id_from_source(self) -> Callable[..., Any]: """the extractor this app names records with An ``id_from_source`` given to ``as_completed`` or ``apply_to`` becomes the app's setting and outranks everything else. A writer's constructor argument comes next, and only a writer's, since ``id_from_source`` means whatever its author chose on any other app. That one has to be supplied: a parameter left at its default does not count, or an app declaring ``id_from_source=get_unique_id`` would always outrank ``set_id_from_source``. """ if self._id_from_source_override is not None: return self._id_from_source_override supplied = self._supplied_args.get("id_from_source") if self.app_type is WRITER and callable(supplied): return supplied return get_id_from_source() def __call__(self, val: T, *args: Any, **kwargs: Any) -> R | NotCompleted: if val is None: return NotCompleted( NotCompletedType.ERROR, self, "unexpected input value None", source=val ) if isinstance(val, NotCompleted) and self._skip_not_completed: return val # val is rebound to the input app's result below, so the identity a # failure gets attributed to has to be taken before that happens source: Any = val if self.app_type is not LOADER and self.input: # passing to connected app val = self.input(val, *args, **kwargs) if isinstance(val, NotCompleted) and self._skip_not_completed: return val if self._check_data_type: type_checked = self._validate_data_type(val, source=source) if not type_checked: return type_checked # type: ignore[return-value] if ( self.app_type is WRITER and self._main_takes_identifier and not args and "identifier" not in kwargs ): # mirrors apply_to, which names a record after the result when the # result knows its own origin and after the input when it does not named = val if _has_source(val) else source unique_id = self._resolve_id_from_source()(named) if not unique_id: msg = ( f"{self.__class__.__name__!r} cannot name a record: " f"no identifier could be derived from {named!r}" ) raise ValueError(msg) kwargs["identifier"] = unique_id try: result = self.main(val, *args, **kwargs) except Exception: result = NotCompleted( NotCompletedType.ERROR, self, traceback.format_exc(), source=source ) if result is None: result = NotCompleted( NotCompletedType.BUG, self, "unexpected output value None", source=source, ) return result def __repr__(self) -> str: val = f"{self.input!r} + " if self.app_type is not LOADER and self.input else "" all_args = {**self._init_vals} args_items = all_args.pop("args", None) data = ", ".join(f"{v!r}" for v in args_items) if args_items else "" kwargs_items = all_args.pop("kwargs", None) data += ( ", ".join(f"{k}={v!r}" for k, v in kwargs_items.items()) if kwargs_items else "" ) data += ", ".join(f"{k}={v!r}" for k, v in all_args.items()) data = f"{val}{self.__class__.__name__}({data})" return textwrap.fill( data, width=80, break_long_words=False, break_on_hyphens=False ) __str__ = __repr__ def _validate_data_type(self, data: Any, source: Any = None) -> bool | NotCompleted: """checks data type matches defined compatible types using typeguard""" if isinstance(data, NotCompleted): if self._skip_not_completed: return data # skip_not_completed=False means the app handles NotCompleted itself return True if isinstance(data, source_proxy): data = data.obj if isinstance(data, _builtin_seqs) and len(data) == 0: return NotCompleted( NotCompletedType.ERROR, self, message="empty data", source=source ) try: check_type(data, self._input_type) return True except TypeCheckError: class_name = data.__class__.__name__ expected = get_type_display_names(self._input_type) msg = f"invalid data type, '{class_name}' not in {', '.join(sorted(expected))}" return NotCompleted( NotCompletedType.ERROR, self, message=msg, source=source ) def as_completed( self, dstore: DataStoreABC | Iterable[Any] | str, parallel: bool = False, par_kw: dict[str, Any] | None = None, id_from_source: GetIdFuncType | None = None, show_progress: bool | Progress = False, ) -> Iterator[Any]: """invokes self composable function on the provided data store Parameters ---------- dstore a path, list of paths, or DataStore to which the process will be applied. parallel run in parallel, according to arguments in par_kwargs. If True, the last step of the composable function serves as the master process, with earlier steps being executed in parallel for each member of dstore. par_kw dict of values for configuring parallel execution. id_from_source extracts a unique identifier from each input. Becomes this app's setting, applying to later calls until another is given. If not provided, defaults to the setting left by an earlier call, then to the ``id_from_source`` this app was constructed with, then to the function registered via ``scinexus.data_store.set_id_from_source``, falling back to ``scinexus.data_store.get_unique_id``. show_progress controls progress bar display. Pass ``True`` for the default progress bar, ``False`` to disable, or a ``Progress`` instance for a custom backend. Notes ----- If run in parallel, this instance serves as the master object and aggregates results. If run in serial, results are returned in the same order as provided. """ if id_from_source is not None: self._id_from_source_override = id_from_source app = propagate_source( self.input if self.app_type is WRITER else self, self._resolve_id_from_source(), ) if isinstance(dstore, str): dstore = [dstore] elif isinstance(dstore, DataStoreABC): dstore = dstore.completed mapped = _proxy_input(dstore) if not mapped: return iter(()) if parallel: from scinexus import parallel as snxpar par_kw = par_kw or {} to_do: typing.Iterable = snxpar.as_completed(app, mapped, **par_kw) else: to_do = map(app, mapped) progress = get_progress(show_progress) # the results are yielded from a nested generator so that this method # is an ordinary function, recording the setting above when the caller # asks rather than when it starts consuming. A progress bar displays # as soon as it is built, so it stays inside the generator def results() -> Iterator[Any]: for obj in progress(to_do, total=len(mapped)): if not isinstance(obj, source_proxy): yield obj continue yield obj.obj if _has_source(obj.obj) else obj return results() def _get_citations(self) -> tuple[Citation, ...]: """Return citations for this app and all composed input apps.""" seen: set[Citation] = set() result: list[Citation] = [] if self._cite is not None: self._cite.app = self.__class__.__name__ seen.add(self._cite) result.append(self._cite) head = getattr(self, "input", None) while head is not None: if head._cite is not None and head._cite not in seen: head._cite.app = head.__class__.__name__ seen.add(head._cite) result.append(head._cite) head = getattr(head, "input", None) return tuple(result) @property def citations(self) -> tuple[Citation, ...]: """Citations for this app and all composed input apps.""" return self._get_citations() @property def bib(self) -> str: """BibTeX formatted string of citations for this app and all composed input apps.""" return "\n\n".join(str(cite) for cite in self.citations) class ComposableApp(AppBase[T, R]): """Adds __add__ for LOADER/GENERIC. Raises ------ TypeError If a subclass defines any method name reserved by the app framework: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``, ``__add__``, ``disconnect``, ``input``. """ _is_intermediate_base: bool = True def __add__(self, other: "ComposableApp[Any, Any]") -> "ComposableApp[Any, Any]": if getattr(other, "app_type", None) not in {WRITER, LOADER, GENERIC}: msg = f"{other!r} is not composable" raise TypeError(msg) if other == self: msg = "cannot add an app to itself" raise ValueError(msg) # Check order if self.app_type is WRITER: msg = "Left hand side of add operator must not be of type writer" raise TypeError(msg) if other.app_type is LOADER: msg = "Right hand side of add operator must not be of type loader" raise TypeError(msg) if not check_type_compatibility(self._return_type, other._input_type): self_names = get_type_display_names(self._return_type) other_names = get_type_display_names(other._input_type) msg = ( f"{self.__class__.__name__!r} return_type {self_names} " f"incompatible with {other.__class__.__name__!r} input " f"type {other_names}" ) raise TypeError(msg) result = copy(other) result.input = copy(self) return result @deprecated_callable( version="2026.9", reason="no longer required", is_discontinued=True, ) def disconnect(self) -> None: # pragma: no cover """Deprecated. No longer required since composition uses copies.""" class WriterApp(ComposableApp[T, R]): """Adds apply_to and set_logger for WRITER. Raises ------ TypeError If a subclass defines any method name reserved by the app framework: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``, ``__add__``, ``disconnect``, ``input``, ``apply_to``, ``set_logger``. """ _is_intermediate_base: bool = True _default_app_type: ClassVar[AppType] = WRITER data_store: DataStoreABC logger: CachingLogger | None def apply_to( self, dstore: DataStoreABC | Iterable[Any] | str | Path, id_from_source: GetIdFuncType | None = None, parallel: bool = False, par_kw: dict[str, Any] | None = None, logger: bool | CachingLogger = True, cleanup: bool = True, show_progress: bool | Progress = False, ) -> DataStoreABC: """invokes self composable function on the provided data store Parameters ---------- dstore a path, list of paths, or DataStore to which the process will be applied. id_from_source makes the unique identifier from elements of dstore that will be used for writing results. Becomes this writer's setting, applying to later calls until another is given. If not provided, defaults to the setting left by an earlier call, then to the ``id_from_source`` this writer was constructed with, then to the function registered via ``scinexus.data_store.set_id_from_source``, falling back to ``scinexus.data_store.get_unique_id``. parallel run in parallel, according to arguments in par_kwargs. If True, the last step of the composable function serves as the master process, with earlier steps being executed in parallel for each member of dstore. par_kw dict of values for configuring parallel execution. logger Controls logging. If True (default), a CachingLogger is created automatically. If False, logging is disabled. A CachingLogger instance can be passed directly. cleanup after copying of log files into the data store, it is deleted from the original location show_progress controls progress bar display Returns ------- The output data store instance Notes ----- This is an append only function, meaning that if a member already exists in self.data_store for an input, it is skipped. If run in parallel, this instance spawns workers and aggregates results. """ if self.input is None: msg = "writer app has no composed input" raise RuntimeError(msg) override = id_from_source id_from_source = ( override if override is not None else self._resolve_id_from_source() ) if isinstance(dstore, str | Path): # one filename dstore = [dstore] elif isinstance(dstore, DataStoreABC): dstore = dstore.completed # TODO this should fail if somebody provides data that cannot produce a unique_id inputs = {} for m in dstore: input_id = Path(m.unique_id) if isinstance(m, DataMember) else m input_id = id_from_source(input_id) # type: ignore[arg-type] if input_id in inputs or not input_id: msg = f"non-unique identifier {input_id!r} detected in data" raise ValueError(msg) if input_id in self.data_store: # we are assuming that this query returns True only when # an input_id is completed, we will not hit this if not_completed continue inputs[input_id] = m if ( not dstore ): # this should just return datastore, because if all jobs are done! msg = "dstore is empty" raise ValueError(msg) # recorded once the run is certain to start, so a refused call leaves # the app named as it was if override is not None: self._id_from_source_override = override self.set_logger(logger) active_logger: CachingLogger | None = self.logger if active_logger: start = time.time() active_logger.log_message(str(self), label="composable function") active_logger.log_versions(["scinexus"]) proxied = _proxy_input(inputs.values()) for result in self.as_completed( proxied, parallel=parallel, par_kw=par_kw, show_progress=show_progress, ): member = self.main( data=getattr(result, "obj", result), identifier=id_from_source(result), # type: ignore[arg-type] ) if active_logger: md5 = getattr(member, "md5", None) active_logger.log_message(str(member), label="output") if md5: active_logger.log_message(md5, label="output md5sum") if active_logger: taken = time.time() - start active_logger.log_message(f"{taken}", label="TIME TAKEN") log_file_path = Path(active_logger.log_file_path or ".") active_logger.shutdown() self.data_store.write_log( unique_id=log_file_path.name, data=log_file_path.read_text(), ) if cleanup: log_file_path.unlink(missing_ok=True) # write citations self.data_store.write_citations(data=self.citations) return self.data_store def set_logger(self, logger: bool | CachingLogger = True) -> None: if logger is False: self.logger = None return if logger is True: logger = CachingLogger(create_dir=True) if not isinstance(logger, CachingLogger): msg = f"logger must be of type CachingLogger not {type(logger)}" raise TypeError(msg) if not logger.log_file_path: src = Path(self.data_store.source).parent # type: ignore[attr-defined] logger.log_file_path = str(src / _make_logfile_name(self)) self.logger = logger class LoaderApp(ComposableApp[T, R]): """Intermediate base class for LOADER apps. Subclasses of ``LoaderApp`` are automatically assigned ``app_type=LOADER``. Loaders sit at the start of a composed pipeline and have no ``input`` attribute. Raises ------ TypeError If a subclass defines any method name reserved by the app framework: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``, ``__add__``, ``disconnect``, ``input``. Examples -------- Define a loader by inheritance:: class my_loader(LoaderApp): def main(self, path): return path """ _is_intermediate_base: bool = True _default_app_type: ClassVar[AppType] = LOADER class NonComposableApp(AppBase[T, R]): """Intermediate base class for NON_COMPOSABLE apps. Subclasses of ``NonComposableApp`` are automatically assigned ``app_type=NON_COMPOSABLE``. Non-composable apps cannot participate in pipeline composition via ``+``. Raises ------ TypeError If a subclass defines any method name reserved by the app framework: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``. Examples -------- Define a non-composable app by inheritance:: class my_app(NonComposableApp): def main(self, val): return val """ _is_intermediate_base: bool = True _default_app_type: ClassVar[AppType] = NON_COMPOSABLE def _class_from_func(func: Callable[..., Any]) -> type[Any]: """make a class based on func Notes ----- produces a class consistent with the necessary properties for the define_app class decorator. func becomes a static method on the class """ # these methods MUST be in function scope so that separate instances are # created for each decorated function def _init(self: Any, *args: Any, **kwargs: Any) -> None: self._args = args self._kwargs = kwargs def _main(self: Any, arg: Any, *args: Any, **kwargs: Any) -> Any: kw_args = deepcopy(self._kwargs) kw_args = {**kw_args, **kwargs} args = (arg, *args, *deepcopy(self._args)) bound = self._func_sig.bind(*args, **kw_args) return self._user_func(**bound.arguments) module = func.__module__ # to be assigned to the generated class sig = inspect.signature(func) class_name = func.__name__ _main = _set_hints(_main, *_get_raw_hints(func, 1)) summary, body = docstring_to_summary_rest(func.__doc__ or "") func.__doc__ = None _class_dict = {"__init__": _init, "main": _main, "_user_func": staticmethod(func)} for method_name, method in _class_dict.items(): method.__name__ = method_name # type: ignore[attr-defined] method.__qualname__ = f"{class_name}.{method_name}" # type: ignore[attr-defined] result = types.new_class(class_name, (), exec_body=lambda x: x.update(_class_dict)) result.__module__ = module # necessary for pickle support result._func_sig = sig # type: ignore[attr-defined] result.__doc__ = summary result.__init__.__doc__ = body # type: ignore[misc] return result def _fix_super_class_cells( old_klass: type[Any], new_klass: type[Any], ) -> None: """Update __class__ closure cells so zero-arg super() works in the new class. When define_app rebuilds a class via types.new_class, methods copied from the original class retain closure cells pointing to the old class. Python's zero-arg super() uses the __class__ cell, so it fails when the instance is of the new class. This function patches those cells to point to new_klass. """ for attr in new_klass.__dict__.values(): fn = attr if isinstance(attr, types.FunctionType) else None if fn is None: # unwrap classmethod / staticmethod fn = getattr(attr, "__func__", None) if not isinstance(fn, types.FunctionType): continue closure = fn.__closure__ if closure is None: continue freevars = fn.__code__.co_freevars for i, name in enumerate(freevars): if name == "__class__": with contextlib.suppress(ValueError): if closure[i].cell_contents is old_klass: closure[i].cell_contents = new_klass @overload def define_app( klass: type[Any] | Callable[..., Any], ) -> type[ComposableApp[Any, Any]]: ... @overload def define_app( # type: ignore[overload-overlap] klass: None = None, *, app_type: Literal[AppType.NON_COMPOSABLE, "non_composable"], skip_not_completed: bool = ..., cite: Citation | None = ..., ) -> Callable[[type[Any] | Callable[..., Any]], type[AppBase[Any, Any]]]: ... @overload def define_app( # type: ignore[overload-overlap] klass: None = None, *, app_type: Literal[AppType.WRITER, "writer"], skip_not_completed: bool = ..., cite: Citation | None = ..., ) -> Callable[[type[Any] | Callable[..., Any]], type[WriterApp[Any, Any]]]: ... @overload def define_app( klass: None = None, *, app_type: AppType | Literal["loader", "writer", "generic", "non_composable"] = ..., skip_not_completed: bool = ..., cite: Citation | None = ..., ) -> Callable[[type[Any] | Callable[..., Any]], type[ComposableApp[Any, Any]]]: ... def define_app( klass: type[Any] | Callable[..., Any] | None = None, *, app_type: AppType | Literal["loader", "writer", "generic", "non_composable"] = GENERIC, skip_not_completed: bool = True, cite: Citation | None = None, ) -> type[Any]: """decorator for building callable apps Parameters ---------- klass either a class or a function. If a function, it is converted to a class with the function bound as a static method. app_type what type of app, typically you just want GENERIC. skip_not_completed if True (default), NotCompleted instances are returned without being passed to the app. Ignored for a writer, which always receives them because ``apply_to`` invokes ``main`` directly. A writer's ``main`` must handle NotCompleted itself. cite a Citation instance describing the software or algorithm. If provided, its ``.app`` attribute is set to the class name. Raises ------ TypeError If the decorated class defines any method name reserved by the app framework. The reserved names depend on the app type. All app types: ``__call__``, ``__repr__``, ``__str__``, ``__new__``, ``__copy__``, ``__eq__``, ``_validate_data_type``, ``as_completed``, ``check_data_type``, ``_get_citations``, ``citations``, ``bib``. Composable apps (GENERIC, LOADER, WRITER) additionally: ``__add__``, ``disconnect``, ``input``. Writer apps additionally: ``apply_to``, ``set_logger``. Examples -------- An example app definition. >>> from scinexus.composable import define_app >>> @define_app ... class noop: ... def main(self, data: int) -> int: ... return data Notes ----- Instances of scinexus apps are callable. If an exception occurs, the app returns a ``NotCompleted`` instance with logging information. Apps defined with app_type ``LOADER``, ``GENERIC`` or ``WRITER`` can be "composed" (summed together) to produce a single callable that sequentially invokes the composed apps. For example, the independent usage of app instances ``app1`` and ``app2`` as ```python { notest } app2(app1(data)) ``` is equivalent to ```python { notest } combined = app1 + app2 combined(data) ``` The ``app_type`` attribute is used to constrain how apps can be composed. ``LOADER`` and ``WRITER`` are special cases. If included, a ``LOADER`` must always be first, e.g. ```python { notest } app = a_loader + a_generic ``` If included, a ``WRITER`` must always be last, e.g. ```python { notest } app = a_generic + a_writer ``` Changing the order for either of the above will result in a ``TypeError``. There are no constraints on ordering of ``GENERIC`` aside from compatability of their input and return types (see below). In order to be decorated with ``@define_app`` a class **must** - implement a method called ``main`` - type hint the first argument of ``main`` - type hint the return type for ``main`` While you can have more than one argument in ``main``, this is currently not supported in composable apps. Overlap between the return type hint and first argument hint is required for two apps to be composed together. ``define_app`` adds a ``__call__`` method which checks an input value prior to passing it to ``app.main()`` as a positional argument. The data checking results in ``NotCompleted`` being returned immediately, unless ``skip_not_completed==False``. If the input value type is consistent with the type hint on the first argument of main it is passed to ``app.main()``. If it does not match, a new ``NotCompleted`` instance is returned. """ if hasattr(klass, "app_type"): msg = ( f"The class {klass.__name__!r} is already decorated, avoid using " # type: ignore[union-attr] "inheritance from a decorated class." ) raise TypeError( msg, ) app_type = AppType(app_type) def wrapped(klass: type[Any] | Callable[..., Any]) -> type[Any]: if inspect.isfunction(klass): klass = _class_from_func(klass) if not inspect.isclass(klass): msg = f"{klass} is not a class" raise ValueError(msg) # Select base class based on app_type base: type[AppBase[Any, Any]] if app_type is WRITER: base = WriterApp elif app_type is not NON_COMPOSABLE: base = ComposableApp else: base = AppBase if "__slots__" in klass.__dict__: msg = "slots are not currently supported" raise NotImplementedError(msg) # Get type hints before rebuilding the class raw_input, raw_return = _get_main_hints(klass) # Collect the user's class dict (excluding metaclass artefacts) original_dict = { k: v for k, v in klass.__dict__.items() if k not in ("__dict__", "__weakref__") } # Prevent __init_subclass__ from running setup (we do it below) original_dict["_define_app_pending"] = True # Preserve user-specified bases (excluding object) so super() # resolves through the original MRO extra_bases = tuple(b for b in klass.__bases__ if b is not object) # Recreate class with the base (types.new_class stores # parameterised form in __orig_bases__ for type checkers) new_klass = types.new_class( klass.__name__, (base[raw_input, raw_return], *extra_bases), # type: ignore[index] exec_body=lambda ns: ns.update(original_dict), ) new_klass.__module__ = klass.__module__ new_klass.__qualname__ = klass.__qualname__ _fix_super_class_cells(klass, new_klass) del new_klass._define_app_pending # type: ignore[attr-defined] # Run setup once with the decorator's arguments _init_subclass_setup(new_klass, app_type, skip_not_completed, cite) return new_klass return wrapped(klass) if klass else wrapped # type: ignore[return-value] def is_app_composable(obj: object) -> bool: """checks whether obj has been decorated by define_app and it's app_type attribute is not NON_COMPOSABLE""" return is_app(obj) and obj.app_type is not NON_COMPOSABLE # type: ignore[attr-defined] def is_app(obj: object) -> bool: """checks whether obj has been decorated by define_app""" return hasattr(obj, "app_type") @register_deserialiser(get_object_provenance(NotCompleted)) def deserialise_not_completed(data: dict[str, Any]) -> NotCompleted: """deserialising NotCompletedResult""" data.pop("version", None) init = data.pop("not_completed_construction") args = init.pop("args") kwargs = init.pop("kwargs") return NotCompleted(*args, **kwargs) cogent3-scinexus-b16bc06/src/scinexus/data_store.py000066400000000000000000001376511525761402500224730ustar00rootroot00000000000000from __future__ import annotations import contextlib import inspect import json import re import reprlib from abc import ABC, abstractmethod from collections import defaultdict from enum import Enum from fnmatch import fnmatchcase from functools import singledispatch from io import TextIOWrapper from pathlib import Path from typing import TYPE_CHECKING, TypedDict, overload from scitrack import get_text_hexdigest # type: ignore[import-untyped] from scinexus._sync import LockMixin from scinexus.deserialise import deserialise_object from scinexus.io_util import get_format_suffixes, open_ from scinexus.parallel import is_master_process if TYPE_CHECKING: # pragma: no cover from collections.abc import Callable, Iterator from typing import Any, Self from citeable import CitationBase NOT_COMPLETED_TABLE = "not_completed" LOG_TABLE = "logs" MD5_TABLE = "md5" # used for log files, not-completed results _special_suffixes = re.compile(r"\.(log|json)$") CITATIONS_FILE = "bibliography.citations" # a checksum file is named for the record it belongs to and for which of the # two kinds that record is. LEGACY is what both kinds shared before, so a # file still carrying it belongs to a record this store cannot identify COMPLETED_CHECKSUM = "cmplt" NOT_COMPLETED_CHECKSUM = "ncmplt" LEGACY_CHECKSUM = "txt" class ChecksumMigration(TypedDict): """what :meth:`DataStoreDirectory.migrate_checksums` did, and did not ``ambiguous`` is a stem both kinds of record carry, so the file cannot be attributed. ``orphaned`` is one no record carries. ``superseded`` is one whose record already has a checksum under the current name. """ migrated: int ambiguous: list[str] orphaned: list[str] superseded: list[str] def _member_id(subdir: str, name: str) -> str: """the store relative identifier of a record held in subdir Parameters ---------- subdir directory the record sits in, empty for a completed record name file name the record is stored under Notes ----- An identifier rather than a path, so it is spelled with "/" wherever it is composed. Joining with Path would spell it with the separator of the platform doing the joining, and a store's members would then read not_completed/nc1.json on POSIX and not_completed\\nc1.json on Windows. """ return f"{subdir}/{name}" if subdir else name def _record_stem(unique_id: str) -> str: """the identifier with its format and compression suffixes removed""" stem = Path(unique_id).name sfx, cmp = get_format_suffixes(stem) for part in (cmp, sfx): if part: stem = stem.removesuffix(f".{part}") return stem def _is_record(name: str, suffix: str) -> bool: """whether a file of this name is a record stored under this suffix Notes ----- A hidden file is not a record. The suffix may carry a trailing wildcard, so the rest is a pattern match, and fnmatchcase because Path.glob and fnmatch both fold case on Windows. """ return not name.startswith(".") and fnmatchcase(name, f"*.{suffix}") def _check_identifier(unique_id: str) -> None: """raise if the identifier has nothing to name a record by Parameters ---------- unique_id identifier as given by the caller Raises ------ ValueError if the stem is blank, or names a hidden file Notes ----- A record is named by its stem. Every data store asks this, so the same identifier is a record in all of them or in none. """ stem = _record_stem(unique_id) if stem.strip() and not stem.startswith("."): return why = "names a hidden file" if stem.strip() else "is blank" msg = f"identifier {unique_id!r} names no record: its stem {why}" raise ValueError(msg) def _check_compression(unique_id: str, suffix: str) -> None: """raise if the identifier names a compression this suffix does not write Parameters ---------- unique_id identifier as given by the caller suffix format suffix the record is about to be stored with Raises ------ ValueError if the two name different compressions, or the identifier names one and the suffix does not """ asked = get_format_suffixes(Path(unique_id).name)[1] stored = get_format_suffixes(f"x.{suffix}")[1] # naming none is never a conflict: the suffix supplies whatever # compression there is, and the identifier is only asked not to # contradict it if asked is None or asked == stored: return carries = f".{stored}" if stored else "no compression" msg = ( f"identifier {unique_id!r} names .{asked}, but a record stored " f"as .{suffix} carries {carries}" ) raise ValueError(msg) def _checksum_name(unique_id: str, *, completed: bool) -> str: """the file a record's checksum is kept in""" suffix = COMPLETED_CHECKSUM if completed else NOT_COMPLETED_CHECKSUM return f"{_record_stem(unique_id)}.{suffix}" def _legacy_checksum_name(unique_id: str) -> str: """the file a record's checksum was kept in before the kinds were named""" return f"{_record_stem(unique_id)}.{LEGACY_CHECKSUM}" NoneType = type(None) class Mode(Enum): r = "r" w = "w" a = "a" APPEND = Mode.a OVERWRITE = Mode.w READONLY = Mode.r # Summary display registry _summary_display_func: Callable[..., Any] | None = None # Unique-ID extractor registry _id_from_source_func: Callable[..., Any] | None = None def set_summary_display(func: Callable[..., Any] | None) -> None: """Set the function used to display data store summaries. Parameters ---------- func A callable with signature ``func(data, *, name) -> Any`` where *data* is a ``dict`` or ``list[dict]`` and *name* identifies the summary method (e.g. ``"describe"``). Pass ``None`` to clear. """ global _summary_display_func # noqa: PLW0603 _summary_display_func = func def get_summary_display() -> Callable[..., Any] | None: """Return the currently registered summary display function, or ``None``.""" return _summary_display_func def _apply_summary_display(data: Any, *, name: str) -> Any: if _summary_display_func is not None: return _summary_display_func(data, name=name) return data def _summary_property(data_method: Callable[..., Any]) -> property: """Create a property that delegates to a protected data method and applies display. The *data_method* should be a method defined on ``DataStoreABC`` (or a subclass) whose name starts with ``_``. Subclasses customise the raw data by overriding the ``_``-prefixed method; the public property created here handles display wrapping automatically. """ method_name = data_method.__name__ public_name = method_name.removeprefix("_") def fget(self: DataStoreABC) -> Any: data = getattr(self, method_name)() return _apply_summary_display(data, name=public_name) return property(fget, doc=data_method.__doc__) class DataMemberABC(ABC): """Abstract base class for DataMember A data member is a handle to a record in a DataStore. It has a reference to its data store and a unique identifier. """ @property @abstractmethod def data_store(self) -> DataStoreABC: ... @property @abstractmethod def unique_id(self) -> str: ... def __str__(self) -> str: return self.unique_id def __repr__(self) -> str: return f"{self.__class__.__name__}(data_store={self.data_store.source}, unique_id={self.unique_id})" def read(self) -> str | bytes: return self.data_store.read(self.unique_id) def __eq__(self, other: object) -> bool: """to check equality of members and check existence of a member in a list of members""" return isinstance(other, type(self)) and (self.data_store, self.unique_id) == ( other.data_store, other.unique_id, ) @property def md5(self) -> str | None: return self.data_store.md5(self.unique_id) class DataStoreABC(LockMixin, ABC): """Abstract base class for DataStore""" _init_vals: dict[str, Any] _completed: list[DataMemberABC] _not_completed: list[DataMemberABC] def __new__(cls, *args: Any, **kwargs: Any) -> Self: obj = object.__new__(cls) init_sig = inspect.signature(cls.__init__) bargs = init_sig.bind_partial(cls, *args, **kwargs) bargs.apply_defaults() init_vals = bargs.arguments init_vals.pop("self", None) obj._init_vals = init_vals obj._completed = [] obj._not_completed = [] # here rather than __init__ so instances built by __new__ alone, # including those pickle reconstructs, still get one obj._cache_lock = cls.new_lock() return obj @property @abstractmethod def source(self) -> str | Path: """string that references connecting to data store, override in subclass constructor""" ... @property @abstractmethod def mode(self) -> Mode: """string that references datastore mode, override in subclass constructor""" ... @property @abstractmethod def limit(self) -> int | None: ... def __repr__(self) -> str: name = self.__class__.__name__ construction = ", ".join(f"{k}={v}" for k, v in self._init_vals.items()) return f"{name}({construction})" def __str__(self) -> str: members = self.members num = len(members) name = self.__class__.__name__ sample = f"{members[:2]}..." if num > 2 else members return f"{num}x member {name}(source='{self.source}', members={sample})" @overload def __getitem__(self, index: int) -> DataMemberABC: ... @overload def __getitem__(self, index: slice) -> list[DataMemberABC]: ... def __getitem__(self, index: int | slice) -> DataMemberABC | list[DataMemberABC]: return self.members[index] def __len__(self) -> int: return len(self.members) def __contains__(self, identifier: object) -> bool: """whether relative identifier has been stored Notes ----- Compared on Path.parts, so a member id composed with either separator names the same record. Not Path equality, which is case insensitive on Windows and would make ID_0.fasta and id_0.fasta one record there and two here. """ if not isinstance(identifier, str): return False wanted = Path(identifier).parts return any(Path(m.unique_id).parts == wanted for m in self) @abstractmethod def read(self, unique_id: str) -> str | bytes: ... def close(self) -> None: """release whatever the store holds open Notes ----- Does nothing for a store that holds nothing, which is every one but :class:`DataStoreSqlite`. It is defined here so a caller can close what :func:`open_data_store` returned without knowing which backend it got. """ @staticmethod def _append_once( current: list[DataMemberABC], member: DataMemberABC, ) -> None: """record a just-written member in the cache, at most once Parameters ---------- current the cached list as it stands now member the member to record """ # current already holds an equal member when a scan in the gap # between the file landing on disk and this call published one, and # when the record is being written a second time. only the first # rebinds the list, so identity cannot stand in for the O(n) test if member not in current: current.append(member) def _check_writable(self, unique_id: str) -> None: if self.mode is READONLY: msg = "datastore is readonly" raise OSError(msg) if unique_id in self and self.mode is APPEND: msg = "cannot overwrite existing record in append mode" raise OSError(msg) @abstractmethod def write(self, *, unique_id: str, data: str | bytes) -> None: self._check_writable(unique_id) @abstractmethod def write_not_completed(self, *, unique_id: str, data: str | bytes) -> None: self._check_writable(unique_id) @abstractmethod def write_log(self, *, unique_id: str, data: str | bytes) -> None: self._check_writable(unique_id) @property def members(self) -> list[DataMemberABC]: # one acquisition spanning both, so the halves describe the same # moment. write() takes a record out of not_completed and then puts # it into completed, so halves read either side of that pair of # steps account for it in neither with self._cache_lock: return self.completed + self.not_completed def __iter__(self) -> Iterator[DataMemberABC]: yield from self.members @property @abstractmethod def logs(self) -> list[DataMemberABC]: ... @property @abstractmethod def completed(self) -> list[DataMemberABC]: ... @property @abstractmethod def not_completed(self) -> list[DataMemberABC]: ... def _summary_logs(self) -> list[dict]: """returns a list of dicts summarising log files""" rows = [] for record in self.logs: lines = str(record.read()).splitlines() first = lines.pop(0).split("\t") row = {"time": first[0], "name": record.unique_id} key: str | None = None mapped: dict[str, str] = {} for line in lines: parts = line.split("\t")[-1].split(" : ", maxsplit=1) if len(parts) == 1: if key is None: msg = "malformed log data: continuation line before any key" raise ValueError(msg) mapped[key] += parts[0] continue key = parts[0] mapped[key] = parts[1] row["python_version"] = mapped["python"] row["who"] = mapped["user"] row["command"] = mapped["command_string"] row["composable"] = mapped.get("composable function", "") rows.append(row) return rows summary_logs = _summary_property(_summary_logs) def _summary_not_completed(self) -> list[dict]: """returns a list of dicts summarising not completed results""" return summary_not_completeds(self.not_completed) summary_not_completed = _summary_property(_summary_not_completed) def _describe(self) -> dict[str, object]: num_not_completed = len(self.not_completed) num_completed = len(self.completed) num_logs = len(self.logs) return { "completed": num_completed, "not_completed": num_not_completed, "logs": num_logs, } describe = _summary_property(_describe) @abstractmethod def drop_not_completed(self, *, unique_id: str | None = None) -> None: ... def _validate(self) -> dict[str, object]: members = self.members correct_md5 = len(members) missing_md5 = 0 for m in members: data = m.read() md5 = self.md5(m.unique_id) if md5 is None: missing_md5 += 1 correct_md5 -= 1 elif md5 != get_text_hexdigest(data): correct_md5 -= 1 incorrect_md5 = len(members) - correct_md5 - missing_md5 return { "md5_correct": correct_md5, "md5_incorrect": incorrect_md5, "md5_missing": missing_md5, "md5_legacy": self._count_legacy_checksums(), "has_log": len(self.logs) > 0, } def _count_legacy_checksums(self) -> int: """how many checksum files are still under the name both kinds shared Notes ----- Zero for a store that keeps no checksum files of its own. """ return 0 def validate(self) -> dict[str, object]: return _apply_summary_display(self._validate(), name="validate") @abstractmethod def md5(self, unique_id: str) -> str | None: """ Parameters ---------- unique_id name of data store member Returns ------- md5 checksum for the member, if available, None otherwise """ def write_citations(self, *, data: tuple[CitationBase, ...]) -> None: """Write citations to the data store. Subclasses should override.""" if not data: return import warnings warnings.warn( f"{type(self).__name__!r} does not support saving citations", UserWarning, stacklevel=2, ) def _summary_citations(self) -> list[dict]: """Return a list of dicts summarising stored citations.""" if type(self)._load_citations is DataStoreABC._load_citations: import warnings warnings.warn( f"{type(self).__name__!r} does not support saving citations", UserWarning, stacklevel=2, ) citations = self._load_citations() return [{"app": c.summary()[0], "citation": c.summary()[1]} for c in citations] summary_citations = _summary_property(_summary_citations) def write_bib(self, dest_path: str | Path) -> None: """Write stored citations as a BibTeX .bib file.""" citations = self._load_citations() if not citations: import warnings warnings.warn( "No citations stored in this data store", UserWarning, stacklevel=2, ) return from citeable import write_bibtex dest_path = Path(dest_path).expanduser().absolute() write_bibtex(citations, dest_path) def _load_citations(self) -> list[CitationBase]: """Load stored citations. Override in subclasses.""" return [] class DataMember(DataMemberABC): """Generic DataMember class, bound to a data store. All read operations delivered by the parent.""" def __init__(self, *, data_store: DataStoreABC, unique_id: str) -> None: self._data_store = data_store self._unique_id = str(unique_id) @property def data_store(self) -> DataStoreABC: return self._data_store @property def unique_id(self) -> str: return self._unique_id def summary_not_completeds( not_completed: list[DataMemberABC], deserialise: Callable[..., Any] | None = None, ) -> list[dict]: """ Parameters ---------- not_completed list of DataMember instances for notcompleted records deserialise a callable for converting not completed contents, the result of member.read() must be a json string """ err_pat = re.compile(r"[A-Z][a-z]+[A-Z][a-z]+\:.+") types = defaultdict(list) indices = "type", "origin" num_bytes = 0 for member in not_completed: record = member.read() if deserialise: record = deserialise(record) if isinstance(record, bytes): num_bytes += 1 continue record = deserialise_object(record) key = tuple(getattr(record, k, None) for k in indices) match = err_pat.findall(record.message) types[key].append([match[-1] if match else record.message, record.source]) if num_bytes == len(not_completed): return [] rows = [] maxtring = reprlib.aRepr.maxstring reprlib.aRepr.maxstring = 45 limit_len = 45 for key in types: msg_list, src_list = list(zip(*types[key], strict=False)) messages = reprlib.repr(", ".join(m.splitlines()[-1] for m in set(msg_list))) sources = ", ".join(s.splitlines()[-1] for s in src_list if s) if len(sources) > limit_len: idx = sources.rfind(",", None, limit_len) + 1 idx = idx if idx > 0 else limit_len sources = f"{sources[:idx]} ..." row = { "type": getattr(key[0], "value", key[0]), "origin": key[1], "message": messages, "num": len(types[key]), "source": sources, } rows.append(row) reprlib.aRepr.maxstring = maxtring # restoring original val return rows def _tidy_and_check_suffix(suffix: str | None) -> str: """tidies suffix by removing leading wildcards and dots""" suffix = suffix or "" suffix = re.sub(r"^[\s.*]+", "", suffix) # tidy the suffix if not suffix or suffix == "*": msg = "suffix is required for DataStoreDirectory and cannot be just a wildcard" raise ValueError(msg) return suffix class DataStoreDirectory(DataStoreABC): """data store backed by a directory on the filesystem""" def __init__( self, source: str | Path, mode: Mode | str = READONLY, suffix: str | None = None, limit: int | None = None, verbose: bool = False, ) -> None: self._mode = Mode(mode) source = Path(source) self._source = source.expanduser() self.suffix = _tidy_and_check_suffix(suffix) self._verbose = verbose self._source_check_create(self._mode) self._limit = limit def __contains__(self, item: object) -> bool: if not isinstance(item, str): return False # an item naming a subdirectory is a member id, exact as given. the # completion below is for bare caller input such as "brca1" if not Path(item).parent.name and not _special_suffixes.search(item): item = f"{item}.{self.suffix}" if self.suffix not in item else item return super().__contains__(item) def _source_check_create(self, mode: Mode) -> None: if not is_master_process(): return sub_dirs = [NOT_COMPLETED_TABLE, LOG_TABLE, MD5_TABLE] source = self.source if mode is READONLY: if not source.exists(): msg = f"'{source}' does not exist" raise OSError(msg) return if not source.exists(): source.mkdir(parents=True, exist_ok=True) for sub_dir in sub_dirs: (source / sub_dir).mkdir(parents=True, exist_ok=True) @property def source(self) -> Path: """path that references the data store""" return self._source @property def mode(self) -> Mode: """string that references datastore mode, override in subclass constructor""" return self._mode @property def limit(self) -> int | None: return self._limit def read(self, unique_id: str) -> str: """reads data corresponding to identifier""" with open_(self.source / unique_id) as infile: return infile.read() def drop_not_completed(self, *, unique_id: str | None = None) -> None: """remove not-completed records from the directory Parameters ---------- unique_id if provided, only drop the record with this identifier, otherwise drop all not-completed records """ # named by the rule that stored it, given the suffix # write_not_completed passes to _write target = self._record_name(unique_id, "json")[0] if unique_id else "" # members carry the subdirectory, so the comparison below needs the # same form. built once: write() drops a twin on every call wanted = _member_id(NOT_COMPLETED_TABLE, target) if target else "" nc_dir = self.source / NOT_COMPLETED_TABLE md5_dir = self.source / MD5_TABLE # the removals and the reset are one region, so a scan cannot run # against a half-emptied directory. it does NOT stop a caller that # already holds the list from watching it shrink: the members are # removed from that list rather than it being rebound. nor is it # atomic -- an unlink can still raise and leave it torn with self._cache_lock: # built only if a file under the shared name turns up, which # takes a scan and is worth nothing in a store without one completed_stems: set[str] | None = None for m in list(self.not_completed): # exact: an endswith test also matches a record whose name # merely ends with this one, such as abc1.json for c1.json if wanted and m.unique_id != wanted: continue file = nc_dir / Path(m.unique_id).name file.unlink() # a checksum is optional -- md5() returns None without one # and _validate counts it under md5_missing -- so a record # that has none is still droppable md5_file = md5_dir / _checksum_name(m.unique_id, completed=False) md5_file.unlink(missing_ok=True) # a file under the name both kinds shared is this record's # only when no completed record carries the stem. with one # there it cannot be attributed, and leaving it would have # it answer for that record once this one is gone legacy = md5_dir / _legacy_checksum_name(m.unique_id) if legacy.exists(): if completed_stems is None: completed_stems = { _record_stem(c.unique_id) for c in self.completed } if _record_stem(m.unique_id) not in completed_stems: legacy.unlink() self.not_completed.remove(m) if not target: # reset _not_completed to force not_completed to rebuild it. # limit makes it a view, so the rebuild may show records # this pass was not asked about self._not_completed: list[DataMemberABC] = [] @property def logs(self) -> list[DataMemberABC]: log_dir = self.source / LOG_TABLE return ( [ DataMember(data_store=self, unique_id=_member_id(LOG_TABLE, m.name)) for m in log_dir.glob("*") ] if log_dir.exists() else [] ) @property def completed(self) -> list[DataMemberABC]: # the lock spans check, scan and publish: a writer appending between # the scan and the publish would otherwise be discarded by it with self._cache_lock: if not self._completed: # built locally and assigned once: appending to # self._completed would publish a partly scanned list found: list[DataMemberABC] = [] # counts what it keeps, not what it looked at: the scan # now sees every entry in the directory, and limit # truncates the members for m in self.source.glob("*"): if not _is_record(m.name, self.suffix): continue found.append(DataMember(data_store=self, unique_id=m.name)) if self.limit and len(found) == self.limit: break self._completed = found return self._completed @property def not_completed(self) -> list[DataMemberABC]: with self._cache_lock: if not self._not_completed: found: list[DataMemberABC] = [] for m in (self.source / NOT_COMPLETED_TABLE).glob("*"): if not _is_record(m.name, "json"): continue found.append( DataMember( data_store=self, unique_id=_member_id(NOT_COMPLETED_TABLE, m.name), ), ) if self.limit and len(found) == self.limit: break self._not_completed = found return self._not_completed def _record_name(self, unique_id: str, suffix: str) -> tuple[str, str | None]: """the file name a record with this identifier is stored under Parameters ---------- unique_id identifier as given by the caller suffix format suffix the record is stored with Returns ------- the file name, and the compression suffix it carries if any Notes ----- The suffix names the file and the identifier supplies the stem, so the completed records of a ``.fasta.gz`` store are all ``.fasta.gz``. The stem is the identifier with a trailing format or compression suffix taken off, recognised as written: in a ``.fasta`` store ``id_0.FASTA`` keeps its extension and becomes ``id_0.FASTA.fasta``. """ name = f"{_record_stem(unique_id)}.{suffix}" return name, get_format_suffixes(name)[1] def _write( self, *, subdir: str, unique_id: str, suffix: str, data: str, ) -> DataMember | None: given = unique_id unique_id = self._record_name(unique_id, suffix)[0] member_id = _member_id(subdir, unique_id) # super().write refuses a read only store and an APPEND overwrite, # and both are more fundamental than a complaint about the name, # so they answer first super().write(unique_id=member_id, data=data) _check_identifier(given) _check_compression(given, suffix) # unique_id names a completed record whatever subdir holds, so this # can only speak for completed ones if not subdir and suffix != "log" and unique_id in self: return None # the newline is named for every suffix, not just the plain one. # the default of None is universal newlines, which on a write turns # each \n into os.linesep, so a compressed record left on it holds # \r\n on windows. reads undo that, which is why it stayed hidden, # but the file itself then differs from the one written elsewhere with open_(self.source / subdir / unique_id, mode="w", newline="\n") as out: out.write(data) if subdir == LOG_TABLE: return None member = DataMember(data_store=self, unique_id=member_id) md5 = get_text_hexdigest(data) # named for the kind as well as the record, so a completed record # and a not-completed one of the same name no longer share a file. # they still share one with a compressed record of that name, since # the stem drops the compression suffix checksum = _checksum_name(unique_id, completed=not subdir) with open_(self.source / MD5_TABLE / checksum, mode="w", newline="\n") as out: out.write(md5) return member def write(self, *, unique_id: str, data: str) -> DataMember: # type: ignore[override] """writes a completed record ending with .suffix Parameters ---------- unique_id unique identifier data text data to be written Returns ------- a member for this record Raises ------ ValueError if unique_id does not name a record, or names a compression this store does not write Notes ----- Drops any not-completed member corresponding to this identifier The store's suffix names the file, so any format suffix on unique_id is replaced by it. A compression suffix says how to read the record back, so it is refused rather than replaced. """ member = self._write( subdir="", unique_id=unique_id, suffix=self.suffix, data=data, ) # one region: the record leaves not_completed and enters completed # together, so no reader finds it in neither with self._cache_lock: self.drop_not_completed(unique_id=unique_id) if member is not None: self._append_once(self._completed, member) return member # type: ignore[return-value] def write_not_completed(self, *, unique_id: str, data: str) -> DataMember: # type: ignore[override] """writes a not completed record as json Parameters ---------- unique_id unique identifier data text data to be written Returns ------- a member for this record Raises ------ ValueError if unique_id does not name a record, or names a compression. These are written as plain json whatever the store's suffix is """ (self.source / NOT_COMPLETED_TABLE).mkdir(parents=True, exist_ok=True) member = self._write( subdir=NOT_COMPLETED_TABLE, unique_id=unique_id, suffix="json", data=data, ) # never None for this subdir, but _write is typed to allow it if member is not None: with self._cache_lock: self._append_once(self._not_completed, member) return member # type: ignore[return-value] def write_log(self, *, unique_id: str, data: str) -> None: # type: ignore[override] """writes a log file Parameters ---------- unique_id unique identifier data text data to be written Raises ------ ValueError if unique_id does not name a record, or names a compression. Logs are written as plain .log whatever the store's suffix is """ (self.source / LOG_TABLE).mkdir(parents=True, exist_ok=True) _ = self._write(subdir=LOG_TABLE, unique_id=unique_id, suffix="log", data=data) def md5(self, unique_id: str) -> str | None: """ Parameters ---------- unique_id name of data store member Returns ------- md5 checksum for the member, if available, None otherwise """ completed = Path(unique_id).parent.name != NOT_COMPLETED_TABLE path = self.source / MD5_TABLE / _checksum_name(unique_id, completed=completed) if path.exists(): return path.read_text() # a store written before the kinds were named keeps both under one # name, so nothing has to be rewritten for it to be readable legacy = self.source / MD5_TABLE / _legacy_checksum_name(unique_id) return legacy.read_text() if legacy.exists() else None def _count_legacy_checksums(self) -> int: md5_dir = self.source / MD5_TABLE return sum(_is_record(p.name, LEGACY_CHECKSUM) for p in md5_dir.glob("*")) def migrate_checksums(self) -> ChecksumMigration: """rename checksum files that predate the two kinds being named Returns ------- how many were renamed, and the stems of those that were not Notes ----- A file under the shared name holds whichever of the two records wrote last, which was never recorded, so one can be attributed only when a single record carries its stem. The rest are reported and left alone rather than guessed at. Requires ``mode="w"``. Read-only cannot rewrite anything, and append undertakes not to touch what is already in the store, which is what renaming these does. """ if self.mode is not OVERWRITE: msg = ( "migrating checksums rewrites files already in the store, " 'which needs mode="w"' ) raise OSError(msg) md5_dir = self.source / MD5_TABLE # read from the directories rather than the member properties, # which limit truncates and which answer from a cache another # writer cannot have updated. attributing a file to a record that # is merely out of view is the guess this exists to avoid completed = { _record_stem(p.name) for p in self.source.glob("*") if _is_record(p.name, self.suffix) } not_completed = { _record_stem(p.name) for p in (self.source / NOT_COMPLETED_TABLE).glob("*") if _is_record(p.name, "json") } migrated = 0 ambiguous: list[str] = [] orphaned: list[str] = [] superseded: list[str] = [] legacies = (p for p in md5_dir.glob("*") if _is_record(p.name, LEGACY_CHECKSUM)) for legacy in sorted(legacies): stem = legacy.name.removesuffix(f".{LEGACY_CHECKSUM}") kinds = (stem in completed, stem in not_completed) if all(kinds): ambiguous.append(stem) continue if not any(kinds): orphaned.append(stem) continue suffix = COMPLETED_CHECKSUM if kinds[0] else NOT_COMPLETED_CHECKSUM target = md5_dir / f"{stem}.{suffix}" # a file already under the current name was written for that # record by this version, so it is the authority. renaming over # it replaces a checksum known to be right with one that may # belong to a record since dropped if target.exists(): superseded.append(stem) continue legacy.rename(target) migrated += 1 return { "migrated": migrated, "ambiguous": ambiguous, "orphaned": orphaned, "superseded": superseded, } def write_citations(self, *, data: tuple[CitationBase, ...]) -> None: if not data: return from citeable import to_jsons path = self.source / CITATIONS_FILE path.write_text(to_jsons(data)) def _load_citations(self) -> list[CitationBase]: from citeable import from_jsons path = self.source / CITATIONS_FILE if not path.exists(): return [] return from_jsons(path.read_text()) class ReadOnlyDataStoreZipped(DataStoreABC): """read-only data store backed by a zip archive""" def __init__( self, source: str | Path, mode: Mode | str = READONLY, suffix: str | None = None, limit: int | None = None, verbose: bool = False, ) -> None: self._mode = Mode(mode) if self._mode is not READONLY: msg = "this is a read only data store" raise ValueError(msg) self.suffix = _tidy_and_check_suffix(suffix) source = Path(source) self._source = source.expanduser() if not self._source.exists(): msg = f"{self._source!s} does not exit" raise OSError(msg) self._verbose = verbose self._limit = limit @property def limit(self) -> int | None: return self._limit @property def mode(self) -> Mode: return self._mode @property def source(self) -> Path: return self._source def read(self, unique_id: str) -> str | bytes: """reads data corresponding to identifier from the zip archive""" import zipfile member_path = str(Path(self.source.stem, unique_id)).replace("\\", "/") with zipfile.ZipFile(self.source) as archive: raw = archive.open(member_path) wrapped = TextIOWrapper(raw, encoding="latin-1") return wrapped.read() def _iter_matches(self, subdir: str, suffix: str | None) -> Iterator[Path]: """archive entries under subdir, stored with this suffix Parameters ---------- subdir directory the entry sits in. An empty string means no check at all, so entries at every depth are considered, which is why a zipped store's completed can pick up a .json out of not_completed and report it twice suffix suffix the entry is stored under, None for any Notes ----- A suffix rather than a glob pattern, because Path.match folds case on Windows and an archive has the same membership wherever it is opened. """ import zipfile with zipfile.ZipFile(self._source) as archive: names = archive.namelist() for name in names: p = Path(name) if subdir and p.parent.name != subdir: continue if p.name.startswith("."): continue if suffix is None or _is_record(p.name, suffix): yield p @property def completed(self) -> list[DataMemberABC]: # this store is read only, so publishing once is already enough for # correctness. the lock collapses N concurrent readers re-parsing the # zip central directory into one scan, and keeps the four member # properties uniform with self._cache_lock: if not self._completed: found: list[DataMemberABC] = [] num_matches = 0 for name in self._iter_matches("", self.suffix): num_matches += 1 member = DataMember(data_store=self, unique_id=name.name) found.append(member) if self.limit and num_matches >= self.limit: break self._completed = found return self._completed @property def not_completed(self) -> list[DataMemberABC]: with self._cache_lock: if not self._not_completed: found: list[DataMemberABC] = [] num_matches = 0 for name in self._iter_matches(NOT_COMPLETED_TABLE, "json"): num_matches += 1 member = DataMember( data_store=self, unique_id=_member_id(NOT_COMPLETED_TABLE, name.name), ) found.append(member) if self.limit and num_matches >= self.limit: break self._not_completed = found return self._not_completed @property def logs(self) -> list[DataMemberABC]: logs: list[DataMemberABC] = [] for name in self._iter_matches(LOG_TABLE, None): m = DataMember(data_store=self, unique_id=_member_id(LOG_TABLE, name.name)) logs.append(m) return logs def md5(self, unique_id: str) -> str | None: """ Parameters ---------- unique_id name of data store member Returns ------- md5 checksum for the member, if available, None otherwise """ completed = Path(unique_id).parent.name != NOT_COMPLETED_TABLE # the legacy name second: an archive cannot be rewritten, so a # store zipped before the kinds were named falls back forever candidates = ( _checksum_name(unique_id, completed=completed), _legacy_checksum_name(unique_id), ) # one pass: _iter_matches opens the archive and reads its central # directory each time it is called, so asking it per candidate # would open the file twice to answer one question found = { name.name: name for name in self._iter_matches(MD5_TABLE, None) if name.name in candidates } for md5_name in candidates: if md5_name not in found: continue m = DataMember(data_store=self, unique_id=_member_id(MD5_TABLE, md5_name)) result = m.read() return result if isinstance(result, str) else result.decode() return None def _count_legacy_checksums(self) -> int: return len(list(self._iter_matches(MD5_TABLE, LEGACY_CHECKSUM))) def drop_not_completed(self, *, unique_id: str | None = None) -> None: """not supported on read-only zip data stores""" msg = "zip data stores are read only" raise TypeError(msg) def write(self, *, unique_id: str, data: str | bytes) -> None: msg = "zip data stores are read only" raise TypeError(msg) def write_not_completed(self, *, unique_id: str, data: str | bytes) -> None: msg = "zip data stores are read only" raise TypeError(msg) def write_log(self, *, unique_id: str, data: str | bytes) -> None: msg = "zip data stores are read only" raise TypeError(msg) def write_citations(self, *, data: tuple[CitationBase, ...]) -> None: msg = "zip data stores are read only" raise TypeError(msg) def _load_citations(self) -> list[CitationBase]: import zipfile from citeable import from_jsons target = str(Path(self.source.stem, CITATIONS_FILE)).replace("\\", "/") try: with zipfile.ZipFile(self.source) as archive: data = archive.read(target).decode("utf-8") return from_jsons(data) except KeyError: return [] def get_unique_id(name: object) -> str | None: """strips any format suffixes from name""" if (name := get_data_source(name)) is None: return None suffixes = ".".join(sfx for sfx in get_format_suffixes(name) if sfx) # name is data as often as it is a path, so the suffix can be anything at # all. Interpolating it into a pattern let '(1)' or '.*' compile. tail = f".{suffixes}" return name[: -len(tail)] if suffixes and name.endswith(tail) else name def set_id_from_source(func: Callable[..., Any] | None) -> None: """Register a custom function for extracting unique IDs from data objects. The registered function is consulted as the default by :meth:`AppBase.as_completed` and :meth:`WriterApp.apply_to` to derive a unique identifier for each input, and by :class:`NotCompleted` to normalise the ``source=`` keyword on error records. Pass ``None`` to clear the registration and restore the built-in :func:`get_unique_id`. Parameters ---------- func A callable taking a single data object and returning a string identifier (or ``None`` if no identifier can be extracted). The callable must be picklable if scinexus apps will be executed in parallel via ``loky`` / MPI. Notes ----- Per-call overrides via the ``id_from_source`` keyword on :meth:`as_completed` and :meth:`apply_to` still take precedence over the registered function. Register before constructing apps for the cleanest behaviour. """ global _id_from_source_func # noqa: PLW0603 _id_from_source_func = func def get_id_from_source() -> Callable[..., Any]: """Return the active unique-ID extractor. Returns the function previously passed to :func:`set_id_from_source`, or :func:`get_unique_id` if nothing has been registered. """ return _id_from_source_func or get_unique_id @singledispatch def get_data_source(data: object) -> str | None: source = getattr(data, "source", None) return None if source is None else get_data_source(source) @get_data_source.register def _(data: str) -> str | None: return get_data_source(Path(data)) @get_data_source.register def _(data: Path) -> str | None: return data.name @get_data_source.register def _(data: dict) -> str | None: try: source = data.get("info", {})["source"] except KeyError: source = data.get("source", None) # noqa return get_data_source(source) @get_data_source.register def _(data: DataMemberABC) -> str | None: return str(data.unique_id) def make_record_for_json( identifier: str, data: Any, completed: bool ) -> dict[str, object]: """returns a dict for storage as json""" with contextlib.suppress(AttributeError): data = data.to_rich_dict() data = json.dumps(data) return {"identifier": identifier, "data": data, "completed": completed} def load_record_from_json(data: Any) -> tuple[str, Any, bool]: """returns identifier, data, completed status from json string""" if isinstance(data, str): data = json.loads(data) value = data["data"] if isinstance(value, str): with contextlib.suppress(json.JSONDecodeError): value = json.loads(value) return data["identifier"], value, data["completed"] cogent3-scinexus-b16bc06/src/scinexus/deserialise.py000066400000000000000000000067111525761402500226270ustar00rootroot00000000000000import json import re from collections.abc import Callable from importlib import import_module from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast if TYPE_CHECKING: # pragma: no cover from scinexus.io_util import PathType P = ParamSpec("P") R = TypeVar("R") _deserialise_func_map: dict[str, Callable[..., Any]] = {} class register_deserialiser: """ registration decorator for functions to inflate objects that were serialised using json. Functions are added to a dict which is used by the deserialise_object() function. The type string(s) must uniquely identify the appropriate value for the dict 'type' entry, e.g. 'scinexus.composable.NotCompleted'. Parameters ---------- args must be unique """ def __init__(self, *args: str) -> None: for type_str in args: if not isinstance(type_str, str): msg = f"{type_str!r} is not a string" raise TypeError(msg) if type_str in _deserialise_func_map: msg = f"{type_str!r} already in {list(_deserialise_func_map)}" raise ValueError(msg) self._type_str = args def __call__(self, func: Callable[P, R]) -> Callable[P, R]: for type_str in self._type_str: _deserialise_func_map[type_str] = func return func def get_class(provenance: str) -> type: """resolve a dotted provenance string to the corresponding class Parameters ---------- provenance fully qualified class name, e.g. ``'scinexus.composable.NotCompleted'`` """ index = provenance.rfind(".") if index <= 0: msg = ( f"invalid provenance string {provenance!r}, expected 'module.class' format" ) raise ValueError(msg) klass = provenance[index + 1 :] nc = "NotCompleted" klass = nc if nc in klass else klass mod = import_module(provenance[:index]) return getattr(mod, klass) _pat = re.compile("[a-z]") def str_to_version(v: str) -> tuple[str, ...]: """parse a version string into a tuple of components Parameters ---------- v version string, e.g. ``'2024.8.7a3'`` """ if letter := _pat.search(v): return tuple( f"{v[: letter.start()]}.{letter.group()}.{letter.end():}".split(".") ) return () def deserialise_object(data: "PathType | str | dict[str, Any]") -> Any: """ deserialises from json Parameters ---------- data path to json file, json string or a dict Returns ------- If the dict from json.loads does not contain a "type" key, the object will be returned as is. Otherwise, it will be deserialised to the original object. Notes ----- The value of the "type" key is used to identify the specific function for recreating the original instance. """ from scinexus.io_util import open_, path_exists if path_exists(path := cast("PathType", data)): with open_(path) as infile: data = json.load(infile) if isinstance(data, str): data = json.loads(str(data)) data = cast("dict[str, Any]", data) type_ = data.get("type", None) if hasattr(data, "get") else None if type_ is None: return data for type_str, func in _deserialise_func_map.items(): # noqa: B007 if type_str in type_: break else: msg = f"deserialising '{type_}' from json" raise NotImplementedError(msg) return func(data) cogent3-scinexus-b16bc06/src/scinexus/io.py000066400000000000000000000156731525761402500207540ustar00rootroot00000000000000import contextlib import json import pickle import typing from collections.abc import Callable from gzip import compress as gzip_compress from gzip import decompress as gzip_decompress from pathlib import Path from typing import Any from scinexus.deserialise import deserialise_object from .composable import define_app from .data_store import ( READONLY, DataStoreABC, DataStoreDirectory, Mode, ReadOnlyDataStoreZipped, ) from .sqlite_data_store import _MEMORY, DataStoreSqlite _datastore_reader_map: dict[str | None, type[DataStoreABC]] = {} class register_datastore_reader: """ registration decorator for read only data store classes The registration key must be a string that of the file format suffix (more than one suffix can be registered at a time). Parameters ---------- args must be unique, a preceding '.' will be added if not already present """ def __init__(self, *args: str | None) -> None: suffixes: list[str | None] = list(args) for i, sfx in enumerate(suffixes): if sfx is None: if sfx in _datastore_reader_map: msg = f"{sfx!r} already in {list(_datastore_reader_map)}" raise ValueError(msg) # noqua: TRY004 continue if not isinstance(sfx, str): msg = f"{sfx!r} is not a string" raise TypeError(msg) if sfx.strip() == sfx and not sfx: msg = "cannot have white-space suffix" raise ValueError(msg) # noqua: TRY004 if suffix := sfx.strip(): suffix = suffix if suffix[0] == "." else f".{suffix}" if suffix in _datastore_reader_map: msg = f"{suffix!r} already in {list(_datastore_reader_map)}" raise ValueError(msg) # noqua: TRY004 suffixes[i] = suffix self._type_str = tuple(suffixes) def __call__(self, func: type[DataStoreABC]) -> type[DataStoreABC]: for type_str in self._type_str: _datastore_reader_map[type_str] = func return func # register the main readers register_datastore_reader("zip")(ReadOnlyDataStoreZipped) register_datastore_reader(None)(DataStoreDirectory) register_datastore_reader("sqlitedb")(DataStoreSqlite) def open_data_store( base_path: str | Path, suffix: str | None = None, limit: int | None = None, mode: str | Mode = READONLY, **kwargs: Any, ) -> DataStoreABC: """returns DataStore instance of a type specified by the path suffix Parameters ---------- base_path path to directory or db suffix suffix of filenames limit the number of matches to return mode opening mode, either r, w, a as per file opening modes """ import zipfile mode = Mode(mode) if not isinstance(suffix, str | type(None)): msg = f"suffix {type(suffix)} not one of string or None" raise TypeError(msg) kwargs = {"limit": limit, "mode": mode, "suffix": suffix, **kwargs} base_path = Path(base_path) base_path = ( base_path if base_path.name == ":memory:" else base_path.expanduser().absolute() ) if base_path.is_dir(): ds_suffix = None elif base_path.suffix == ".sqlitedb" or base_path.name == _MEMORY: ds_suffix = ".sqlitedb" kwargs.pop("suffix") elif zipfile.is_zipfile(base_path): ds_suffix = ".zip" elif base_path.suffix: ds_suffix = base_path.suffix else: # triggered when mode="w" ds_suffix = None if base_path.name == _MEMORY and mode is READONLY: msg = "in memory readonly sqlitedb" raise NotImplementedError(msg) if ds_suffix is None and suffix is None: msg = "a suffix is required if using a directory data store" raise ValueError(msg) klass = _datastore_reader_map[ds_suffix] return klass(base_path, **kwargs) @define_app(skip_not_completed=False) def pickle_it(data: typing.Any) -> bytes: """Serialises data using pickle.""" return pickle.dumps(data) @define_app(skip_not_completed=False) def unpickle_it(data: bytes) -> typing.Any: "Deserialises pickle data." return pickle.loads(data) # noqa: S301 @define_app(skip_not_completed=False) class compress: """Compresses bytes data.""" def __init__(self, compressor: Callable[..., Any] = gzip_compress) -> None: """ Parameters ---------- compressor function for compressing bytes data, defaults to gzip """ self.compressor = compressor def main(self, data: bytes) -> bytes: """compress bytes data using the configured compressor""" return self.compressor(data) @define_app(skip_not_completed=False) class decompress: """Decompresses data.""" def __init__(self, decompressor: Callable[..., Any] = gzip_decompress) -> None: """ Parameters ---------- decompressor a function for decompression, defaults to the gzip decompress function """ self.decompressor = decompressor def main(self, data: bytes) -> bytes: """decompress bytes data using the configured decompressor""" return self.decompressor(data) def as_dict(obj: typing.Any) -> dict[str, typing.Any]: """returns result of to_rich_dict method if it exists""" with contextlib.suppress(AttributeError): obj = obj.to_rich_dict() return obj @define_app(skip_not_completed=False) class to_primitive: """convert an object to primitive python types suitable for serialisation""" def __init__(self, convertor: Callable[..., Any] = as_dict) -> None: """ Parameters ---------- convertor callable that converts an object to primitive types, defaults to ``as_dict`` """ self.convertor = convertor def main(self, data: typing.Any) -> typing.Any: """returns dict from an object""" return self.convertor(data) @define_app(skip_not_completed=False) class from_primitive: """deserialises from primitive python types""" def __init__(self, deserialiser: Callable[..., Any] = deserialise_object) -> None: """ Parameters ---------- deserialiser callable that recreates an object from primitive types, defaults to ``deserialise_object`` """ self.deserialiser = deserialiser def main(self, data: typing.Any) -> typing.Any: """either json or a dict from an object""" return self.deserialiser(data) @define_app def to_json(data: dict[str, Any]) -> str: """Convert primitive python types to json string.""" return json.dumps(data) @define_app def from_json(data: str) -> dict[str, Any]: """Convert json string to primitive python types.""" return json.loads(data) DEFAULT_DESERIALISER = unpickle_it() + from_primitive() DEFAULT_SERIALISER = to_primitive() + pickle_it() cogent3-scinexus-b16bc06/src/scinexus/io_util.py000066400000000000000000001340601525761402500220010ustar00rootroot00000000000000from __future__ import annotations import codecs import contextlib import functools import locale import re import shutil import uuid from bz2 import open as bzip_open from gzip import open as gzip_open from io import BufferedIOBase, BytesIO, TextIOWrapper from lzma import open as lzma_open from os import PathLike from pathlib import Path, PurePath from tempfile import mkdtemp from typing import ( IO, TYPE_CHECKING, Any, Generic, Literal, NamedTuple, TypeVar, cast, overload, ) from urllib.parse import ParseResult, urlparse from urllib.request import urlopen from charset_normalizer import detect _wout_period = re.compile(r"^\.") if TYPE_CHECKING: from collections.abc import Callable, Iterator from types import TracebackType PathType = str | PathLike[Any] | PurePath | Path _StrOrBytes = TypeVar("_StrOrBytes", str, bytes) class _Separators(NamedTuple, Generic[_StrOrBytes]): """line separators matching the mode a file object was opened in""" boundaries: _StrOrBytes newline: _StrOrBytes carriage_return: _StrOrBytes empty: _StrOrBytes # str.splitlines() breaks on all of these, bytes.splitlines() breaks # only on newline and carriage return _TEXT_SEPARATORS = _Separators( "\n\r\v\f\x1c\x1d\x1e\x85\N{LINE SEPARATOR}\N{PARAGRAPH SEPARATOR}", "\n", "\r", "", ) _BINARY_SEPARATORS = _Separators(b"\n\r", b"\n", b"\r", b"") @functools.singledispatch def is_url(path: str | bytes | Path | PathLike | ParseResult) -> bool: # noqa: ARG001 """whether a path is a url""" return False @is_url.register def _(path: str) -> bool: return is_url(urlparse(path)) @is_url.register def _(path: bytes) -> bool: return is_url(urlparse(path.decode("utf8"))) @is_url.register def _(path: ParseResult) -> bool: return path.scheme in {"http", "https", "file"} def _get_compression_open( path: PathType | None = None, compression: str | None = None, ) -> Callable[..., Any] | None: """returns function for opening compression formats Parameters ---------- path file path or url compression file compression suffix Returns ------- function for opening compressed files or None if unknown compression """ if not (path or compression): msg = "either path or compression argument must be provided" raise ValueError(msg) if compression is None and path is not None: _, compression = get_format_suffixes(path) return None if compression is None else _compression_handlers.get(compression) def _detect_encoding(path: PathType) -> str | None: """returns the encoding a text read of path will decode with Parameters ---------- path file path, decompressed first if it names a compression Returns ------- the name of the encoding, or None where the content identifies none and the opener would take that as the locale default Notes ----- The name is sniffed from the first hundred bytes. Where that identifies nothing the answer is whatever the opener for the path would then do, which is the locale default for every suffix but zip. open_zip substitutes latin-1, which decodes any byte, so a caller reading the bytes itself has to substitute it too rather than decode the same archive differently from open_. """ op = _get_compression_open(path) or open with op(path, mode="rb") as infile: data = infile.read(100) encoding = cast("str | None", detect(data)["encoding"]) if encoding is None and get_format_suffixes(path)[1] == "zip": return "latin-1" return encoding def _check_binary_mode_args(encoding: str | None, kwargs: dict[str, Any]) -> None: """raises if a decoding argument is paired with a binary mode Parameters ---------- encoding the encoding the caller named, None meaning they named none kwargs the remaining arguments, inspected for errors and newline. Ones whose value is None are removed, having named nothing Notes ----- builtin open, gzip, bz2 and lzma all raise ValueError here, and all of them test the value against None rather than asking whether the argument was passed, so that a caller relaying an unset argument of its own is not rejected for it. The openers that do their own decoding follow them on both counts. """ named = ["encoding"] if encoding is not None else [] for name in ("errors", "newline"): if name not in kwargs: continue if kwargs[name] is None: del kwargs[name] else: named.append(name) if named: msg = f"binary mode does not take {', '.join(named)}" raise ValueError(msg) def open_zip(filename: PathType | IO[Any], mode: str = "r", **kwargs: Any) -> IO[Any]: """open a single member zip-compressed file Parameters ---------- filename path to the archive, or, for a read, an open stream of it. One that cannot seek is read into memory and closed, since a zip cannot be read front to back. A write raises TypeError for a stream, having an archive to create rather than one to read mode a read mode returns the member, a write mode returns an atomic_write() instance kwargs encoding, errors and newline are used for the decoding of a text read. A text write hands all three to the file it opens, along with anything else it is given. On a read the rest go to ZipFile.open, which takes pwd and force_zip64 Note ---- A read raises ValueError if the archive holds more than one record. A read in a text mode decodes with the given encoding, falling back to latin-1, which decodes any byte. A read in a binary mode returns the member itself and does no decoding. A binary mode raises ValueError if given an encoding, errors or newline whose value is not None. A None means the caller named nothing, as it does for builtin open. """ mode = mode or "r" binary_mode = "b" in mode mode = mode[:1] # a path has no seekable attribute and a stream does, which is how # the two are told apart without asking the caller is_stream = hasattr(filename, "seekable") encoding = kwargs.pop("encoding", None) if binary_mode: _check_binary_mode_args(encoding, kwargs) if mode.startswith("w"): if is_stream: # atomic_write would reach Path(path) with it and complain # about __fspath__, naming its own parameter rather than # what the caller did msg = "a write needs a path to the archive, not an open stream" raise TypeError(msg) # mode has been truncated to its first letter, so put the b back # for a binary write. what is left goes to the file the writes # land in, under its own argument rather than splatted, so that # a name like tmpdir cannot bind to a parameter of atomic_write write_mode = "wb" if binary_mode else mode return atomic_write( # type: ignore[return-value] cast("PathType", filename), mode=write_mode, in_zip=True, encoding=encoding, open_kwargs=kwargs, ) from zipfile import ZipFile # ZipFile.open takes pwd and force_zip64 and nothing else, so the # arguments that belong to the decoding are taken out here. the # guard above means a binary mode has none of them left to take text_kwargs = { name: kwargs.pop(name) for name in ("errors", "newline") if name in kwargs } # latin-1 decodes any byte, so it is the fallback when the caller # names no encoding. the test is against None rather than falsiness, # so that an empty encoding reaches the codec lookup and is rejected # here as it is for the other suffixes encoding = encoding if encoding is not None else "latin-1" mode = mode.strip("t") # a zip is read back to front, ZipFile seeks to the end to find the # central directory, so a stream that cannot seek has to be held in # memory first. an http response is such a stream, and reporting it # as not being a zip is what ZipFile does with one if is_stream: stream = cast("IO[bytes]", filename) if not stream.seekable(): # draining it is what takes ownership, and from there it is # closed whether the read finishes or not: nothing else # holds it, so a read that fails part way, as a network # stream that drops does, would otherwise leave it open with # the exception going past it with contextlib.closing(stream): filename = BytesIO(stream.read()) with ZipFile(filename) as zf: if len(zf.namelist()) != 1: msg = "Archive is supposed to have only one record." raise ValueError(msg) opened = zf.open( zf.namelist()[0], mode=cast("Literal['r', 'w']", mode), **kwargs, ) if binary_mode: return opened # the member is open and is not the caller's to close until it # is returned, so a wrapper that rejects the encoding with # LookupError or the newline with ValueError has to release it. # the cleanup is registered rather than hung off those two # names, so that it also runs for whatever else may come out of # the constructor with contextlib.ExitStack() as cleanup: cleanup.callback(opened.close) wrapper = TextIOWrapper(opened, encoding=encoding, **text_kwargs) cleanup.pop_all() return wrapper _compression_handlers: dict[str, Callable[..., Any]] = { "gz": gzip_open, "bz2": bzip_open, "zip": open_zip, "xz": lzma_open, "lzma": lzma_open, } def open_(filename: PathType, mode: str = "rt", **kwargs: Any) -> IO[Any]: """open that handles different compression Parameters ---------- filename path or url, if a url delegates processing to open_url mode standard file opening mode kwargs passed to open functions Returns ------- an object compatible with the file protocol Notes ----- A bare "r" or "w" means text, for a compressed file as much as for an uncompressed one, so it agrees with builtin open rather than with gzip, bz2 and lzma, which take a bare mode as binary. Use "rb" or "wb" for bytes. This applies to "r" and "w" only. A bare "a" is still binary for gz, bz2, xz and lzma and text for an uncompressed file, and neither "a" nor "x" works on a zip at all, since a zip member can only be opened for reading or writing. """ if not filename: msg = f"{filename} not a valid file name or url" raise ValueError(msg) if is_url(filename): return open_url(filename, mode=mode, **kwargs) # type: ignore[arg-type] mode = mode or "rt" if mode in {"r", "w"}: # gzip, bz2 and lzma take a bare "r" or "w" as binary, where # builtin open and open_zip take it as text. Say which is meant, # so that r and w agree across the suffixes and, for a read, so # the encoding worked out below is one the handler will accept. # "a" and "x" are left alone, open_zip cannot support them mode = f"{mode}t" filename = Path(filename).expanduser() op = _get_compression_open(filename) or open encoding = kwargs.pop("encoding", None) # the pop above means kwargs can no longer hold an encoding, so ask # the value whether the caller named one need_encoding = mode.startswith("r") and "b" not in mode if need_encoding and encoding is None: encoding = _detect_encoding(filename) return op(filename, mode, encoding=encoding, **kwargs) def _decompressed(response: IO[Any], compression: str | None) -> IO[Any]: """returns a reader for the content of a url response Parameters ---------- response an open url response compression compression suffix of the url, None leaves the response alone Notes ----- The response itself is returned, not closed, so a caller that has to abandon the result still has the object whose close releases the connection. """ if compression: opener = _get_compression_open(compression=compression) if opener is not None: # the handlers disagree on what their default mode means, # open_zip's "r" is text while the others are binary, so ask # for bytes and leave the text decision to the caller return cast("IO[Any]", opener(response, mode="rb")) return response class _ClosingReader(BufferedIOBase): """a binary reader that also closes the stream it was built over Notes ----- gzip, bz2 and lzma close only a file they opened themselves, so a file object handed to them is left open when the reader over it is closed, and a zip member does the same. A url response decompressed through one of them therefore stays open once open_url has returned, with no handle on it anywhere to close it with. It is an io.BufferedIOBase rather than a bare delegating object so that it is still an I/O object to anything that asks. typeguard tests IO[bytes] and IO[str] with isinstance against the io ABCs, so a plain proxy is rejected by every one of them, and a text read is a real TextIOWrapper built over this rather than a proxy around one. one. """ def __init__(self, reader: IO[bytes], stream: IO[Any]) -> None: """ Parameters ---------- reader the object reads are served from stream the one underneath it, closed after it """ self._reader = reader self._stream = stream def __getattr__(self, name: str) -> Any: # only reached when normal lookup fails, and the lookup of # _reader must not come back here when __init__ has not run, as # it has not for the instance copy.copy makes with __new__ reader = self.__dict__.get("_reader") if reader is None: raise AttributeError(name) return getattr(reader, name) def readable(self) -> bool: return True def read(self, size: int | None = -1) -> bytes: return self._reader.read(-1 if size is None else size) def read1(self, size: int = -1) -> bytes: return self.read(size) def seekable(self) -> bool: return self._reader.seekable() def seek(self, offset: int, whence: int = 0) -> int: return self._reader.seek(offset, whence) def tell(self) -> int: return self._reader.tell() def close(self) -> None: """closes the reader, then the stream under it Notes ----- The two are looked for rather than assumed. IOBase calls this from __del__, and an instance that never went through __init__ has neither, which is what copy.copy builds before it looks for __setstate__. An exception in __del__ cannot be raised, so it would be reported as an unraisable rather than handled. """ reader = self.__dict__.get("_reader") stream = self.__dict__.get("_stream") try: if reader is not None: reader.close() finally: try: if stream is not None: stream.close() finally: super().close() def open_url(url: str | ParseResult, mode: str = "rt", **kwargs: Any) -> IO[Any]: """open a url Parameters ---------- url A url of file in http or https web address mode mode of reading file, 'rb', 'rt', 'r' kwargs encoding, errors and newline for a text mode, used to decode the response. An encoding overrides the charset named by the response headers Raises ------ Raises IOError if mode is write or it's not a url. Raises ValueError if encoding, errors or newline is given with a binary mode. A value of None counts as not given, as it does for builtin open. Returns ------- file object which reads binary if "b" in mode, else text. """ _, compression = get_format_suffixes( url.path if isinstance(url, ParseResult) else url, ) mode = mode or "r" if "r" not in mode: msg = "opening a url only allowed in read mode" raise OSError(msg) if not is_url(url): msg = f"URL scheme must be http, https or file, not {str(url)[:20]!r}" raise OSError(msg) binary_mode = "b" in mode encoding = kwargs.pop("encoding", None) if binary_mode: # an encoding of None is not an argument: open_ hands its kwargs # on before it pops the encoding, so a None arrives here for any # binary read _check_binary_mode_args(encoding, kwargs) if kwargs: # the guard above leaves nothing that belongs to a text mode, # so anything still here is a name the function does not # take. a binary mode has nothing to hand it to, where a # text mode would at least reach TextIOWrapper and be told # off by it, so raise the error that would have come from # there rather than ignore the argument msg = f"open_url() got an unexpected keyword argument {min(kwargs)!r}" raise TypeError(msg) url_parsed = url if isinstance(url, ParseResult) else urlparse(url) response = urlopen(url_parsed.geturl(), timeout=10) # the request has been made, and until the reader is returned there # is no handle on the response for anyone else to close, so a # failure between here and there leaks a socket. The wrapper gives # LookupError for an unknown codec, ValueError for an illegal # newline and TypeError for an unknown argument, and opening a zip # adds BadZipFile and the ValueError for a multi-member archive. # That list is what has been seen rather than what can happen, so # the cleanup is registered rather than hung off it with contextlib.ExitStack() as cleanup: cleanup.callback(response.close) # the charset is taken before the decompression, which for a # zip that cannot seek has already closed the response by the # time it returns if not binary_mode and encoding is None: encoding = response.headers.get_content_charset() source = _decompressed(response, compression) if compression: # without a compression the source is the response itself, # and closing it, or a TextIOWrapper over it, closes it. # With one there is a decompressor in between that will not, # so the ownership is spelled out. It goes on the binary # layer so that a text read is a TextIOWrapper over this # rather than something wrapped around a TextIOWrapper # typeshed models BufferedIOBase as an IOBase and not as an # IO[bytes], where at runtime it answers to both, so the # cast says what isinstance already agrees with source = cast("IO[Any]", _ClosingReader(source, response)) reader = source if binary_mode else TextIOWrapper(source, encoding, **kwargs) cleanup.pop_all() return reader def _path_relative_to_zip_parent(zip_path: Path, member_path: Path) -> Path: """returns member_path relative to zip_path Parameters ---------- zip_path: Path member_path: Path Notes ----- with zip_path = "parentdir/named.zip", then member_path="named/member.tsv" or path="member.tsv" will return "named/member.tsv" """ zip_name = zip_path.name.replace(".zip", "") if zip_name not in member_path.parts: return Path(zip_name) / member_path return Path(*member_path.parts[member_path.parts.index(zip_name) :]) class atomic_write: """performs atomic write operations, cleans up if fails""" def __init__( self, path: PathType, tmpdir: PathType | None = None, in_zip: PathType | bool | None = None, mode: str = "w", encoding: str | None = None, *, open_kwargs: dict[str, Any] | None = None, ) -> None: """ Parameters ---------- path path to file, or relative to directory specified by in_zip tmpdir directory where temporary file will be created in_zip path to the zip archive containing path, e.g. if in_zip="path/to/data.zip", then path="data/seqs.tsv" Decompressing the archive will produce the "data/seqs.tsv" mode file writing mode encoding text encoding open_kwargs further arguments for open_, so errors and newline reach the file the writes go to. A dict rather than **kwargs, so that a name matching one of the parameters above cannot bind to it instead """ path = Path(path).expanduser() _, cmp = get_format_suffixes(path) zip_path: Path | None = None if in_zip: if isinstance(in_zip, bool): zip_path = path if cmp == "zip" else None else: zip_path = Path(in_zip) if zip_path and cmp == "zip": path = Path(str(path)[: str(path).rfind(".zip")]) if zip_path: path = _path_relative_to_zip_parent(zip_path, path) self._path = path self._cmp = cmp self._mode = mode self._file: IO[Any] | None = None self._encoding = encoding self._open_kwargs = open_kwargs or {} self._in_zip = zip_path self._tmppath = self._make_tmppath(tmpdir) self.succeeded: bool | None = None self._close_func = ( self._close_rename_zip if zip_path else self._close_rename_standard ) def _make_tmppath(self, tmpdir: PathType | None) -> Path: """returns path of temporary file Parameters ---------- tmpdir: Path to directory Returns ------- full path to a temporary file Notes ----- Uses a random uuid as the file name, adds suffixes from path """ suffixes = ( "".join(self._path.suffixes) if not self._in_zip else "".join(self._path.suffixes[:-1]) ) parent = self._in_zip.parent if self._in_zip else self._path.parent if not parent.exists(): raise OSError(f"Parent dir '{parent}' of provided path does not exist") name = f"{uuid.uuid4()}{suffixes}" tmpdir = Path(mkdtemp(dir=parent)) if tmpdir is None else Path(tmpdir) if not tmpdir.exists(): msg = f"{tmpdir} directory does not exist" raise FileNotFoundError(msg) return tmpdir / name def _get_fileobj(self) -> IO[Any]: """returns file to be written to""" if self._file is None: # __init__ has already made the directory to write into, and # a failure here means the caller never gets the object # whose exit or close would have removed it again. an # argument the open will not take is TypeError, an unknown # codec LookupError and a text-only argument under a binary # mode ValueError, but the open can also fail for reasons # that have nothing to do with the arguments, so the cleanup # is registered rather than tied to those three with contextlib.ExitStack() as cleanup: cleanup.callback(shutil.rmtree, self._tmppath.parent) self._file = open_( self._tmppath, self._mode, encoding=self._encoding, **self._open_kwargs, ) cleanup.pop_all() return self._file def __enter__(self) -> IO[Any]: return self._get_fileobj() def _close_rename_standard(self, src: Path) -> None: dest = Path(self._path) try: dest.unlink() except FileNotFoundError: pass finally: src.rename(dest) shutil.rmtree(src.parent) def _close_rename_zip(self, src: Path) -> None: from zipfile import ZipFile, ZipInfo if self._in_zip is None: msg = "in_zip path is unexpectedly None" raise RuntimeError(msg) # ZipInfo.from_file is what turns a member path into the name an # archive stores. It drops a drive, collapses "..", strips a # leading separator and uses posix separators, so asking it is # the only way to compare against namelist() in the same terms. # A path of "arch/sub/../target.txt" is stored as # "arch/target.txt", and comparing the two appends a duplicate arcname = ZipInfo.from_file(src, self._path).filename try: # append mode is used for the test as well as the write. It # tolerates a missing file and the 0-byte residue of an # interrupted write, where a read-only open of either raises # BadZipFile, and it saves reading the central directory a # second time. Closing without writing leaves the archive # byte for byte as it was with ZipFile(self._in_zip, "a") as out: replacing = arcname in out.namelist() if not replacing: out.write(str(src), arcname=arcname) if replacing: self._replace_zip_member(src, arcname, self._in_zip) finally: shutil.rmtree(src.parent) def _replace_zip_member(self, src: Path, arcname: str, in_zip: Path) -> None: """rewrites the archive with src in place of the member arcname Parameters ---------- src path to the file holding the new content arcname name of the member it replaces in_zip path to the archive being rewritten Notes ----- A zip has no way to take a member out, so appending under a name already in the archive leaves two entries under that name. The one a reader finds then depends on whether it works from the central directory or from the local headers, and open_ refuses the archive outright for holding more than one record. The archive is rewritten instead, and put in place with a rename so that a failure part way through leaves the original untouched. The rename gives the archive a new inode, so a hard link to it goes stale. The mode is carried across, which the umask default on the new file would otherwise widen, and a symlinked archive is written through to its target rather than replaced by a plain file. """ from zipfile import ZipFile rewritten = in_zip.parent / f"{uuid.uuid4()}.zip" # every other member is decompressed on the way across, so this # can fail on the content of a member that has nothing to do # with the one being replaced: BadZipFile for a bad CRC, # RuntimeError for an encrypted member, OSError for the disk. # That list is what has been seen rather than what can happen, # so the cleanup is registered rather than hung off it. It # covers the put-back as well, so that nothing is left beside # the original until the rename has taken. Once that succeeds # the path no longer exists and the callback would be a no-op, # but it is dropped rather than relied on to do nothing with contextlib.ExitStack() as cleanup: cleanup.callback(rewritten.unlink, missing_ok=True) with ( ZipFile(in_zip) as existing, ZipFile(rewritten, "w") as out, ): out.comment = existing.comment for info in existing.infolist(): if info.filename == arcname: continue # the member is copied through its ZipInfo so that # its name, timestamp and compression survive. the # reader is opened first because opening the writer # zeroes the sizes on the ZipInfo it is handed with existing.open(info) as member, out.open(info, "w") as dest: shutil.copyfileobj(member, dest) out.write(str(src), arcname=arcname) target = in_zip.resolve() if in_zip.is_symlink() else in_zip shutil.copymode(target, rewritten) rewritten.replace(target) cleanup.pop_all() def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: if self._file is None: msg = "file object is unexpectedly None" raise ValueError(msg) self._file.close() if exc_type is None: self._close_func(self._tmppath) self.succeeded = True else: self.succeeded = False shutil.rmtree(self._tmppath.parent) def write(self, text: str | bytes) -> None: """writes text to file""" fileobj = self._get_fileobj() fileobj.write(text) def close(self) -> None: """closes file""" self.__exit__(None, None, None) def get_format_suffixes(filename: PathType) -> tuple[str | None, str | None]: """returns file, compression suffixes""" filename = Path(filename) if not filename.suffix: return None, None suffixes = [_wout_period.sub("", sfx).lower() for sfx in filename.suffixes[-2:]] cmp_suffix = suffixes[-1] if suffixes[-1] in _compression_handlers else None if len(suffixes) == 2 and cmp_suffix is not None: suffix = suffixes[0] elif cmp_suffix is None: suffix = suffixes[-1] else: suffix = None return suffix, cmp_suffix def path_exists(path: PathType) -> bool: """whether path is a valid path and it exists""" with contextlib.suppress(Exception): return Path(path).exists() return False class _DecodedReader: """reads a byte stream in byte-sized chunks and decodes them Notes ----- read(n) on a text file object returns n characters, so under a multi-byte encoding it takes several times n bytes of the file. A reader working to a memory budget wants the budget spent on bytes, so the bytes are read here and decoded on the way past. Line endings are not translated, where a text file object would turn "\\r\\n" into "\\n". It makes no difference to the lines that come out, since str.splitlines() breaks on "\\r", "\\n" and "\\r\\n" alike and drops the terminator either way. """ def __init__(self, infile: IO[bytes], encoding: str | None) -> None: """ Parameters ---------- infile open file object in a binary mode encoding name of the encoding, None meaning the locale default, as it does for builtin open """ self._infile = infile # the test is against None rather than falsiness, so that an # empty encoding reaches the codec lookup and is rejected there, # as it is on the other paths through this module if encoding is None: encoding = locale.getpreferredencoding(do_setlocale=False) self._decoder = codecs.getincrementaldecoder(encoding)() def read(self, size: int = -1) -> str: """returns the text decoded from the next size bytes Parameters ---------- size number of bytes to read, a negative meaning read it all Notes ----- A chunk ending part way through a character decodes to the empty string, with the bytes held by the decoder until the rest of the character arrives. That is not the end of the file, so reading continues until there is either text to return or nothing left to read. Returning the empty string instead would be read as the end of the file and would silently truncate. A stateful encoding can hold far more than one character back. A run of iso-2022-jp escape sequences decodes to nothing at all, so a read of that run returns only once past it, having taken more than size bytes to do so. """ if size < 0: return self._decoder.decode(self._infile.read(), final=True) while True: raw = self._infile.read(size) text = self._decoder.decode(raw, final=not raw) if text or not raw: return text def _chunk_size_for(path: Path, chunk_size: int | None) -> int | None: """returns None where path is small enough to read in one go Parameters ---------- path file the chunk size is for chunk_size number of bytes to read in one go, None meaning read it all Notes ----- st_size of a compressed file is the compressed size, which says nothing about how much comes out of it, so the shortcut is only taken where the two are the same number. A file that compresses below the budget can decompress to any size at all. """ if chunk_size is None: return None _, compression = get_format_suffixes(path) if compression is None and path.stat().st_size < chunk_size: return None return chunk_size def _check_chunk_size(chunk_size: int | None) -> None: """raises if chunk_size is not a usable number of bytes Parameters ---------- chunk_size number of bytes to read in one go, None meaning read it all Raises ------ ValueError if chunk_size is not a positive whole number, None excepted Notes ----- Zero and a negative fail in opposite directions, and neither says so. read(0) returns an empty string, which a read loop cannot tell from the end of the file, so nothing at all is yielded for a file that plainly has contents. read(-1) reads to the end, so the whole file arrives in one chunk, which is what None already means and the opposite of the bounded memory the argument exists to ask for. A fraction is rejected too. read() raises TypeError for one, but the same value reaching num_lines has nothing downstream to catch it, so both are refused in the same terms. """ if chunk_size is not None and (chunk_size <= 0 or chunk_size % 1): msg = f"chunk_size must be a positive whole number of bytes, not {chunk_size!r}" raise ValueError(msg) def _splitlines( infile: IO[_StrOrBytes], chunk_size: int | None, sep: _Separators[_StrOrBytes], ) -> Iterator[_StrOrBytes]: """yields lines from an open file object Parameters ---------- infile open file object, in text or binary mode chunk_size number of bytes to load in one go, None means read it all sep line separators matching the mode of infile Notes ----- No line ending translation is assumed of the reader. A carriage return reaches here in either mode, so a "\\r\\n" split by a chunk boundary has to be stitched back together rather than counted as two line endings. A reader that does translate, such as a file object opened in a text mode, simply never presents the case. """ # fragments of a line that spans a chunk boundary, joined only # when the line is complete and about to be yielded pending: list[_StrOrBytes] = [] # an empty line, held back until a later line proves it was not the # last one, as a trailing empty line is not yielded held: _StrOrBytes | None = None # whether the previous chunk ended on a carriage return, which may # be the first half of a "\r\n" split by the chunk boundary split_return = False while True: data = infile.read() if chunk_size is None else infile.read(chunk_size) if not data: # end of file break lines = data.splitlines() if split_return and data.startswith(sep.newline): # this newline completes the line ending started by the # previous chunk, it does not begin a new line del lines[0] split_return = data.endswith(sep.carriage_return) # a chunk ending on a line boundary has no unfinished last line, # otherwise that line continues into the next chunk tail = None if data[-1:] in sep.boundaries else lines.pop(-1) if lines and pending: pending.append(lines[0]) lines[0] = sep.empty.join(pending) pending.clear() if lines: if held is not None: # these lines follow it, so it was not the last one yield held held = None if not lines[-1]: held = lines.pop(-1) yield from lines if tail is not None: pending.append(tail) if pending: if held is not None: yield held yield sep.empty.join(pending) @overload def iter_splitlines( path: PathType, chunk_size: int | None = ..., *, as_bytes: Literal[False] = ..., ) -> Iterator[str]: ... @overload def iter_splitlines( path: PathType, chunk_size: int | None = ..., *, as_bytes: Literal[True], ) -> Iterator[bytes]: ... @overload def iter_splitlines( path: PathType, chunk_size: int | None = ..., *, as_bytes: bool, ) -> Iterator[str | bytes]: ... def iter_splitlines( path: PathType, chunk_size: int | None = 1_000_000, *, as_bytes: bool = False, ) -> Iterator[Any]: """yields line from file Parameters ---------- path data file chunk_size number of bytes to load in one go from path as_bytes if True, lines are returned as bytes and path is opened in binary mode, otherwise lines are returned as str Raises ------ ValueError if chunk_size is not a positive whole number, None excepted. Raised on the first iteration, as any error inside a generator function is, and before path is looked at, so a bad argument is reported as one rather than as a missing file Notes ----- Loads chunks of data from the file, yields one line at a time. An empty last line is not yielded, so a file ending on a line terminator gives the same lines as one that does not. chunk_size counts bytes read in one go, in both modes and whether or not the file is compressed. Text is read as bytes and decoded on the way past, rather than read through a text file object, whose read(n) returns n characters and so takes several times n bytes of a file in a multi-byte encoding. For a compressed file the count is of the bytes coming out of the decompression, which is the memory the read costs, rather than of the bytes on disk. The two modes do not always split a file into the same number of lines. str.splitlines() breaks on "\\r", "\\n" and "\\r\\n" and on a further eight characters including vertical tab and form feed. bytes.splitlines() breaks only on "\\r", "\\n" and "\\r\\n". """ _check_chunk_size(chunk_size) url = is_url(path) if url: chunk_size = None else: path = Path(path).expanduser() chunk_size = _chunk_size_for(path, chunk_size) if as_bytes: with open_(path, mode="rb") as infile: # open_ is typed as returning IO[Any], the cast binds the # type variable so a mismatched separator is a type error binary = cast("IO[bytes]", infile) yield from _splitlines(binary, chunk_size, _BINARY_SEPARATORS) elif url: # a url is read whole, so there is no budget to keep to, and the # charset from the response headers that open_url uses is better # evidence than a sniff of the content with open_(path) as infile: yield from _splitlines( cast("IO[str]", infile), chunk_size, _TEXT_SEPARATORS ) else: encoding = _detect_encoding(path) with open_(path, mode="rb") as infile: reader = _DecodedReader(cast("IO[bytes]", infile), encoding) yield from _splitlines( cast("IO[str]", reader), chunk_size, _TEXT_SEPARATORS, ) @overload def iter_line_blocks( path: PathType, num_lines: int | None = ..., chunk_size: int | None = ..., *, as_bytes: Literal[False] = ..., ) -> Iterator[list[str]]: ... @overload def iter_line_blocks( path: PathType, num_lines: int | None = ..., chunk_size: int | None = ..., *, as_bytes: Literal[True], ) -> Iterator[list[bytes]]: ... @overload def iter_line_blocks( path: PathType, num_lines: int | None = ..., chunk_size: int | None = ..., *, as_bytes: bool, ) -> Iterator[list[str] | list[bytes]]: ... def iter_line_blocks( path: PathType, num_lines: int | None = 1000, chunk_size: int | None = 5_000_000, *, as_bytes: bool = False, ) -> Iterator[list[Any]]: """yields list of num_lines lines from path Parameters ---------- path data file num_lines number of lines per block. If None just returns all lines. chunk_size number of bytes to load in one go from path as_bytes if True, lines are returned as bytes and path is opened in binary mode, otherwise lines are returned as str Raises ------ ValueError if num_lines or chunk_size is not a positive whole number, None excepted. Raised on the first iteration, as any error inside a generator function is Notes ----- Lines are produced by iter_splitlines, see its notes for how the two modes differ. If num_lines is None the whole file accumulates in one block, so peak memory is the size of the file. """ if num_lines is not None and (num_lines <= 0 or num_lines % 1): # the block is complete when its length equals num_lines, which # never holds for a value that is not a positive whole number, # so the whole file came back as one block. That is what # num_lines=None asks for and the opposite of what a small # number asks for msg = f"num_lines must be a positive whole number of lines, not {num_lines!r}" raise ValueError(msg) lines = [] for line in iter_splitlines(path, chunk_size=chunk_size, as_bytes=as_bytes): lines.append(line) if len(lines) == num_lines: yield lines lines = [] if lines: yield lines def iter_record_chunks( *, path: PathType, delimiter: bytes, chunk_size: int | None = 5_000_000, ) -> Iterator[bytes]: """yield bytes between successive occurrences of ``delimiter`` Parameters ---------- path data file. Accepts a path, URL, or any ``PathType`` and opens it via ``open_(path, mode="rb")`` so compressed formats are handled transparently. If ``path`` is a URL the stream is read in full (``chunk_size`` is forced to ``None``). delimiter bytes delimiter on which records are split. Must be non-empty. chunk_size bytes read per iteration. If ``None``, or if the file is uncompressed and smaller than ``chunk_size``, it is read in a single call. A compressed file is read in chunks whatever its size on disk, since that size says nothing about how much comes out of the decompression. Yields ------ bytes each item is the content between two successive delimiters. The first item is whatever precedes the first delimiter (often empty for files that start with a delimiter). The final item is whatever follows the last delimiter; callers filter as needed for their format. Raises ------ ValueError if ``delimiter`` is empty, or if ``chunk_size`` is zero or less. Notes ----- Reads ``path`` in chunks of ``chunk_size`` bytes and splits on ``delimiter``, holding any trailing partial record across chunk boundaries so that delimiters spanning a boundary are detected correctly. Peak memory is bounded by ``chunk_size`` plus the size of the largest record, rather than the full file size. Operates on raw bytes only; callers that need text decoding should do so per yielded record. Examples -------- >>> import tempfile, pathlib >>> with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: ... _ = f.write(b">a\\nAAA>b\\nBBB>c\\nCCC") ... tmp = pathlib.Path(f.name) >>> list(iter_record_chunks(path=tmp, delimiter=b">", chunk_size=8)) [b'', b'a\\nAAA', b'b\\nBBB', b'c\\nCCC'] >>> tmp.unlink() """ if not delimiter: msg = "delimiter must be non-empty" raise ValueError(msg) _check_chunk_size(chunk_size) if is_url(path): chunk_size = None else: path = Path(path).expanduser() chunk_size = _chunk_size_for(path, chunk_size) # We accommodate a chunked read falling within a delimiter # by extracting the overlap_len of the last (potentially partial) # record and prepending it to the next chunk. # We only need to keep the last len(delimiter) - 1 bytes, as a delimiter # cannot span more than that. overlap_len = len(delimiter) - 1 with open_(path, mode="rb") as infile: pending: list[bytes] = [] # carry represents the portion of the last (potentially partial) record # that we need to prepend to the next chunk. carry = b"" while True: chunk = infile.read() if chunk_size is None else infile.read(chunk_size) if not chunk: break parts = (carry + chunk).split(delimiter) last = parts.pop() cut = max(len(last) - overlap_len, 0) carry = last[cut:] for part in parts: pending.append(part) yield b"".join(pending) pending.clear() if cut: pending.append(last[:cut]) if pending or carry: pending.append(carry) yield b"".join(pending) cogent3-scinexus-b16bc06/src/scinexus/misc.py000066400000000000000000000045561525761402500212760ustar00rootroot00000000000000"""Selected utility functions.""" from __future__ import annotations import inspect import re from typing import TYPE_CHECKING, ParamSpec, TypeVar if TYPE_CHECKING: # pragma: no cover from collections.abc import Callable P = ParamSpec("P") R = TypeVar("R") def get_object_provenance(obj: object) -> str: """returns string of complete object provenance""" # algorithm inspired by Greg Baacon's answer to # https://stackoverflow.com/questions/2020014/get-fully-qualified-class # -name-of-an-object-in-python if isinstance(obj, type) or inspect.isfunction(obj): mod = obj.__module__ name = obj.__name__ else: mod = obj.__class__.__module__ name = obj.__class__.__name__ return name if mod is None or mod == "builtins" else f"{mod}.{name}" def extend_docstring_from( source: object, pre: bool = False ) -> Callable[[Callable[P, R]], Callable[P, R]]: def docstring_inheriting_decorator(dest: Callable[P, R]) -> Callable[P, R]: parts = [source.__doc__ or "", dest.__doc__ or ""] # trim leading/trailing blank lines from parts for i, part in enumerate(parts): lines = part.split("\n") if not lines[0].strip(): lines.pop(0) if lines and not lines[-1].strip(): lines.pop(-1) parts[i] = "\n".join(lines) if pre: parts.reverse() dest.__doc__ = "\n".join(parts) return dest return docstring_inheriting_decorator _doc_block = re.compile( r"^\s*(Parameters|Notes|Raises)", flags=re.IGNORECASE | re.MULTILINE, ) def docstring_to_summary_rest(text: str) -> tuple[str, str]: """separates the summary at the start of a docstring from the rest Notes ----- Assumes numpydoc style. """ if not text: return "", "" pos = _doc_block.search(text) if pos is None: return text, "" summary = text[: pos.start()].rstrip() text = text[pos.start() :] return summary, text.lstrip("\n").rstrip(" ") def in_jupyter() -> bool: """whether code is being executed within a jupyter notebook""" val = True try: # primitive approach, just check whether the following function # is in the namespace get_ipython # type: ignore[name-defined] # noqa: B018 except NameError: val = False return val cogent3-scinexus-b16bc06/src/scinexus/parallel.py000066400000000000000000000665271525761402500221450ustar00rootroot00000000000000from __future__ import annotations import concurrent.futures as concurrentfutures import itertools import multiprocessing import numbers import os import sys import threading import warnings from abc import ABC, abstractmethod from collections.abc import Sized from types import MappingProxyType from typing import TYPE_CHECKING, Generic, Literal, ParamSpec, TypeVar, cast from scinexus.misc import extend_docstring_from if TYPE_CHECKING: # pragma: no cover from collections.abc import Callable, Generator, Iterable from typing import Any MPI: Any if os.environ.get("DONT_USE_MPI", 0): MPI = None else: try: from mpi4py import MPI # type: ignore[import-not-found,no-redef] from mpi4py import futures as MPIfutures # noqa: N812 except ImportError: MPI = None else: COMM = MPI.COMM_WORLD if COMM.Get_attr(MPI.UNIVERSE_SIZE) == 1: MPI = None USING_MPI = MPI is not None P = ParamSpec("P") R = TypeVar("R") T = TypeVar("T") BackendType = Literal["multiprocess", "threads", "loky", "mpi"] class Parallel(ABC): """abstract base class for parallel execution backends Subclass this to integrate a custom parallel engine (e.g. ray, dask). """ @abstractmethod def imap( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: """yield results of ``f`` applied to each element of ``s``, in order""" @abstractmethod def as_completed( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: """yield results of ``f`` applied to each element of ``s``, in completion order""" @abstractmethod def is_master_process(self) -> bool: """return True if the current process is the master""" @abstractmethod def get_rank(self) -> int: """return the rank of the current process""" @abstractmethod def get_size(self) -> int: """return the number of available workers""" class MultiprocessBackend(Parallel): """parallel backend using the stdlib ``concurrent.futures.ProcessPoolExecutor``""" def imap( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: max_workers = _resolve_max_workers_local(max_workers) chunksize = _resolve_chunksize(s, max_workers, kwargs.get("chunksize")) ctx = multiprocessing.get_context("spawn") with concurrentfutures.ProcessPoolExecutor( max_workers=max_workers, mp_context=ctx ) as executor: yield from executor.map(f, s, chunksize=chunksize) def as_completed( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: max_workers = _clamp_max_workers_local(max_workers) _check_chunksize(kwargs.get("chunksize")) ctx = multiprocessing.get_context("spawn") with concurrentfutures.ProcessPoolExecutor( max_workers=max_workers, mp_context=ctx ) as executor: to_do = [executor.submit(f, e) for e in s] for result in concurrentfutures.as_completed(to_do): yield result.result() def is_master_process(self) -> bool: return multiprocessing.parent_process() is None def get_rank(self) -> int: return _get_rank_local() def get_size(self) -> int: return multiprocessing.cpu_count() class ThreadBackend(Parallel): """parallel backend using the stdlib ``concurrent.futures.ThreadPoolExecutor`` On a free-threaded build the workers run without contending for a global lock, and neither the arguments nor the results are pickled, so closures and lambdas are accepted where the process backends refuse them. ``chunksize`` is checked and then ignored, because a thread pool takes one task per item and has no per-item transport cost to amortise. ``max_workers`` is validated as it is for the process backends, so ``imap`` refuses a value above the CPU count, ``as_completed`` clamps one, and both refuse a value below one. Notes ----- Workers share the app instance and everything reachable from it. A ``main`` that mutates ``self``, or that writes module-level state such as the ``numpy.random`` global generator, is a race here where it was harmless under the process backends. """ def imap( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: max_workers = _resolve_max_workers_local(max_workers) _check_chunksize(kwargs.get("chunksize")) with concurrentfutures.ThreadPoolExecutor( max_workers=max_workers, initializer=_assign_rank_thread ) as executor: yield from executor.map(f, s) def as_completed( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: max_workers = _clamp_max_workers_local(max_workers) _check_chunksize(kwargs.get("chunksize")) with concurrentfutures.ThreadPoolExecutor( max_workers=max_workers, initializer=_assign_rank_thread ) as executor: to_do = [executor.submit(f, e) for e in s] for result in concurrentfutures.as_completed(to_do): yield result.result() def is_master_process(self) -> bool: return multiprocessing.parent_process() is None and _get_rank_thread() == 0 def get_rank(self) -> int: return _get_rank_thread() def get_size(self) -> int: return multiprocessing.cpu_count() class LokyBackend(Parallel): """parallel backend using the loky library loky provides reusable process pools that are more robust than the stdlib ``ProcessPoolExecutor``, particularly in Jupyter notebooks. Requires ``pip install "scinexus[loky]"``. """ def imap( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: import loky # type: ignore[import-untyped,import-not-found] max_workers = _resolve_max_workers_local(max_workers) chunksize = _resolve_chunksize(s, max_workers, kwargs.get("chunksize")) with loky.get_reusable_executor(max_workers=max_workers) as executor: yield from executor.map(f, s, chunksize=chunksize) def as_completed( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: import loky # type: ignore[import-untyped,import-not-found] max_workers = _clamp_max_workers_local(max_workers) _check_chunksize(kwargs.get("chunksize")) with loky.get_reusable_executor(max_workers=max_workers) as executor: to_do = [executor.submit(f, e) for e in s] for result in concurrentfutures.as_completed(to_do): yield result.result() def is_master_process(self) -> bool: import loky # type: ignore[import-untyped,import-not-found] ctxt = loky.backend.get_context() return ctxt.parent_process() is None def get_rank(self) -> int: return _get_rank_local() def get_size(self) -> int: return multiprocessing.cpu_count() class MPIBackend(Parallel): """parallel backend using MPI via mpi4py Requires ``pip install "scinexus[mpi]"`` and an MPI implementation (e.g. OpenMPI). """ def __init__(self) -> None: if MPI is None: msg = "Cannot use MPI" raise RuntimeError(msg) self._mpi = MPI self._comm = COMM self._futures = MPIfutures self._workers: int = _worker_budget(self._comm.Get_size()) def imap( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: if_serial = cast( "Literal['raise', 'ignore', 'warn']", kwargs.get("if_serial", "raise"), ) self._check_serial(if_serial) max_workers = _resolve_max_workers_mpi(max_workers, self._workers) chunksize = _resolve_chunksize(s, max_workers, kwargs.get("chunksize")) with self._futures.MPIPoolExecutor(max_workers=max_workers) as executor: yield from executor.map(f, s, chunksize=chunksize) def as_completed( self, f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, **kwargs: Any, ) -> Generator[R]: if_serial = cast( "Literal['raise', 'ignore', 'warn']", kwargs.get("if_serial", "raise"), ) self._check_serial(if_serial) pickled_f: Callable[[T], R] = PicklableAndCallable(f) max_workers = _resolve_max_workers_mpi(max_workers, self._workers) _check_chunksize(kwargs.get("chunksize")) with self._futures.MPIPoolExecutor(max_workers=max_workers) as executor: to_do = [executor.submit(pickled_f, e) for e in s] for result in concurrentfutures.as_completed(to_do): yield result.result() def is_master_process(self) -> bool: process_cmd = sys.argv[0] process_file = process_cmd.split(os.sep)[-1] return False if process_file == "server.py" else self._comm.Get_rank() == 0 def get_rank(self) -> int: return self._comm.Get_rank() def get_size(self) -> int: return self._workers def _check_serial(self, if_serial: Literal["raise", "ignore", "warn"]) -> None: if self._workers == 1: err_msg = ( "Execution in serial: this job has one worker. Rank 0 is the" " master and hands out the work rather than doing it, so" " -n 3 is the smallest launch with a second worker:\n" " $ mpiexec -n python -m mpi4py.futures" " " ) if if_serial == "raise": raise RuntimeError(err_msg) if if_serial == "warn": warnings.warn(err_msg, UserWarning, stacklevel=4) class PicklableAndCallable(Generic[P, R]): """wraps a callable so it is picklable for use with MPI executors""" def __init__(self, func: Callable[P, R]) -> None: self.func = func def __call__(self, *args: P.args, **kw: P.kwargs) -> R: return self.func(*args, **kw) BACKEND_TYPES: MappingProxyType[BackendType, type[Parallel]] = MappingProxyType( { "multiprocess": MultiprocessBackend, "threads": ThreadBackend, "loky": LokyBackend, "mpi": MPIBackend, } ) _THREAD_BACKEND = ThreadBackend() _thread_state = threading.local() _rank_lock = threading.Lock() _rank_counter = itertools.count(1) def _get_rank_thread() -> int: """return the rank of the current thread, 0 for any thread not in a pool Rank 0 meaning "not one of our workers" is what lets ``ThreadBackend.is_master_process`` keep reporting master for a thread the caller created, such as a web request handler or a GUI worker. A thread that a worker itself starts is in that group too, so it reports master where the equivalent thread inside a worker process does not. """ return int(getattr(_thread_state, "rank", 0)) def _assign_rank_thread() -> None: """give the calling thread a rank no other live worker holds Used as a ``ThreadPoolExecutor`` initializer. The count runs across pools rather than restarting within each, so nested and concurrent pools cannot hand one rank to two threads that are alive at once, and successive pools number their workers the way successive process pools do: ``multiprocessing`` names them ``SpawnProcess-2``, ``-4``, ``-8`` and never returns to 1. The lock keeps the increment atomic without relying on ``itertools.count``, whose atomicity is an implementation detail of CPython rather than a guarantee of the language. """ with _rank_lock: _thread_state.rank = next(_rank_counter) def _check_max_workers(max_workers: int | None) -> None: """raise unless max_workers is None or an int of at least 1""" if max_workers is None: return _check_positive_int(max_workers, "max_workers", "an int or None") def _resolve_max_workers_local(max_workers: int | None) -> int: """resolve max_workers for local (non-MPI) backends""" _check_max_workers(max_workers) cpu = multiprocessing.cpu_count() if max_workers is None: return cpu if max_workers > cpu: msg = f"max_workers ({max_workers}) must be less than or equal to CPU count ({cpu})" raise ValueError(msg) return int(max_workers) def _clamp_max_workers_local(max_workers: int | None) -> int: """clamp max_workers for local as_completed, raising only below one""" _check_max_workers(max_workers) if max_workers is None or max_workers > multiprocessing.cpu_count(): return multiprocessing.cpu_count() return int(max_workers) def _worker_budget(world_size: int) -> int: """return the number of workers an MPI job has Rank 0 is the master and hands out the work rather than doing any, so the pool is the rest of the ranks, floored at one. """ return max(world_size - 1, 1) def _resolve_max_workers_mpi(max_workers: int | None, budget: int) -> int: """return the budget, reporting a max_workers that disagrees with it The count comes from the ranks that were launched, so a request for more workers or for fewer cannot be obeyed. """ _check_max_workers(max_workers) if max_workers is not None and max_workers != budget: warnings.warn( f"max_workers ({max_workers}) is not the {budget} workers this" " job has. scinexus takes the count from the ranks launched by" " mpiexec -n, so this request is not used.", UserWarning, stacklevel=3, ) return budget def _get_rank_local() -> int: """return the rank of the current process for local backends""" process_name = multiprocessing.current_process().name return int(process_name.split("-")[-1]) if process_name != "MainProcess" else 0 def _check_integral(value: object, name: str, expected: str = "an int") -> None: """raise unless value is of an integer type and is not a bool ``expected`` names what this caller accepts, since only some take None. """ # bool is Integral and numpy.bool_ is neither a bool nor Integral, so # one test alone lets one of them through if isinstance(value, bool) or not isinstance(value, numbers.Integral): msg = f"{name} must be {expected}, got {value!r}" raise TypeError(msg) def _check_positive_int(value: object, name: str, expected: str = "an int") -> None: """raise unless value is of an integer type and is at least 1""" _check_integral(value, name, expected) if cast("numbers.Integral", value) < 1: msg = f"{name} ({value}) must be greater than 0" raise ValueError(msg) def _check_chunksize(chunksize: int | None) -> None: """raise unless chunksize is None or an int of at least 1""" if chunksize is None: return _check_positive_int(chunksize, "chunksize", "an int or None") def _resolve_chunksize( s: Iterable[Any], max_workers: int, chunksize: int | None ) -> int: """resolve chunksize, defaulting via get_default_chunksize for Sized inputs""" _check_chunksize(chunksize) if chunksize is None: return get_default_chunksize(s, max_workers) if isinstance(s, Sized) else 1 return int(chunksize) def _validate_if_serial( if_serial: str, ) -> Literal["raise", "ignore", "warn"]: """validate and normalise the if_serial parameter""" if_serial = if_serial.lower() if if_serial not in ("ignore", "raise", "warn"): msg = f"invalid choice '{if_serial}'" raise ValueError(msg) return cast("Literal['raise', 'ignore', 'warn']", if_serial) def get_default_chunksize(s: Sized, max_workers: int) -> int: """compute a stable chunksize for distributing items across workers Parameters ---------- s a sized collection of work items max_workers number of worker processes, an int of at least 1 Raises ------ TypeError if max_workers is not of an integer type ValueError if max_workers is below 1 """ _check_positive_int(max_workers, "max_workers") chunksize, remainder = divmod(len(s), max_workers * 4) if remainder: chunksize += 1 # an empty input divides to 0 with no remainder, and the executors that # receive this refuse a chunk size of 0 return int(max(chunksize, 1)) _default_backend: Parallel | None = None _default_backend_lock = threading.Lock() _auto_selected = False _mpi_backend: MPIBackend | None = None def _replace_locks_after_fork() -> None: """give a forked child its own module locks A fork copies each lock in whatever state the parent held it, so a child that inherits a held one would wait for an owner that does not exist in it. Registered below on platforms that can fork. """ global _default_backend_lock, _rank_lock # noqa: PLW0603 _default_backend_lock = threading.Lock() _rank_lock = threading.Lock() if hasattr(os, "register_at_fork"): os.register_at_fork(after_in_child=_replace_locks_after_fork) def _gil_enabled() -> bool: """report whether the GIL is currently in force ``sys._is_gil_enabled`` arrived in 3.13, so its absence means a build that always has the GIL. On a free-threaded build it can still report ``True``, because ``PYTHON_GIL=1`` forces the GIL on and because importing an extension module that does not declare free-threading support re-enables it. """ probe = getattr(sys, "_is_gil_enabled", None) return True if probe is None else bool(probe()) def _in_worker_process() -> bool: """report whether this process was started by one of the pool backends One question covers every backend that starts processes. loky builds its workers through a ``multiprocessing`` context, so its contexts expose this very function and its workers answer it the same way a spawned worker does. """ return multiprocessing.parent_process() is not None def _auto_backend_type() -> BackendType: """the backend to use when the caller has not chosen one Threads are worth using only where they can run at the same time, and only in the master process. A spawned or loky worker re-imports this module with no default set, and a thread backend there would describe that worker as rank 0 and as the master, which it is not. """ if _gil_enabled() or _in_worker_process(): return "multiprocess" return "threads" def _make_backend(backend: BackendType) -> Parallel: """create a backend instance from a backend type string""" if backend == "loky": try: import loky # type: ignore[import-untyped,import-not-found] # noqa: F401 except ImportError: msg = 'loky is not installed, use pip install "scinexus[loky]"' raise ImportError(msg) from None elif backend == "mpi" and MPI is None: msg = 'mpi4py is not installed, use pip install "scinexus[mpi]"' raise ImportError(msg) return BACKEND_TYPES[backend]() def set_parallel_backend( backend: BackendType | Parallel | None = None, ) -> None: """set the default parallel execution backend Parameters ---------- backend a ``Parallel`` instance, a string literal (``"multiprocess"``, ``"threads"``, ``"loky"``, or ``"mpi"``), or ``None`` to choose again according to the interpreter. Notes ----- A choice made here is honoured until it is changed, which is how to opt out of the thread backend on a free-threaded build. It applies to this process alone, since a worker process does not inherit it. """ global _default_backend, _auto_selected # noqa: PLW0603 if backend is None or isinstance(backend, Parallel): chosen = backend elif backend in BACKEND_TYPES: chosen = _make_backend(backend) else: msg = ( f"unknown backend {backend!r}, expected 'multiprocess'," " 'threads', 'loky', 'mpi', or a Parallel instance" ) raise ValueError(msg) # the backend is built before the lock is taken, so that a refused name # or a missing package leaves the current choice alone, and so that no # import runs while the lock is held. the pair is then written together, # or a caller reading between the two stores would see this choice as # one to replace on the next look at the GIL with _default_backend_lock: _default_backend = chosen _auto_selected = False def get_parallel_backend(backend: BackendType | None = None) -> Parallel: """return the current parallel execution backend Parameters ---------- backend if provided, return an instance of this backend type without changing the global default. This lets a package obtain the backend it needs without disrupting the settings of other packages. Returns ------- When no backend has been set and ``backend is None``, a ``ThreadBackend`` if the GIL is not in force and this is not itself a pool worker, and a ``MultiprocessBackend`` in every other case. Notes ----- Left to choose for itself, this asks about the GIL on every call rather than once. Importing an extension module that does not declare free-threading support re-enables the GIL, and that can happen after the first parallel call, which would otherwise leave threads running one at a time for the rest of the process. A backend passed to ``set_parallel_backend`` is never replaced. """ if backend is not None: return _make_backend(backend) global _default_backend, _auto_selected # noqa: PLW0603 wanted = _auto_backend_type() with _default_backend_lock: stale = _auto_selected and not isinstance( _default_backend, BACKEND_TYPES[wanted] ) if _default_backend is None or stale: _default_backend = _make_backend(wanted) _auto_selected = True return _default_backend def _effective_backend() -> Parallel: """return the backend for the current process context If MPI is active, always returns an ``MPIBackend`` regardless of the default -- MPI worker processes don't inherit the parent's backend setting, and introspection functions like ``get_rank()`` must use the MPI communicator to report correctly. A thread one of this module's pools started is described by ``ThreadBackend`` for the same reason, whatever the default is. The default records what the caller asked for, not what is running the current thread, so consulting it would have a worker report itself as the master and as rank 0 whenever the pool came from anywhere other than ``set_parallel_backend``. """ global _mpi_backend # noqa: PLW0603 if USING_MPI: if _mpi_backend is None: _mpi_backend = MPIBackend() return _mpi_backend if _get_rank_thread(): return _THREAD_BACKEND return get_parallel_backend() def get_rank() -> int: """Returns the rank of the current process""" return _effective_backend().get_rank() def get_size() -> int: """Returns the num cpus""" return _effective_backend().get_size() SIZE = ( _worker_budget(COMM.Get_size()) # type: ignore[possibly-undefined] if USING_MPI else multiprocessing.cpu_count() ) def is_master_process() -> bool: """ Evaluates if current process is master In case of MPI checks whether current process is being run on file generated by mpi4py.futures In case of Multiprocessing checks if generated process name included "ForkProcess" for Windows or "SpawnProcess" for POSIX In case of threads, a thread the pool created is not the master, and any other thread is. Callers gate the creation of shared resources on this, so a caller's own thread has to keep reporting master. """ return _effective_backend().is_master_process() def imap( f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, use_mpi: bool = False, if_serial: Literal["raise", "ignore", "warn"] = "raise", chunksize: int | None = None, ) -> Generator[R]: """ Parameters ---------- f function that operates on values in s s series of inputs to f max_workers maximum number of workers, an int of at least 1. Defaults to None, meaning every available CPU. A bool, and anything else that is not of an integer type, is refused. use_mpi use MPI for parallel execution. Temporarily switches to ``MPIBackend`` for the duration of the call. if_serial action to take if conditions will result in serial execution. Valid values are 'raise', 'ignore', 'warn'. Defaults to 'raise'. chunksize Size of data chunks executed by worker processes, an int of at least 1. Defaults to None, where a stable chunksize is determined by get_default_chunksize(). Checked wherever it is accepted, but only imap on the process and MPI backends chunks the work by it. Returns ------- imap and as_completed are generators yielding result of f(s[i]), map returns the result series. imap and map return results in the same order as s, as_completed returns results in the order completed (which can differ from the order in s). Notes ----- To use MPI, you must have openmpi (use conda or your preferred package manager) and mpi4py (use pip or conda) installed. In addition, your initial script must have a ``if __name__ == '__main__':`` block. You then invoke your program using `$ mpiexec -n python3 -m mpi4py.futures ` """ if_serial = _validate_if_serial(if_serial) if use_mpi: backend = MPIBackend() yield from backend.imap( f, s, max_workers=max_workers, if_serial=if_serial, chunksize=chunksize ) else: yield from get_parallel_backend().imap( f, s, max_workers=max_workers, chunksize=chunksize ) @extend_docstring_from(imap) def map( f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, use_mpi: bool = False, if_serial: Literal["raise", "ignore", "warn"] = "raise", chunksize: int | None = None, ) -> list[R]: return list(imap(f, s, max_workers, use_mpi, if_serial, chunksize)) @extend_docstring_from(imap, pre=True) def as_completed( f: Callable[[T], R], s: Iterable[T], max_workers: int | None = None, use_mpi: bool = False, if_serial: Literal["raise", "ignore", "warn"] = "raise", chunksize: int | None = None, ) -> Generator[R]: if_serial = _validate_if_serial(if_serial) if use_mpi: backend = MPIBackend() yield from backend.as_completed( f, s, max_workers=max_workers, if_serial=if_serial, chunksize=chunksize ) else: yield from get_parallel_backend().as_completed( f, s, max_workers=max_workers, chunksize=chunksize ) cogent3-scinexus-b16bc06/src/scinexus/progress.py000066400000000000000000000431051525761402500222000ustar00rootroot00000000000000"""Generic progress bar framework with pluggable backends.""" from __future__ import annotations import contextlib from abc import ABC, abstractmethod from collections.abc import Sized from typing import TYPE_CHECKING, Literal, TypeVar if TYPE_CHECKING: # pragma: no cover from collections.abc import Iterable, Iterator from types import TracebackType from typing import Any, Self T = TypeVar("T") ProgressType = Literal["tqdm", "rich"] class ProgressContext(ABC): """A handle for push-based progress updates.""" def __init__(self, start: float = 0.0, end: float = 1.0) -> None: """ Parameters ---------- start start of the progress range (0.0-1.0) end end of the progress range (0.0-1.0) """ self._start = start self._end = end def _map(self, progress: float) -> float: return self._start + progress * (self._end - self._start) @abstractmethod def update(self, *, progress: float, msg: str = "") -> None: """Update the progress bar. Parameters ---------- progress fraction complete [0.0, 1.0], mapped to [start, end] range msg description text to display """ def close(self) -> None: # noqa: B027 """Close the progress bar. Override in subclasses with cleanup.""" def __enter__(self) -> Self: """Enter the context manager, returning self.""" return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: """Exit the context manager, calling close().""" self.close() class Progress(ABC): """A callable that wraps an iterable to display progress.""" @abstractmethod def __call__( self, iterable: Iterable[T], *, total: int | None = None, msg: str = "", ) -> Iterator[T]: """Wrap an iterable to display a progress bar. Parameters ---------- iterable items to iterate over total total number of items. Inferred from ``len(iterable)`` when possible. msg description text to display """ @abstractmethod def child(self, *, leave: bool | None = None) -> Progress: """Return a child Progress for nested bars. Parameters ---------- leave whether the child bar persists after completion. ``None`` inherits the parent setting. """ @abstractmethod def context( self, *, msg: str = "", start: float = 0.0, end: float = 1.0, ) -> ProgressContext: """Create a push-based progress context. Parameters ---------- msg initial description start start of the progress range (0.0-1.0) end end of the progress range (0.0-1.0) """ def close(self) -> None: # noqa: B027 """Finalize progress display. Override in subclasses with cleanup.""" def __enter__(self) -> Self: """Enter the context manager, returning self.""" return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: """Exit the context manager, calling close().""" self.close() class _NoOpContext(ProgressContext): """A no-op progress context that silently ignores updates.""" def update(self, *, progress: float, msg: str = "") -> None: pass _NO_OP_CONTEXT = _NoOpContext() # pylint: disable=unused-argument # ruff: noqa: ARG002 class NoProgress(Progress): """A no-op progress wrapper that passes through the iterable unchanged.""" def __call__( self, iterable: Iterable[T], *, total: int | None = None, msg: str = "", ) -> Iterator[T]: """Yield items from the iterable unchanged.""" yield from iterable def child(self, *, leave: bool | None = None) -> NoProgress: """Return self since no-op progress needs no nesting.""" return self def context( self, *, msg: str = "", start: float = 0.0, end: float = 1.0, ) -> ProgressContext: """Return a shared no-op context that silently ignores updates.""" return _NO_OP_CONTEXT class _TqdmContext(ProgressContext): """Push-based progress context backed by a tqdm bar.""" def __init__(self, bar: Any, start: float, end: float) -> None: super().__init__(start, end) self._bar = bar def update(self, *, progress: float, msg: str = "") -> None: self._bar.n = self._map(progress) if msg: self._bar.set_description(msg) self._bar.refresh() def close(self) -> None: self._bar.close() class TqdmProgress(Progress): """Progress wrapper using tqdm.auto (handles TTY, Jupyter, etc.).""" def __init__( self, refresh_per_second: float = 10.0, bar_format: str | None = "{desc}: {bar} {n_fmt}/{total_fmt} [{elapsed}<{remaining}]", leave: bool | None = None, colour: str | None = None, bar_width: int | None = None, **tqdm_kwargs: Any, ) -> None: """ Parameters ---------- refresh_per_second how often to refresh the display bar_format custom bar format string passed to tqdm leave whether the bar persists after completion. ``None`` uses position-based logic (persist at position 0, clear otherwise). colour bar colour, forwarded to tqdm bar_width total width of the progress bar in characters **tqdm_kwargs additional keyword arguments (e.g. ``dynamic_ncols=True``) forwarded to tqdm """ if refresh_per_second <= 0: msg = f"refresh_per_second must be positive, got {refresh_per_second!r}" raise ValueError(msg) self._position = tqdm_kwargs.pop("position", 0) self._refresh_per_second = refresh_per_second self._bar_format = bar_format self._leave = leave self._colour = colour self._bar_width = bar_width self._tqdm_kwargs = tqdm_kwargs self._bar: Any = None self._children: list[TqdmProgress] = [] def _resolve_leave(self) -> bool: return self._leave if self._leave is not None else self._position == 0 def _make_bar(self, *, total: float | None, msg: str) -> Any: from tqdm.auto import tqdm # type: ignore[import-untyped] if self._bar_width is not None: ncols_kwargs: dict[str, Any] = { "ncols": self._bar_width, "dynamic_ncols": False, } tqdm_kw = { k: v for k, v in self._tqdm_kwargs.items() if k != "dynamic_ncols" } else: tqdm_kw = self._tqdm_kwargs ncols_kwargs = {} if "dynamic_ncols" in tqdm_kw else {"dynamic_ncols": True} return tqdm( total=total, desc=msg, position=self._position, leave=self._resolve_leave(), mininterval=1.0 / self._refresh_per_second, bar_format=self._bar_format, colour=self._colour, **ncols_kwargs, **tqdm_kw, ) def __call__( self, iterable: Iterable[T], *, total: int | None = None, msg: str = "", ) -> Iterator[T]: total = ( len(iterable) if isinstance(iterable, Sized) and total is None else total ) if self._bar is None: self._bar = self._make_bar(total=total, msg=msg) else: self._bar.n = 0 self._bar.last_print_n = 0 self._bar.total = total if msg: self._bar.set_description(msg) for item in iterable: yield item self._bar.update(1) self._bar.refresh() def close(self) -> None: """Close all bars so the cursor moves past them.""" for child in reversed(self._children): if child._bar is not None: child._bar.close() child._bar = None if self._bar is not None: self._bar.close() self._bar = None def __del__(self) -> None: with contextlib.suppress(Exception): self.close() def child(self, *, leave: bool | None = None) -> TqdmProgress: """Return a child TqdmProgress at the next cursor position. Parameters ---------- leave whether the child bar persists after completion. ``None`` inherits the parent setting. """ child = TqdmProgress( refresh_per_second=self._refresh_per_second, bar_format=self._bar_format, leave=leave if leave is not None else self._leave, colour=self._colour, bar_width=self._bar_width, **self._tqdm_kwargs, ) child._position = self._position + 1 self._children.append(child) return child def context( self, *, msg: str = "", start: float = 0.0, end: float = 1.0, ) -> ProgressContext: """Create a tqdm-backed push-based progress context.""" bar = self._make_bar(total=1.0, msg=msg) return _TqdmContext(bar, start, end) class _RichContext(ProgressContext): """Push-based progress context backed by a rich progress bar.""" def __init__( self, progress: Any, task: Any, start: float, end: float, *, leave: bool, ) -> None: super().__init__(start, end) self._progress = progress self._task = task self._leave = leave def update(self, *, progress: float, msg: str = "") -> None: self._progress.update( self._task, completed=self._map(progress), description=msg or None ) def close(self) -> None: if self._leave: self._progress.update(self._task, completed=1.0) else: self._progress.remove_task(self._task) class RichProgress(Progress): """Progress wrapper using the rich library (requires ``pip install "scinexus[rich]"``).""" def __init__( self, progress: Any = None, refresh_per_second: float = 10.0, leave: bool = False, colour: str | None = None, bar_width: int | None = None, **rich_kwargs: Any, ) -> None: """ Parameters ---------- progress an existing ``rich.progress.Progress`` instance, or None to create one on first call refresh_per_second how often to refresh the display leave whether completed bars persist in the display colour bar colour, applied when auto-creating the display bar_width width of the progress bar portion in characters **rich_kwargs additional keyword arguments (e.g. ``disable=True``) forwarded to ``rich.progress.Progress`` """ if refresh_per_second <= 0: msg = f"refresh_per_second must be positive, got {refresh_per_second!r}" raise ValueError(msg) self._progress = progress self._owns_progress = progress is None self._refresh_per_second = refresh_per_second self._leave = leave self._colour = colour self._bar_width = bar_width self._rich_kwargs = rich_kwargs self._task: Any = None self._last_total: int | None = None self._children: list[RichProgress] = [] def _ensure_progress(self) -> Any: if self._progress is None: from rich.progress import ( # type: ignore[import-not-found] BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, ) from rich.progress import ( Progress as RProgress, ) bar_kwargs: dict[str, Any] = {} if self._bar_width is not None: bar_kwargs["bar_width"] = self._bar_width if self._colour is not None: bar_kwargs["complete_style"] = self._colour bar_kwargs["finished_style"] = self._colour bar_column = BarColumn(**bar_kwargs) self._progress = RProgress( TextColumn("[progress.description]{task.description}"), bar_column, TimeElapsedColumn(), TimeRemainingColumn(), refresh_per_second=self._refresh_per_second, **self._rich_kwargs, ) self._progress.start() return self._progress def __call__( self, iterable: Iterable[T], *, total: int | None = None, msg: str = "", ) -> Iterator[T]: total = ( len(iterable) if isinstance(iterable, Sized) and total is None else total ) rp = self._ensure_progress() if self._task is None: self._task = rp.add_task(msg, total=total) else: rp.reset(self._task, total=total, description=msg) self._last_total = total for item in iterable: yield item rp.advance(self._task) rp.refresh() if not self._leave: rp.remove_task(self._task) self._task = None def _cleanup_task(self) -> None: if self._task is not None and self._progress is not None: if self._leave and self._last_total is not None: self._progress.update(self._task, completed=self._last_total) else: self._progress.remove_task(self._task) self._task = None def close(self) -> None: """Finalize the rich progress display so the cursor moves past it.""" for child in reversed(self._children): child._cleanup_task() self._cleanup_task() if self._owns_progress and self._progress is not None: self._progress.stop() self._progress = None def __del__(self) -> None: with contextlib.suppress(Exception): self.close() def child(self, *, leave: bool | None = None) -> RichProgress: """Return a child RichProgress sharing the same display. Parameters ---------- leave whether the child bar persists after completion. ``None`` inherits the parent setting. """ child = RichProgress( progress=self._ensure_progress(), refresh_per_second=self._refresh_per_second, leave=leave if leave is not None else self._leave, colour=self._colour, bar_width=self._bar_width, **self._rich_kwargs, ) self._children.append(child) return child def context( self, *, msg: str = "", start: float = 0.0, end: float = 1.0, ) -> ProgressContext: """Create a rich-backed push-based progress context.""" rp = self._ensure_progress() task = rp.add_task(msg, total=1.0) return _RichContext(rp, task, start, end, leave=self._leave) _default_progress: Progress | None = None def set_progress_backend( progress: ProgressType | Progress | None = None, **kwargs: Any ) -> None: """Set the default Progress used when ``show_progress=True``. Parameters ---------- progress A ``Progress`` instance, a string literal (``"tqdm"`` or ``"rich"``), or ``None`` to reset to the default (``TqdmProgress``). **kwargs additional keyword arguments forwarded to the backend constructor when ``progress`` is ``"tqdm"`` or ``"rich"`` """ global _default_progress # noqa: PLW0603 if progress is None or isinstance(progress, Progress): _default_progress = progress elif progress == "tqdm": _default_progress = TqdmProgress(**kwargs) elif progress == "rich": try: import rich # type: ignore[import-not-found] # noqa: F401 except ImportError: msg = 'rich is not installed, use pip install "scinexus[rich]"' raise ImportError(msg) from None _default_progress = RichProgress(**kwargs) else: msg = f"unknown progress type {progress!r}, expected 'tqdm', 'rich', or a Progress instance" raise ValueError(msg) def get_progress(show_progress: bool | Progress = False, **kwargs: Any) -> Progress: """Resolve a ``show_progress`` argument into a ``Progress`` instance. Parameters ---------- show_progress If a ``Progress`` instance, returned as-is. If ``True``, returns the module default (set via ``set_progress_backend``, or ``TqdmProgress``). If falsy, returns ``NoProgress``. **kwargs additional keyword arguments forwarded to the backend constructor. When a default has been set via ``set_progress_backend``, a new instance of the same type is created with these kwargs. Ignored when ``show_progress`` is a ``Progress`` instance. """ if isinstance(show_progress, Progress): return show_progress if not show_progress: return NoProgress() if _default_progress is not None: return type(_default_progress)(**kwargs) if kwargs else _default_progress return TqdmProgress(**kwargs) cogent3-scinexus-b16bc06/src/scinexus/py.typed000066400000000000000000000000001525761402500214440ustar00rootroot00000000000000cogent3-scinexus-b16bc06/src/scinexus/sqlite_data_store.py000066400000000000000000001022071525761402500240410ustar00rootroot00000000000000from __future__ import annotations import datetime import os import re import sqlite3 import threading import warnings from pathlib import Path from typing import TYPE_CHECKING, Any from scitrack import get_text_hexdigest # type: ignore[import-untyped] from scinexus.data_store import ( APPEND, LOG_TABLE, OVERWRITE, READONLY, DataMember, DataMemberABC, DataStoreABC, DataStoreDirectory, Mode, _check_identifier, _member_id, ) from scinexus.misc import extend_docstring_from from scinexus.parallel import is_master_process if TYPE_CHECKING: # pragma: no cover from citeable import CitationBase RESULT_TABLE = "results" _MEMORY = ":memory:" _mem_pattern = re.compile(r"^\s*[:]{0,1}memory[:]{0,1}\s*$") NoneType = type(None) # dealing with python3.12 deprecation of datetime objects and their sqlite3 handling def _datetime_to_iso(timestamp: datetime.datetime) -> str: """timestamp in ISO 8601 format""" return timestamp.isoformat() sqlite3.register_adapter(datetime.datetime, _datetime_to_iso) def _datetime_from_iso(data: bytes) -> datetime.datetime: """timestamp from ISO 8601 format""" return datetime.datetime.fromisoformat(data.decode()) sqlite3.register_converter("timestamp", _datetime_from_iso) # create db def open_sqlite_db_rw(path: str | Path) -> sqlite3.Connection: """creates a new sqlitedb for read/write at path, can be an in-memory db The connection may be used from any thread, so a caller that shares one between threads must serialise its use. Notes ----- This function embeds the schema. There are three tables: - results: analysis objects, may be completed or not completed - logs: log-file contents - state: whether db is locked to a process Returns ------- Handle to a sqlite3 session """ db = _connect_sqlite_db_rw(path) _create_schema(db) return db def _connect_sqlite_db_rw(path: str | Path) -> sqlite3.Connection: """connect for read/write without creating the schema""" db = sqlite3.connect( path, isolation_level=None, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, check_same_thread=False, ) db.row_factory = sqlite3.Row return db def _create_schema(db: sqlite3.Connection) -> None: """add the store's tables to db, leaving any that are already there""" create_template = "CREATE TABLE IF NOT EXISTS {};" # note it is essential to use INTEGER for the autoincrement of primary key to work creates = [ "state(state_id INTEGER PRIMARY KEY, record_type TEXT, lock_pid INTEGER)", f"{LOG_TABLE}(log_id INTEGER PRIMARY KEY, log_name TEXT, date timestamp, data BLOB)", f"{RESULT_TABLE}(record_id TEXT PRIMARY KEY, log_id INTEGER, md5 BLOB, is_completed INTEGER, data BLOB)", "citations(citation_id INTEGER PRIMARY KEY, data TEXT)", ] for table in creates: db.execute(create_template.format(table)) def open_sqlite_db_ro(path: str | Path) -> sqlite3.Connection: """returns db opened as read only The connection may be used from any thread, so a caller that shares one between threads must serialise its use. Returns ------- Handle to a sqlite3 session """ db = sqlite3.connect( f"file:{path}?mode=ro", isolation_level=None, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, uri=True, check_same_thread=False, ) db.row_factory = sqlite3.Row if not has_valid_schema(db): msg = "database does not have a valid schema" raise ValueError(msg) return db def _owner_token() -> str: """identifies the process and thread taking a lock Notes ----- Written into ``state.lock_pid``, whose INTEGER is an affinity rather than a constraint, so the column takes this without a schema change. A value that is still a bare integer was written by a version that recorded only the process. A thread id names a thread only while that thread is alive. Linux recycles ids, so a token can come to name an unrelated live thread, which would then be allowed to release a lock it never took. Holding the owning ``threading.Thread`` would close that, at the cost of keeping the object alive. """ return f"{os.getpid()}:{threading.get_native_id()}" def has_valid_schema(db: sqlite3.Connection) -> bool: # TODO: should be a full schema check query = "SELECT name FROM sqlite_master WHERE type='table'" result = db.execute(query).fetchall() table_names = {r["name"] for r in result} _required = {RESULT_TABLE, LOG_TABLE, "state"} _optional = {"citations"} return _required <= table_names <= (_required | _optional) class DataStoreSqlite(DataStoreABC): """data store backed by a SQLite database A store is created by the master process, opened and written by one session, and read from any thread. It runs its statements one at a time on a single connection, so concurrent readers get their own rows back. A caller that takes the connection from :attr:`db` and uses it itself is outside that. """ store_suffix = "sqlitedb" def __init__( self, source: str | Path, mode: Mode | str = READONLY, limit: int | None = None, verbose: bool = False, ) -> None: if _mem_pattern.search(str(source)): self._source: str | Path = _MEMORY else: source = Path(source).expanduser() self._source = ( source if source.suffix[1:] == self.store_suffix # sliced to remove "." else Path(f"{source}.{self.store_suffix}") ) self._mode = Mode(mode) if mode is not READONLY and limit is not None: msg = "Using limit argument is only valid for readonly datastores" raise ValueError( msg, ) self._limit = limit self._verbose = verbose self._holds_lock = False self._db: sqlite3.Connection | None = None self._closed = False self._log_id: int | None = None self._opened_by = _owner_token() if self._mode is not READONLY and is_master_process(): # opening a store for writing is not writing to it: it may be # opened to read, or to release a lock left behind. so a lock # this cannot take is left for the write that meets it to raise self._open_and_claim(warn=False) def __getstate__(self) -> dict[str, object]: return {**self._init_vals} def __setstate__(self, state: dict[str, Any]) -> None: # this will reset connections to read only db's obj = self.__class__(**state) self.__dict__.update(obj.__dict__) def __del__(self) -> None: """drop the connection, leaving the lock to mark an unclosed store""" # no SQL: this runs during collection and at interpreter exit, where # a statement can block on another connection's write lock or find # the machinery it needs already torn down db: sqlite3.Connection | None = getattr(self, "_db", None) if db is None: return db.close() # the warning is about a lock left on disk for the next opener to # trip over, so it is worth making only by a store that took one and # wrote it somewhere that outlives the process. closing first means # a warning turned into an error cannot strand the connection if getattr(self, "_holds_lock", False) and self._source != _MEMORY: warnings.warn( f"data store {str(self.source)!r} was not closed, so it still " "holds the lock. call close() to release it", UserWarning, stacklevel=1, ) @property def source(self) -> str | Path: """string that references connecting to data store, override in subclass constructor""" return self._source @property def mode(self) -> Mode: """string that references datastore mode, override in override in subclass constructor""" return self._mode @property def limit(self) -> int | None: return self._limit def _check_open(self) -> None: """raise if the store has been closed""" if self._closed: msg = f"data store {str(self.source)!r} is closed" raise OSError(msg) def _open_and_claim(self, warn: bool = True) -> None: """open the database and take its lock for this session A lock another session holds leaves a store that reads but refuses every write until ``unlock(force=True)`` releases it. Reported as a warning unless *warn* is false. """ try: _ = self.db except OSError as refused: # a lock is the only OSError reachable from the one call site, # which is gated on the master: the store cannot yet be closed, # a worker's refusal to create cannot arise here, and sqlite's # own failures are not OSError. raising would make the message's # own advice impossible to follow, since it is this store that # has to carry out the release if warn: warnings.warn(str(refused), UserWarning, stacklevel=2) def _check_writing_session(self) -> None: """raise unless this is the session that opened the store Reading is open to any thread. """ # a write from elsewhere lands on a database this session holds the # lock on, under a log entry it did not open if _owner_token() == self._opened_by: return msg = ( f"data store {str(self.source)!r} was opened by {self._opened_by}, " f"which is the only session that may write to it. This is " f"{_owner_token()}." ) raise OSError(msg) def _open_rw(self) -> sqlite3.Connection: """open for writing, where only the master process creates the store An in-memory store is exempt, having no file to race over. """ if self._source == _MEMORY or is_master_process(): return open_sqlite_db_rw(self.source) # sqlite3.connect creates the file, so this comes before the connect if not Path(self.source).exists(): msg = ( f"data store {str(self.source)!r} does not exist, and only " "the master process creates one" ) raise OSError(msg) db = _connect_sqlite_db_rw(self.source) try: # a file that is not a database raises here rather than # answering, and the connection has to go either way valid = has_valid_schema(db) except BaseException: db.close() raise if valid: return db db.close() msg = ( f"data store {str(self.source)!r} has no schema, and only " "the master process creates one" ) raise OSError(msg) def _connection(self) -> sqlite3.Connection: """the connection, opened if it is not already, taking no lock""" # the open is under the same lock as every use of what it returns. # threads racing this check-then-set each open a database only one # of them goes on to use, and every other is orphaned: a descriptor # and a page cache nothing will close with self._cache_lock: self._check_open() if self._db is None: self._db = ( open_sqlite_db_ro(self.source) if self.mode is READONLY else self._open_rw() ) if self._db is None: msg = "database connection is unexpectedly None" raise ValueError(msg) return self._db @property def db(self) -> sqlite3.Connection: with self._cache_lock: db = self._connection() # taking the lock is a separate step from opening, and is # retried until it succeeds. a refusal that left a connection # behind would be taken as proof of a lock by every access # after it. # ownership is thread scoped, so a worker reading a store whose # lock is free must not take one here: the opening session # could then neither write behind it nor release it if not self._holds_lock and _owner_token() == self._opened_by: self.lock() return db def _db_for(self, *, claim: bool) -> sqlite3.Connection: """the connection, claiming the store's lock unless told not to ``claim=False`` reaches a store this session may not write to. """ return self.db if claim else self._connection() def _execute( self, sql: str, params: tuple[Any, ...] = (), *, claim: bool = True, ) -> None: """run a statement on the store's connection""" # a connection carries a statement cache that CPython keys by the # text of the statement and shares between threads without # protection, so two threads running one SELECT with different # parameters bind the same statement and read each other's rows. # every statement here and in _claim is taken under this lock with self._cache_lock: self._db_for(claim=claim).execute(sql, params) def _fetchone( self, sql: str, params: tuple[Any, ...] = (), *, claim: bool = True, ) -> Any: # noqa: ANN401 """the first row of a statement, or None if it produced none""" with self._cache_lock: cursor = self._db_for(claim=claim).execute(sql, params) try: return cursor.fetchone() finally: # a statement left part way through a scan is still checked # out of the cache, which is the state another thread trips # over, so the cursor goes before the lock does cursor.close() def _fetchall( self, sql: str, params: tuple[Any, ...] = (), *, claim: bool = True, ) -> list[Any]: """every row of a statement""" with self._cache_lock: cursor = self._db_for(claim=claim).execute(sql, params) try: return cursor.fetchall() finally: cursor.close() def _init_log(self) -> None: timestamp = datetime.datetime.now(tz=datetime.UTC) self._execute(f"INSERT INTO {LOG_TABLE}(date) VALUES (?)", (timestamp,)) self._log_id = self._fetchone( f"SELECT log_id FROM {LOG_TABLE} where date = ?", (timestamp,), )["log_id"] def close(self) -> None: """release the lock and the connection, ending the store's life Waits for any statement another thread has in flight. Warns if the lock is one this session may not release, since the store is going and nothing after it will report the lock left behind. """ with self._cache_lock: db: sqlite3.Connection | None = getattr(self, "_db", None) if db is None: self._closed = True return try: # an explicit close is the only thing that releases the # lock, so one still set marks a store whose session ended # another way self.unlock() # a lock taken by another thread is not this one's to # release, and the store is going, so nothing else will # report it held = self._lock_id if self._holds_lock else None if held is not None and self._source != _MEMORY: warnings.warn( f"data store {str(self.source)!r} is still locked by " f"{held}, call unlock(force=True) before closing to clear it", UserWarning, stacklevel=2, ) finally: # in a finally so a store whose lock could not be released # is still shut, rather than left usable by the failure self._db = None self._closed = True # all three describe the connection that is going self._log_id = None self._completed = [] self._not_completed = [] db.close() def read(self, unique_id: str) -> str | bytes: """reads data corresponding to identifier A log record is named "logs/", a result by its name alone. Either separator is accepted, since the table is taken with Path. """ uid_path = Path(unique_id) table_name = str(uid_path.parent) if table_name not in ( ".", LOG_TABLE, ): msg = f"unknown table for {str(uid_path)!r}" raise ValueError(msg) if table_name != LOG_TABLE: cmnd = f"SELECT * FROM {RESULT_TABLE} WHERE record_id = ?" result = self._fetchone(cmnd, (uid_path.name,)) return result["data"] cmnd = f"SELECT * FROM {LOG_TABLE} WHERE log_name = ?" result = self._fetchone(cmnd, (uid_path.name,)) return result["data"] @property def completed(self) -> list[DataMemberABC]: # the lock spans check, scan and publish, so concurrent readers make # one scan between them rather than one each with self._cache_lock: if not self._completed: self._completed = self._select_members( table_name=RESULT_TABLE, is_completed=True, ) return self._completed @property def not_completed(self) -> list[DataMemberABC]: """returns database records of type NotCompleted""" with self._cache_lock: if not self._not_completed: self._not_completed = self._select_members( table_name=RESULT_TABLE, is_completed=False, ) return self._not_completed def _select_members( self, *, table_name: str, is_completed: bool, ) -> list[DataMemberABC]: limit = f"LIMIT {self.limit}" if self.limit else "" rows = self._fetchall( f"SELECT record_id FROM {table_name} WHERE is_completed=? {limit}", (is_completed,), ) return [DataMember(data_store=self, unique_id=r["record_id"]) for r in rows] @property def logs(self) -> list[DataMemberABC]: """returns all log records""" rows = self._fetchall(f"SELECT log_name FROM {LOG_TABLE}") return [ DataMember(data_store=self, unique_id=_member_id(LOG_TABLE, r["log_name"])) for r in rows if r["log_name"] ] def _write( self, *, table_name: str, unique_id: str, data: str | bytes, is_completed: bool, ) -> DataMemberABC | None: """ Parameters ---------- table_name name of table to save data. It must be _RESULT_TABLE or _LOG_TABLE. unique_id unique identifier that data will be saved under. data data to be saved. is_completed flag to identify NotCompleted results Returns ------- DataMember instance or None when writing to _LOG_TABLE Raises ------ ValueError if unique_id does not name a record OSError if called from a session other than the one that opened the store """ self._check_writing_session() _check_identifier(unique_id) if self._log_id is None: self._init_log() if table_name == LOG_TABLE: # TODO how to evaluate whether writing a new log? cmnd = f"UPDATE {table_name} SET data =?, log_name =? WHERE log_id=?" self._execute(cmnd, (data, unique_id, self._log_id)) return None md5 = get_text_hexdigest(data) if unique_id in self and self.mode is not APPEND: cmnd = f"UPDATE {table_name} SET data= ?, log_id=?, md5=? WHERE record_id=?" self._execute(cmnd, (data, self._log_id, md5, unique_id)) else: cmnd = f"INSERT INTO {table_name} (record_id,data,log_id,md5,is_completed) VALUES (?,?,?,?,?)" self._execute(cmnd, (unique_id, data, self._log_id, md5, is_completed)) return DataMember(data_store=self, unique_id=unique_id) def drop_not_completed(self, *, unique_id: str | None = None) -> None: """remove not-completed records from the database Parameters ---------- unique_id if provided, only drop the record with this identifier, otherwise drop all not-completed records Raises ------ OSError if called from a session other than the one that opened the store """ self._check_writing_session() vals: tuple[int] | tuple[int, str] if not unique_id: cmnd = f"DELETE FROM {RESULT_TABLE} WHERE is_completed=?" vals = (0,) else: cmnd = f"DELETE FROM {RESULT_TABLE} WHERE is_completed=? AND record_id=?" vals = (0, unique_id) self._execute(cmnd, vals) self._not_completed = [] @property def _lock_id(self) -> int | str | None: """returns lock_pid: an owner token, or a bare pid from an older version""" result = self._fetchone( "SELECT lock_pid FROM state WHERE lock_pid IS NOT NULL " "ORDER BY state_id LIMIT 1", claim=False, ) return result[0] if result else None @property def locked(self) -> bool: """returns if the store is locked at all""" return self._lock_id is not None def lock(self) -> None: """if writable, locks the database to this store's session Does nothing if the store is read only or already holds the lock. Raises ------ OSError if the store is closed, or a lock is recorded that this store did not take """ with self._cache_lock: self._check_open() if self.mode is READONLY: return if self._db is None: msg = "database connection is unexpectedly None" raise RuntimeError(msg) # a store already locked is answered from this read, which needs # only a shared lock. claiming takes the write lock, so going # straight there would make a refusal wait out the busy timeout # behind any writer and then fail as OperationalError locked = self._lock_id # the flag alone is not trusted: the lock row it refers to can # have been cleared since, and then has to be claimed again if self._holds_lock and locked == _owner_token(): return if locked is None: locked = self._claim() # a lock marks a store whose session did not end through # close(), so its records were never confirmed complete. no # mode that writes may build on that without being told to if locked is not None: msg = ( f"You are trying to open {str(self.source)!r} for writing but " f"it is locked by {locked}. " ) # process and thread ids are both recycled, so a lock left # by an owner that is gone can carry a live one's token. the # advice for a lock this process holds covers that case too if locked == _owner_token(): msg += ( "That is this process, so another store in this " "process has it open for writing. Reuse that store, " "or close() it before opening a new one. If no such " "store is open, the lock is stale: call " "unlock(force=True) on a writable store to release it." ) elif isinstance(locked, str) and locked.startswith(f"{os.getpid()}:"): msg += ( "That is another thread of this process, which has a " "store open for writing. Write through that store, or " "close() it in that thread before opening a new one. " "If that thread has finished without closing it, the " "lock is stale: call unlock(force=True) on a writable " "store to release it." ) else: msg += "Call unlock(force=True) on a writable store to release it." raise OSError( msg, ) self._holds_lock = True def _claim(self) -> int | str | None: """record this session as the owner, or report who already is""" # the whole transaction is one region rather than a statement at a # time: a thread reading between the BEGIN and the COMMIT reads # inside a transaction it knows nothing of. so a BEGIN waiting out # the busy timeout behind a competing process holds up every reader # of this store, not just this one with self._cache_lock: db = self._connection() db.execute("BEGIN IMMEDIATE") try: result = db.execute( "SELECT state_id,lock_pid FROM state ORDER BY state_id", ).fetchall() # is not None rather than truthy: a lock recorded as 0 is a lock locked = next( (r["lock_pid"] for r in result if r["lock_pid"] is not None), None, ) if locked is None and result: # we will update an existing state_id = result[0]["state_id"] db.execute( "UPDATE state SET lock_pid=? WHERE state_id=?", (_owner_token(), state_id), ) if len(result) > 1: # rows the two-statement version left behind. reaching # here means none of them holds a lock, and only the # first carries a record_type, so they record nothing db.execute("DELETE FROM state WHERE state_id>?", (state_id,)) elif locked is None: db.execute( "INSERT INTO state(lock_pid) VALUES (?)", (_owner_token(),), ) # inside the try: a commit wants an exclusive lock where the # begin wanted a reserved one, so it is the statement here most # likely to be refused, and one left unended strands the # connection in a transaction it can never start another after db.execute("COMMIT") except BaseException: # sqlite ends the transaction itself on some errors, and asking # again then raises over the real failure if db.in_transaction: db.execute("ROLLBACK") raise return locked def unlock(self, force: bool = False) -> None: """remove a lock this session took. If force, remove any. ignored if mode is READONLY""" with self._cache_lock: self._check_open() if self.mode is READONLY: return lock_id = self._lock_id if lock_id is None: return # a lock recorded by an older version names only a process, # which this session cannot claim to be, so clearing one takes # force if lock_id == _owner_token() or force: self._execute("UPDATE state SET lock_pid=NULL", claim=False) self._holds_lock = False @extend_docstring_from(DataStoreDirectory.write) def write(self, *, unique_id: str, data: str | bytes) -> DataMemberABC: # type: ignore[override] if unique_id.startswith(RESULT_TABLE): unique_id = Path(unique_id).name super().write(unique_id=unique_id, data=data) self.drop_not_completed(unique_id=unique_id) member = self._write( table_name=RESULT_TABLE, unique_id=unique_id, data=data, is_completed=True, ) if member is None: msg = "write to results table failed to produce a member" raise RuntimeError(msg) if member not in self._completed: self._completed.append(member) return member @extend_docstring_from(DataStoreDirectory.write_log) def write_log(self, *, unique_id: str, data: str | bytes) -> None: if unique_id.startswith(LOG_TABLE): unique_id = Path(unique_id).name super().write_log(unique_id=unique_id, data=data) _ = self._write( table_name=LOG_TABLE, unique_id=unique_id, data=data, is_completed=False, ) @extend_docstring_from(DataStoreDirectory.write_not_completed) def write_not_completed( # type: ignore[override] self, *, unique_id: str, data: str | bytes ) -> DataMemberABC: if unique_id.startswith(RESULT_TABLE): unique_id = Path(unique_id).name super().write_not_completed(unique_id=unique_id, data=data) member = self._write( table_name=RESULT_TABLE, unique_id=unique_id, data=data, is_completed=False, ) if member is None: msg = "write to results table failed to produce a member" raise RuntimeError(msg) self._not_completed.append(member) return member def md5(self, unique_id: str) -> str | None: """ Parameters ---------- unique_id name of data store member Returns ------- md5 checksum for the member, if available, None otherwise """ cmnd = f"SELECT * FROM {RESULT_TABLE} WHERE record_id = ?" result = self._fetchone(cmnd, (unique_id,)) return result["md5"] if result else None def write_citations(self, *, data: tuple[CitationBase, ...]) -> None: if not data: return self._check_writing_session() if not self._has_citations_table(): self._execute( "CREATE TABLE IF NOT EXISTS citations" "(citation_id INTEGER PRIMARY KEY, data TEXT)", ) from citeable import to_jsons json_data = to_jsons(data) if existing := self._fetchone("SELECT citation_id FROM citations"): self._execute( "UPDATE citations SET data=? WHERE citation_id=?", (json_data, existing["citation_id"]), ) else: self._execute("INSERT INTO citations(data) VALUES (?)", (json_data,)) def _load_citations(self) -> list[CitationBase]: from citeable import from_jsons if not self._has_citations_table(): return [] result = self._fetchone("SELECT data FROM citations") return from_jsons(result["data"]) if result else [] def _has_citations_table(self) -> bool: result = self._fetchone( "SELECT name FROM sqlite_master WHERE type='table' AND name='citations'", ) return result is not None def _describe(self) -> dict[str, object]: if self.locked and self._lock_id != _owner_token(): title = ( f"Locked db store. Locked by {self._lock_id}, " f"this session is {_owner_token()}." ) elif self.locked: title = "Locked to the current process and thread." else: title = "Unlocked db store." result = super()._describe() result["title"] = title return result @property def record_type(self) -> str: """class name of completed results""" result = self._fetchone("SELECT record_type FROM state") return result["record_type"] @record_type.setter def record_type(self, obj: object) -> None: from scinexus.misc import get_object_provenance self._check_writing_session() rt = self.record_type if self.mode is OVERWRITE and rt: msg = f"cannot overwrite existing record_type {rt}" raise OSError(msg) n = get_object_provenance(obj) self._execute("UPDATE state SET record_type=? WHERE state_id=1", (n,)) def _summary_not_completed(self) -> list[dict]: """returns a list of dicts summarising not completed results""" from scinexus.data_store import summary_not_completeds from scinexus.io import DEFAULT_DESERIALISER return summary_not_completeds( self.not_completed, deserialise=DEFAULT_DESERIALISER, ) cogent3-scinexus-b16bc06/src/scinexus/typing.py000066400000000000000000000173001525761402500216440ustar00rootroot00000000000000"""defined type hints for app composability""" from __future__ import annotations from pathlib import Path from types import UnionType from typing import ( TYPE_CHECKING, Any, ForwardRef, Protocol, TypeVar, Union, get_args, get_origin, runtime_checkable, ) from scinexus.data_store import DataMemberABC if TYPE_CHECKING: from collections.abc import Callable NESTED_HINTS = (Union, UnionType, list, tuple, set) _type_namespace_providers: list[Callable[[], dict[str, type]]] = [] def register_type_namespace( provider: Callable[[], dict[str, type]], ) -> None: """register a lazy namespace provider for forward-reference resolution Parameters ---------- provider a zero-arg callable returning a dict of {name: type}. It is invoked lazily (each time _resolve_name needs a fallback) so downstream packages can defer heavy imports. Providers are responsible for their own caching. Notes ----- Registration is idempotent: re-registering the same callable is a no-op. Providers are consulted in registration order, and the first provider that yields ``name`` wins. """ if provider not in _type_namespace_providers: _type_namespace_providers.append(provider) def _clear_type_namespace_providers() -> None: """remove all registered namespace providers (intended for tests)""" _type_namespace_providers.clear() @runtime_checkable class HasSource(Protocol): @property def source(self) -> Any: ... @runtime_checkable class HasInfo(Protocol): @property def info(self) -> HasSource: ... @runtime_checkable class SerialisableType(Protocol): """a runtime-checkable protocol for objects that serialise to a dict Any object that implements a ``to_rich_dict`` method returning ``dict[str, object]`` satisfies this protocol. Writer apps rely on this to convert results before storing them in a data store -- both ``DataStoreDirectory`` and ``DataStoreSqlite`` call ``to_rich_dict()`` during the write path. """ def to_rich_dict(self) -> dict[str, object]: ... IdentifierType = str | Path | DataMemberABC """accepted types for identifying members of a data store A loader app can receive a file path as a string, a ``pathlib.Path``, or a ``DataMemberABC`` handle returned by iterating over a data store.""" def _resolve_name( name: str, module_globals: dict[str, object] | None = None ) -> type[Any]: """resolves a string name to a type Checks ``module_globals`` first, then falls back to any namespace providers registered via :func:`register_type_namespace`. """ if module_globals and name in module_globals: result = module_globals[name] if isinstance(result, type): return result for provider in _type_namespace_providers: ns = provider() if name in ns: result = ns[name] if isinstance(result, type): return result msg = f"cannot resolve type name {name!r}" raise TypeError(msg) def resolve_type_hint( hint: object, module_globals: dict[str, object] | None = None ) -> object: """walks a type hint tree and resolves all forward references to classes Parameters ---------- hint a type hint (TypeVar, Union, ForwardRef, str, or concrete class) module_globals optional dict of the module where the hint was defined, used to resolve forward references from user code """ # Protocol classes (like SerialisableType) -- return as-is if getattr(hint, "_is_protocol", False) and hint is not Protocol: return hint # TypeVar with __bound__ -> resolve bound class if isinstance(hint, TypeVar): if hint.__bound__: bound = hint.__bound__ if isinstance(bound, ForwardRef): bound = _resolve_name(bound.__forward_arg__, module_globals) return bound if hint.__constraints__: resolved = tuple( resolve_type_hint(c, module_globals) for c in hint.__constraints__ ) return Union[resolved] # type: ignore # noqa: UP007 msg = f"unconstrained TypeVar {hint!r} cannot be resolved" raise TypeError(msg) # Union / UnionType -> recurse origin = get_origin(hint) if origin is Union or isinstance(hint, UnionType): args = tuple(resolve_type_hint(a, module_globals) for a in get_args(hint)) return Union[args] # type: ignore # noqa: UP007 # Container types (list[X], tuple[X,Y], set[X]) if origin in (list, tuple, set): args = tuple(resolve_type_hint(a, module_globals) for a in get_args(hint)) return origin[args] if args else hint # ForwardRef if isinstance(hint, ForwardRef): return _resolve_name(hint.__forward_arg__, module_globals) # plain str return _resolve_name(hint, module_globals) if isinstance(hint, str) else hint def get_type_display_names(hint: object) -> frozenset[str]: """extracts human-readable class names from a resolved type hint Parameters ---------- hint a resolved type hint (one that has been through resolve_type_hint) """ names: set[str] = set() origin = get_origin(hint) if origin is Union or isinstance(hint, UnionType) or origin in (list, tuple, set): for arg in get_args(hint): names |= get_type_display_names(arg) elif isinstance(hint, type): names.add(hint.__name__) elif isinstance(hint, TypeVar): # fallback for unresolved TypeVars -- shouldn't normally happen names.add(hint.__name__) return frozenset(names) def _get_concrete_classes(hint: object) -> set[type[Any]]: """extracts concrete classes from a resolved type hint, walking Unions""" classes = set() origin = get_origin(hint) if origin is Union or isinstance(hint, UnionType): for arg in get_args(hint): classes |= _get_concrete_classes(arg) elif origin in (list, tuple, set): classes.add(origin) elif isinstance(hint, type): classes.add(hint) return classes def _is_protocol(hint: object) -> bool: """checks if a type hint is or contains a runtime-checkable Protocol""" if getattr(hint, "_is_protocol", False) and hint is not Protocol: return True origin = get_origin(hint) if origin is Union or isinstance(hint, UnionType): return any(_is_protocol(a) for a in get_args(hint)) return False def check_type_compatibility(return_hint: object, input_hint: object) -> bool: """composition-time check: is the return type compatible with the input type? Parameters ---------- return_hint resolved return type of the upstream app input_hint resolved input type of the downstream app Returns ------- True if the types are compatible, False otherwise """ # typing.Any is compatible with everything if return_hint is Any or input_hint is Any: return True # If either side is or contains a Protocol, be lenient -- runtime check_type # provides the real safety net if _is_protocol(return_hint) or _is_protocol(input_hint): return True return_classes = _get_concrete_classes(return_hint) input_classes = _get_concrete_classes(input_hint) # Check if any return class is a subclass of any input class (or vice versa) for ret_cls in return_classes: for inp_cls in input_classes: try: if issubclass(ret_cls, inp_cls) or issubclass(inp_cls, ret_cls): return True except TypeError: # issubclass can fail for some types if ret_cls is inp_cls: return True return False cogent3-scinexus-b16bc06/src/scinexus/warning.py000066400000000000000000000170211525761402500217770ustar00rootroot00000000000000from __future__ import annotations import functools import inspect from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast from warnings import catch_warnings, simplefilter from warnings import warn as _warn if TYPE_CHECKING: # pragma: no cover from collections.abc import Callable, Sequence from typing import Any, Literal def deprecated( _type: Literal["class", "method", "function", "argument", "module"], old: str, new: str, version: str, reason: str | None = None, stack_level: int = 4, ) -> None: """a convenience function for deprecating classes, functions, arguments. Parameters ---------- _type should be one of class, method, function, argument, module old, new the old and new names version the version by which support for the old name will be discontinued reason why, and what choices users have stack_level as per warnings.warn """ msg = f"{_type} {old} will be removed in version {version}, use {new} instead" if reason is not None: msg = f"{msg}\nreason={reason!r}" with catch_warnings(): simplefilter("always") _warn(msg, DeprecationWarning, stacklevel=stack_level) def discontinued( _type: Literal["class", "method", "function", "argument", "module"], old: str, version: str, reason: str | None = None, stack_level: int = 4, ) -> None: """convenience func to warn about discontinued attributes Parameters ---------- _type should be one of class, method, function, argument, module old the attributes name version the version by which support for the old name will be discontinued reason why, and what choices users have stack_level as per warnings.warn """ msg = f"{_type} {old} is discontinued and will be removed in version {version}" if reason is not None: msg = f"{msg}\nreason={reason!r}" with catch_warnings(): simplefilter("always") _warn(msg, DeprecationWarning, stacklevel=stack_level) _discontinued = discontinued # renamed to avoid name clash with discontinued argument in deprecated args decorator P = ParamSpec("P") R = TypeVar("R") def deprecated_args( *, version: str, reason: str, old_new: Sequence[tuple[str, str]] | None = None, discontinued: Sequence[str] | None = None, stack_level: int = 4, ) -> Callable[..., Any]: """ A decorator that marks specific arguments of a function as deprecated. The decorator accepts a list of 2-tuples specifying the mapping of old argument names to new argument names. When the decorated function is called with any of the old argument names, they will be replaced with their corresponding new names in the kwargs dictionary. Parameters ---------- version The version when the old arguments will be removed in calver format, e.g. 'YYYY.MM' reason Reason for deprecation or guidance on what to do old-new A list of deprecated old and replacement new argument names. discontinued Names of single or multiple arguments to be discontinued. This should only be applied to arguments that have no effect. Returns ------- The decorated function. Warnings -------- DeprecationWarning A warning will be raised when the decorated function is called for each deprecated argument used in the calling function. Examples -------- To use, change the signature of the function / method by removing the deprecated / discontinued arguments. Apply the decorator to the function, indicating the old and new the argument names. >>> @deprecated_args( ... version="2024.1", ... reason="Use new_name instead", ... old_new=[("old_arg", "new_arg")], ... ) ... def my_function(new_arg): ... return new_arg >>> my_function(new_arg=42) 42 """ if old_new is None: old_new = [] discontinued = [discontinued] if isinstance(discontinued, str) else discontinued old_args = dict(old_new).keys() def decorator(func: Callable[P, R]) -> Callable[P, R]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: if old_args & kwargs.keys(): for old, new in old_new: if old in kwargs: kwargs[new] = kwargs.pop(old) deprecated( "argument", old, new, version, reason, stack_level=stack_level, ) if discontinued: for dropped in discontinued: if dropped in kwargs: _discontinued( "argument", dropped, version, reason, stack_level=stack_level, ) kwargs.pop(dropped) return func(*args, **kwargs) return wrapper return decorator def deprecated_callable( *, version: str, reason: str, new: str | None = None, is_discontinued: bool = False, stack_level: int = 4, ) -> Callable[[Callable[P, R]], Callable[P, R]]: """ A decorator that marks callables (function or method) as deprecated or discontinued.. Parameters ---------- version The version when it will be removed in calver format, e.g. 'YYYY.MM' reason Reason for deprecation or guidance on what to do new If the callable is being replaced, this is the replacement, e.g. 'ClassName.new_method()' is_discontinued If True the callable is being discontinued. stack_level as per warnings.warn Returns ------- The decorated callable. Warnings -------- DeprecationWarning A warning will be raised when the decorated function is called. Examples -------- Here's an example of how to use the `deprecated_callable` decorator to mark the function `my_function` as deprecated in favour of a new function. >>> @deprecated_callable( ... version="2023.6", reason="function rename", new="a_function" ... ) ... def my_function(arg): ... return arg >>> callable(my_function) True """ def decorator(func: Callable[P, R]) -> Callable[P, R]: sig = set(inspect.signature(func).parameters) _type: Literal["method", "function", "class"] = ( "method" if sig & {"self", "cls", "klass"} else "function" ) old = func.__name__ if old in {"__init__", "__init_subclass__"}: # we're really deprecating a class, so get that name old = func.__qualname__.split(".")[-2] _type = "class" if not is_discontinued and new is None: msg = "Must specify new callable if deprecating." raise ValueError(msg) def depr_func() -> None: if is_discontinued: discontinued(_type, old, version, reason, stack_level) else: deprecated(_type, old, cast("str", new), version, reason, stack_level) @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: depr_func() return func(*args, **kwargs) return wrapper return decorator cogent3-scinexus-b16bc06/tests/000077500000000000000000000000001525761402500164715ustar00rootroot00000000000000cogent3-scinexus-b16bc06/tests/conftest.py000066400000000000000000000021621525761402500206710ustar00rootroot00000000000000from collections.abc import Iterator from pathlib import Path import pytest from scinexus.data_store import set_id_from_source from scinexus.parallel import set_parallel_backend from scinexus.typing import register_type_namespace try: from cogent3.app.typing import _get_resolution_namespace register_type_namespace(_get_resolution_namespace) except ImportError: pass @pytest.fixture def tmp_dir(tmp_path_factory): return tmp_path_factory.mktemp("scinexus_tests") @pytest.fixture(scope="session") def DATA_DIR(): return Path(__file__).parent / "data" @pytest.fixture def HOME_TMP_DIR() -> Iterator[Path]: """makes a temporary directory""" import tempfile home = Path("~") with tempfile.TemporaryDirectory(dir=home.expanduser()) as dn: yield home / dn @pytest.fixture def reset_id_from_source() -> Iterator[None]: """Restore the default ID extractor after the test.""" yield set_id_from_source(None) @pytest.fixture def reset_parallel_backend() -> Iterator[None]: """Restore the default parallel backend after the test.""" yield set_parallel_backend(None) cogent3-scinexus-b16bc06/tests/data/000077500000000000000000000000001525761402500174025ustar00rootroot00000000000000cogent3-scinexus-b16bc06/tests/data/brca1.fasta000066400000000000000000005102501525761402500214150ustar00rootroot00000000000000>FlyingFox TGTGGCACAAATGCTCATGCCAGCTCTTTACAGCATGAGAAC---AGTTTATTATACACTAAAGACAGAATGAATGTAGA AAAGACTGACTTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAACAGATGGGTTGAAACTAAGGAAA CATGTAACGAT---ATGCAGACTTCCAGCACAGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTGAATGGGAGAATAGAA CTGAATAAGCAGAAACCTCCATGCTCTGACAGTCCTAGAGAT---TCTCAAGAT---ATTTCTTGGATAACACGGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAATATTAACTTCTGATGTCTCACCTGATGGGAGGTCTG AATCAAATGTG---------------GTAGAAGTTCCAAAT------GAAGTAGATGGATACTCTGGTGCTTCAGAGAAA ATAGCTTTAAAGGCCAATGATCCTCATGGTGCTTTAATGTGC------GAAAGAGTTCACTCCAAACTGGTAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACATATCGGAGGAAAGCAAGCCTCCCTAACTTGAGCCACATAACTGAAAATC TAATTACAGGAGCATCTGCTATAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGTATCCCCTCACAAATAAACTAAAGCGTAAAAGGAGAACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAATAGATTTGGCAGTTGTTCAGAAAACTCCGGAAAACATAATTGAGGAAACTGACCAAATAG AGCAGAAT---------GGTCATGTGATGAATAGTACTAATAATGGTCATGAGAATGAAACAAAAGGTGATTAT---GTT CAGAAGAAGAAAAATACAAACCCAACAGAA------TCATTGGAAAAAGAATCTACTTTCAAAACTAAAGCTGAACCTAT ATGCAGCAGCATAAGCAATATGGAACTAGAATTAAATATCCACAGTTCAAAAGCAGTTAAGAAGAATAGGCTGAGGAGGA AGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGCCCACCTAATCATACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---CTGAAGGAAAAA---AATTCTGACCAAATGCCAGTCAGACACAGCAAAAA ACTTCAATTCATAGAAGATAAAGAATCTTCAACTGGAGCCAAGAAGAATAACAAGCCAAATGAGACAATCAATAAAAGAC TTGCCAGTGATGCTTTTCCAGAATTAAATATAACAAACATACCTGGTTTTTTTACTAATGGTTCAAGTTCTAATAAACTT CAAGAGTTTGTCAATCCTAGCCTTCAAAGAGAAGAAATAAAAGAGAAC---CTAGGAACAATTCAAGTGTCTAATAGTAC CAAGGACCCCAAAATTTTGATCTTCGGTGAAGGAAGA---GGTTCACAA---ACTGATCGATCTACAGAGAGTACCAGTA TTTTATTGGTGCCTGAAACGGATTATGGCACTCAAGATAGTATCTCATTACTGGAACCTGACATCCCAGAG---AGGGTA AAG---ACAGCACCAAACCATCAT------------GCAGCAATTAAAAACCCCAGAGAACTTATTCATGGTTGT---TC TGAAGATACTAGAAATGATGCAGAGGGCTTTAAAGATCCATTGAGACGTGAAGTTAAC---TACANNNNNNNNNNN---- -----NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN NNTTCAAACCCAGGAAATCTAGAAAAGGAATGTGCAACAGGCTATGCCCACTCCAAGTCCTTGAGGAAACAAAGTCCAAA AGTCACTCTTGAATGTGACCGAAAAGAA---AATCAGGGAAAGAAAGAGTCTAACGTCAAGCATGTGCAGGCAGTTTATA CAACTGTAGGCTTTCCTGTGGTTTGTGAGAAAGAAAAAAAGCCAGGAGATTATGCTAAATATGGCATAAAAGAAGTCTCT AGGCTTTGTCAGTCATTTCAGTTCAGA---GAAAATGAAACTGAACTCACTATTGCAAATAAACTTGGAATTTCACAAAA CCCATATCATATGCCATCCATTCCTCCCATCAAGTCATCTGTTAAAACTACATGTAAGAAAAAT---CTGTCAGAGGAAA AGTTTGAAGAACATTCAATATCCCCTGAAAGAACAATAGGAAATGAGACCATCATTCAAAGTACAGTGGGCACAATTAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAGGCAGCTCAAGCAGTATTTATGAAGCAGGTTCCAGTACTAACGAACT AGGCTCTAGTGTCAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAAGTA GAAACAGAGGACCACAATTAAATGCTGTGCTTCAATTGGGTCTCATGCAGCCTGAAGTCTATAAGCAAAGCCTT---CCT CTAAGTAATTGTAAACATCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAATGCAGATGTCTC TCTACGTCAGATTTCAGATAACTTAGAGCAA---CCTATGGGAAACAGTAATGCTTCTCAGGTTTGTTCTGAGACACCGG ATGACCTGTTAAATGATGACAAAATAAAAGAGAATATCGGCTTTGATGAAAGTGGCATTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAGAAAGGAGAATTCAAAAGGAGCCCTAGTCCCTTAGCCCAT---ACAAGTTTGTCTCAAGGTCGCCG AAGAGGGGCTAGGAAATTAGAGTCCTCAGAAGAGGA------------- >DogFaced TGTGGCACAAATACTCATGCCAACTCATTACAGCATGAGAACAGCAGTTTATTATACACTAAAGACAGAATGAATGTAGA AAAGACTGACTTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGCAGAACAGATGGGTTGAAACTAAGGAAA CATGTAATGAT---AGGCAGACTTCCAGCANAGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTGAATGGAAGAATAAAA CTGAATAAGCAGAAACCTCCATGCTCTGACAGTCCTAGAGAT---TCCAAAGAT---ATTCCTTGGATAACACGGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAACATTAACTTCTGATGTCTTACTTGATGAGAGGTCTG AATCAAATGTG---------------GTAGAAGTTCCAAAT------GAAGTAGATGGATACTCTGGTGCTTCAGAGGAA ATAGCCTTAAAGGCCAGTGATCCTCATGGTGCTTTAATATGT------GAAAGAGTTCACTCCAAATTGATAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACATATCGGAGGAAAGCAAGCCTCCCTAACTTAAGCCACATAACTGAAAATC TAATTACAAGAGCATCTGCTACAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGTGCCCCCTCACAAATAAACTAAAACGTAAAAGAAGAACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAATAGATTTGACAACTGTTCAAAAAACTTCTGAAAATATAATTGAGGGAACTGACCAAATAG AGCAGAAT---------GGTCATGTGATGAATAGTTCTAATGATGGTCATGAGAATGAAACAAAAGGTGATTAT---GTT CAGAAGAAGAAAAATACAAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTAAAGTTGAGTCTGT ACCCAACAACATAAGCAATGTGGAACTAGAATTAAATATTCACGGTTCAAAAGCACTCAAGAAGAATAGNCTGAGGAGGA AGTCCTNTACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATTCAAGCCCACCTAATCATACTGAACTACAA ATTGATAGTTGTTCCAGCAGTGAAGAA---CTGAAGGAAAAA---AATTCTGACCGAATGCCAGACAGACACAGCAAAAA ACTTCAGTTCGTAGAAGATAAAGAATCTGCAACTGGAGCCAAGAAGAATAACATGCCAAATGAGGCAATAAATAAAAGAC TTTCCAGTGAAGCTTTTCCCGAATTAAATATAACAAACGTACCTGGTTTTTTTACTAATGGTTCAAGTTCTAATAAACGT CAAGAGTTTGTCAATCCTAGCCTTCAAGGAGAAGAAATAAAAGAGAAT---CTACGAACAATTCAAGTGTCTAATAGCAC CAAAGACCCCAAAATTCTAATCTTTGGTGAAGGAAGA---GGTTCACAA---ACTGATCGATCTACAGAGAGTACCAGTA TTTTATTGGGACCTGAAACGGATTATGGCACTCAAGATAGTATCTCATTACTGGAATCTGACATCCCAGGG---AGGGCA AAG---ACAGCACCAAACCAACATGCAGATCTGTGTGCAGCAATTGAAAACCCCAGAGAACTTATTCATGATTGT---TT TAAAGAAACTAGAAATGACACAGAGAGCTTTAAAGATCCATTGAGACATGAAGTTAAC---TCCACNNNNNNNNNN---- -----NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN NNTTCAGACCCAGGAAATCTAGAAAAGGAATGTGCAACAGGCTATGCCCACTCCAGGTCCTTGATAAAACAAAGTCCAAA AGTCACTCTTGAATGTGACCGAAAAGGA---AATCAGGGAAAGAAAGAGTCTAACATNGAGCATGTGCAGGCAGTTTATA CAACTATAGGCTTTCCTGGGGTTTCTGAGAAAGACAAAAAGCCAGGAGATTATGCCAGATATGGCATAAAAGAAGTCTCT AGGCTTTGTCAGTCATTTCAGTCTAGA---AGAAATGAAACTGAGCTCACTATTGCAAATAAACTTGGACTTTCACAAAA CCCATATCATATGCCATCCATTTCTCCCATCAAGTCATCTGTTAAAACTATATGTAAGAAAAAT---CTGTCAGAGGAAA AGTTTGAAGAACATTCAATATTCCCTGAAAGAGCAATAGGAAATGAGACCATCATTCAAAGTACAGTGGGCACAATTAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAGGCAGCTCAAGCGGTATTTATGAAGCAGGTTCCAGTACCAATGAACT AGGCTCTAGTGTCAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAAGTA GAAACAGAGGACCAAAATTAAATGCTGTGCTTCAGTTGGGTCTCATGCAGCCTGAAGTCTATGAGCAAAGCCTT---CCT CTAAGTAATTGTAAACATTCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTGGTTCAGGCTGTTAATGCAGATGTCTC TCCANGTCAAATTTCAGATAACTTAGAGCAA---CCTATGGGAAACAGTAATATTTCTCAGGTTTGTTCTGAGACACCGG ATGACCTGTTAAATGATGACAAAATAAAGGACAATATCAGCTTTGATGAAAGTGGCATTCAGGAAAGATCTGCTGTTTTT AGCAAAAATGTCCAGAAAGGAGAATTCAGAAGGAGCCCTAGTCCCTTAGCCCAT---GCAAGTTTGTCTCAAGGTCGCCC AAGAAGGGC---------------------------------------- >FreeTaile TGTGGCACAGATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTACTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAGCAGCCTGGCTTAGCAAAGAGCCAGCAGAGCAGATGGGCTGAAAGTAAAGAAA CATGTAATGAT---AGGCAGACTCTCAGCACAGAGAAAAGGGTAGTTCTGAATGCTGATCCTCTGAATAGG--------- ------AGAAAAGAACCTCCAGGCTCTAACTATCCTAGAGAT---TCCCAAGAT---GTTCCTTGGATAACACGGAGTAG TAGCATACAGAAAGTTAATGAGTGGTTCTCCAGACGTGATGAAATACTAACTACTGGTGGCTCACATAACGGTAGATTTG AATCAAATGTTGAAGTAGCTGGTGCAGTAGAAGTTCCAAAT------GAAGTAGATGGATATTCTGGTTCTCCAGAGAAA ATAGCCTTAATGGCCTGTGATCCTCCTGATGCTTTAATATGT------GAAAGAGTCTCCTCTAAACCACTAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGAAAGGCAAGCTTCCCTAACTTGAGCCACATAAGTGAAAATC TAATTATAGGAGCATCTGCTATAGAACCCCAGGTAACAAAA--------------------------------------- ---------------------GAATGTCCCCTCACAAATAAACTAAAGCGTAAAAGAAGA---ACATCAAGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCAGTTGTTCAAAAGACTCTTGAAAAGGTAATTGAGGGAACTGACCAAATAG AACAGAAT---------GGTCATGTGATGAGTATTACTAGTAATTGTCATGAGAATAAAACAAAAGGTGATTAT---GTT CAGAAAGAGAAAAATTCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCACAACTAAAGCTGAACCTAT AAGCAGCAGCATAAGCAATATGGAACTAGAGTTAAATATCCACAGTTCAAAAGCACCTAAGAAGAATAGGCTGAAGAGGA AGTCCTCTACCAGGCACATTTATGCACTTGAACTAGTAGTCAATAGAAATCCAAGCCCACCTAATCACACTGAACTACAA ATTGATAGCTGTTCTAGCAGTGAAGAG---GTGAAGGAAAAA---AATTCTGACCAAATACCAGTCAGACACAGCAAAAA GCTTCAACTCATGGAAGGTAAAGAACCTGCAACTGGAGCCAAGAAGAGTAACAAATCAAATGAACAAATAAATAAAAAAC TTGCCAGTGATGTTTTTCCAGAACTAAACTTAACAAACATACCTGGTTTTTTAAGTAATGATTCAAGTTCTAATAAACTT AAAGAGTTTGTCAATCCTAACCTTCAAAGAGAAGAAATAACAGAGAAC---CTAGGAACAGTTCACATGTCTAATAGTAC CAAAGACCTCAAAGATCTGATATTAAGTGGAGGAAGA---AGTTTGCAA---ACTGATAGATCTATGGAGAGTACCAATA TTTTATTGGTACCTGAAACTGATTATGGCACGCAGGATAGTATCTCATTACTGGAACCTGACACCCCAGGG---AAGGCA AAA---AAAGCTCCAAATCAATATGCGGGTCTGTGTGCAGAAATTAAAAACCNCAAGGAACTTATCCATGGTTGT---TC TAACGATAATAGAAATGACAGAGAGGACATTAAGGATCTATTGAGACCTGAAGTTAAC---CACANNNNNNNNNNNNNNN NNNNNNNGGAAGAGAGTGAACTTGATACACAGTATTTACAGAATACATTCAAGGTTTCAAAACGTCAGTCGTTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATATGCAACAGTCTATGGCCACTCCAGGTCCTTAAGGAAACAAAGTCCAAA AGTCACTCTTGAATGTGGACAAAAAGAAGAAAATCAGGGAGAGAAAGAATCTGAAATCAAGTATGTACGGGGAGTTCACA CAACTGCAGGCTTTCCTGTGGTTTGTGAGAAAGACGAAAAGCCAGAAGAATATGCCAAATGTAGCATAAAAGGAACCTCT AGCCTTTGTCAGCCACCTCAGTTCAGA---GGCAACGAAACTGAACTCACTATTGCAAATAAACCCGGAATTTCACGAAA CCCATATCATATACCATGCATTTCTCCCATCAGGTCCTTTGTTAAAACTATAAATAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACATTCAGTGTCACCTGAAAGAGCAATGAGAAATGAGAAT---ATTCTAAGTACAGTGAGCCCAATTAGC CTAAATAAC---AGAGAAAGCACTTTTAAAGAAGGCAGCTCAAGC---------------------AGTACTAATGAAGT AGGCTCTAGTACCAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGATCAAAAATAAATGCTATGCTCAGATCAGGTCTCATGCAACCTGAAGTCTATAAGCCAAGCCTT---TCT GTAAGTAATTGTGAACATCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAATGCAGATTTCTC TCCATGTCAAATTTCAGATAACTTAGAACAA---TCTATGGGAAGTACTCCTGCTTCTCAGGTTTGTTCTGAGACACCAG ATGACCTGTTAAATGATGACAAAATAAAGGAGAATAGCAGCTTTGCTGGAAGTGGCATTAAGGAAAGATCTGCTATTTTT AGCAAAAGTGTCAAGAAAGAAAAATTCAGAAGGAGCCCTAGCCCCTTTGCCCAT---ACACATTTGACTCATACTCGCCA AAGAGGGGCCAGGAAATTAGAGTCCTCAG-------------------- >LittleBro TGTGGCACAGATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTACTACTCACCAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAGCAGCCTGGCTTAGCAAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAAGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAGGGTAATTCTGGATGCTGATCCTCTGAATGGG--------- ------GAAAAAGAACTTCCACGCTCTGACCATCCCAGAGAT---TCCCAAGAT---GTGCCTTGGATAACACGGAGCAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGGCGTGATGAAATACTAACTTCTGATGGCTCACATAATGGCAAGTCTG AGTCAAATGCTGAAGTAGCTGGTGCAGTGGAAGTTGCAAAT------GAAGTAGATGGGTATTCTGGTTCTCCAGGGAAA ATAACCTTAATGGCCCATGATCCTCATGGTGCTTTAACCTGTGAAAGTGAAAGAGTTCACTCCAAACCAGTAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCAAACTTGAGCCACATAACTAAAAATC TAATTGTAGGAGCATCTGCTATAGAACCTCAGATAGCACAA--------------------------------------- ---------------------GAGTGTCCCCTCACAAATAAACTAAAGCGTAAAAGGAGAAGTACATCNGGCCTTCATCC TGAGGATTTTATTAAGAAAGTAGATTTGGCAGTTGTTCAAAAGACTCCTGAAGAGATAATTAAGGGAACTGACCGAATAG AACAGAAT---------GGTCATGAGATGAATATTACTAATAATGATCATGAGAATGAAACAAAAGGTGATTGT---GTT CAGAAAGAGAAAAATGCTAACCTAACAGAA------TCACTGGAAAAAGAATCTGCGTTCACAAGTAAAGCTGAACCTAT AAGCAGCAGCATAAGCAATATGGAACTAGAATTAAATGTCCACAGTTCAAAAGCACCTAAGAAGAATAGGCTGAAGAGGA AGTCCTCTACCAGGCCTATTCATGCACTTGAACTAGTAGTCAATAGAAATCCGAGCCCATCTAACCATACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---GTGAAGGAAAAA---AATTCTGACCAAATACCAGTCAGACACAGCAAGAA GCTTCAACTCATGGAAGGTAAAGAACCTGCAGCTGGAGCCAAGAAGAGTAATAAGTCAGATGAACAAATAAATAAAAAAC TTGCCAGTGATGCTTTTCTAGAACAAAACTTAACAAACATGCCTGGTGTTTTTACTAATGGTTTAAGCTCTAATAAGCTT AACGAGTTTGTCGATCCTAACCTACAAAGAGAAGAAACAGAAGAGAAC---CTAGGAGCAGTTCAAATGTCTAATAGTAC CAAAGACCTCGAAGATCTGACATTAAGTGGAGGAAGA---AGTGTGCAA---ATTGATAGATCTAAAGAGAGTACCAATA TTGTATTGGTACCTGAAACTGATTATGGCACACAGGATAGTGTCTCATTACTGGAACCTGACATCCCAGGG---AAGGCA AAA---ACAGCTCCAAATCAATGTGGGGATCTGTGTGCAGCAGTTAAAAATCCTAAAGAACTTATTCGTGGTTGT---TC TAAAGATATTAGAAATGACAGAGAGGGCTTTAAGGATCTATTGAGATGTGAAGTTAAC---CACACGCAGGAGACAAGCA TAGAAGTGGAAGAGAGTGAACTTGATACACAGGAATTACAGAATACATTCAAGGTGTCAAAGCGCCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATGTGCAACAGCCTATGCTCACTCCCAGTCTTTAAGGAAACAAAGTCCAAA AGTCACTCTTGAATGTGGACAAAAAGAAGAAAATCAGGGAAAGAAAGAATCTAAAATCAAGCATGTACAGGCAGTTCACA CAGCTGTAGGCTTTCCTGTGGTTTGTGAGAAAGACAGAAAGCCAGGAGAGTATGCCAAATACAGCATAAAAGGAACCTCT ATGCATTGCCNGTCCTCTCAGTTCAGA---GGCAACAAAACTGAACTCACTATTACAGATAAATATGGACTTTCCCCAAA CCCATATCATATACCATCCATTTCTCCCATCAAGTCATTTGTTAAAACTGTAAGTAAGAAAAAC---CTGTCAAAGGAAA AGTTTGAGGAACATTTAGTGTCACCTGAAAGAGCAATGGGAAATGAGAAC---ATTCAAAGTACAGTGAGCCCAATTAGC CTAAGTAACATTAGAGAAAGCGCTTTTAAAGAAAGCAGCTCAAGC---------------------AGTACTAATGAAGG GGGCTCTAGTATCAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCATGCAGAACTAGATA TAAACAAAGGATCAAAATTAACTGCTATGCTCAGATTAGGTCTCATGCAACCCGAAGTCTATAAGCCAAGCCTT---CCT GTAAGTAATTGTAAACATCCTGAAGTAAAAAGGCAAGGAGACAACGAAGGACTAGTTCAGGCTGTTAATGCAGACTTCCC TCCATGTCAAATTTCAGATAACCTAGAACAA---CCTATGGGAAGTAGTCCTGTTTCTCAGGTTTGTTCTGCGACACCGG ATGACTTGTTAACTGATGATGAAATAAAGGAGAATAGCGGCTTTGATGAAAGTGGCATTAANGAAAGATCTGCTGTTTTT AGCAAAGATGTTCAGAAAGAAGAATTCAGAGGGAGCCCTAGCCCCTTAGCCCAT---ACATATTTGACTCGGAGTTGCCA AAGAAGGGCCAGGAAATTAGAGTCCTCAGAAGAGGA------------- >TombBat TGTGGCACAAGTACTCATGCCAGCTCAGTACAGCATGAGAACAGCAGTTTACTACTCACTAAAGACAGAATGAACGTAGA AAAGCTTGACTTCTGTAATAAAAGCAAGCAGCCTGGCTTAGCAAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAAGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAGGGTAGTTGTGAATGCTGATACCCTGGATGGG--------- ------AGAAAAGAACCTCCATACTCTGACTGTCCTAATGAT---TCCCAAGAT---GTCCCTTGGATAACAGGGAATAG TAGCATACAGAAAGTTAGTGAGTGGTTTTCCAGGCGTGATGAAATATTAACTTCTGATGGATCACATGATGGGAGATCTG AATCAAATATGGAAGTAGCTGGTGCAGTAGAAGTTCCATAT------GAAGTAGATGGATATTCTGATTCTCCAGAGAAA ATAGGCTTAATGGCCAGTGATCCTCTTGGTGCTTTACTATGTGAAAGTGAAAGAATCCACTCCAAACCAGTAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCATATAACTGAAAATC TAATTATAGGAGCACCTACTATACAATCTCAGATAACACAA--------------------------------------- ---------------------AATTGTCCCCTCACAGATAAACTAAAGCATAAAAGAAGAACTACATCAGGCCTTCGTCC TGAGGATTTTATCAAGAAGGTAGATTTGGCAGCTGTTCAGAAGACTCCTGAAAAGATAATTGAGGGAACTGATCAAACAG AACAGAAT---------GGTTCTGTGATGAATATTACTGATAATGGTCATGAGGATGAAACAAAATGTGATTAT---GGT CAGAAAGAGAAAAATGCTAACCCAGCAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGCATAAGCAACATGGAACTAGAGTTAAATATCAACAGTTCAAAAGCACCTAAGAAGAATAGACTGAGGAGGA AGTCCTCTACCAGGCATATTTATGCACTTGAACTGGTAGTCAATAGAAATCCAAGCCCACCTAATCATATTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---GTGAAGAAAAGA---AATTCTGACCAAATACCAGTCAGGCACAGCAAAGA GCCTCAACTCGTGGAAGGTGAAGAACCTACGACTAGAGCCAAGAAG---AATAAGTCAAATGAACAAATAAATAAAAGAC TTGCCAGTGATACTTTTCCAGAACTAAATTTAAAAAACATACCTGGTTTTTTTACTAATGGTTCAAGTTCTAATAAACTT CAAGAGTTTGTTGATCCTAACCTTCGAAGAGAAGAAGTGGAAGAGAAC---CTAGGAACAATTCAAGTGTCTGATAGTAC CAAAGACCTCAAAGATCTGATATTAAGTGGAGGAAGA---AGTTTGCAA---ACTGATAGATCTATGGAGAGTACCAATA TTTTATTGGTACCTGAAACTGATTATGACACTCAGTATAGTATCTCATTACTGGAACCTGACACCCCAGGG---AAGGCA AAA---ACAGCACCAAGTCAACATGCGAGTCTGTGTGCAGCAATTGAAAACCCCAAGGAGTTTAACCATGGTTGT---TC TAAAGATACTAGAAGTGACACAGAGGGTGTTAAGGATCTACTGAGATGTGAAATTAAC---CNCACNNNNNNNNNNNNNN NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNGAATACATTCNNGNTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATTCAGAAAATCCAGAAAAGGAATGTGCAACAGTCTATGCCCACTCCAAGTCCTTAAGGAAACACAGTCCAAA AGTCACTCTTGGATGTGGTCAAAAAGAAGAAAATCAGGAAGAGAAAGAATCTAAAATCAAGCATGTACAGGCTGTTCACA CAGCTGCAGGCCTTCCTGCCATTTGTGAGAAAGACAAGAAGCCAGGAGAATATGACAGATACAATATAAAAGGAATCTCT AGGCTTTGTCAGTTATCTCAGTTCAGA---GGCAATGAAACTGAACTCACTATTGAAAATAAACACAGAATTTTACAAAA CCCATATCATATATCACCCATCTCTCCCATCAGGTCATCTGTTAAAACGATAAGTAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGAAACAGTCAGTGTCACCTGAAAAAGCAATGGGAAATGAGAACATCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAATGTTAGAGAAAGAGCTGTTAAAGAAGGCAGCTCAAGC---------------------AGTACTAACGAAGT AGGCTCTAGTATCGATGAAGCAGGTTCCAGT---------------------GGTAAAAACATTGGAGCAGAACTAGATA GAAACAGAGGATCAAAATTAAGTGCTGTTCTCAGATTAGGTCTCATGCAACCCGAAGTCTATAAGCCAAGCCTT---CCT ATAAGTAATTGTAAACACTCTGAAATAGAAAGGCAAGGAGAAAATGAAGTAGTAGTTCAGGCTGTTAATGCA-------- ----TGTCAAATTTCAGATAACTTAGAACAG---CCTATGGGAAGTAGTCCTGTTTCTCAGGCTTGTTCTGAGACACCAG ATGACCTATTAGATGATGACAAAATAAAGGAGAATAGCAGCTTTGCTGAAAGTGGCATTAAGGAAAGATCTGCTATTTTT AGCAAAAGTGCCCAGGAA---GAACTCAGCAGGAGCCCTAGCCCCTTAACCCAT---ACACATTTGGCTCAGGGTCAGCA GAGAAGGGCCGGGAAATTAGAGCC------------------------- >RoundEare ---------------------NGCTCATTANAGCNTGAGAACAGCAGTTTACTGCTCACTGAGGACCAGATGAGTGTGGG AAAGGCTGAATTCCGTCATGAAAGCAAGCAGCCCGGCTTAGCGAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAAGAAA CATGTGACGAT---AGGCAGGCTCCCAGCGCAGAGGAAAGGGCAGTTCTGAATGCTGATCCCCAGAATGGG--------- ------AGGGAAGAATCTCCATCCTCTGACCACCCTAGAGAT---TCCCAAGAT---GTTCCTTGGATAACACGGAATAG CAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAACACGGACTTCCAACGGCTCCCACGGTGGGAGGCCTG AGTCAGACACGGAAGGAGCCGACACGGTAGAAGCTCCGGAC------GAAGTGCGTGGATGCCCTGGCTCTCCAGAGAAC ACAGCCTCGCCGGCCGGCGAGCCTCATGGCGCTTTAATGTGCGGAAGTGAAAGCGTCCACTCCAAACCAGTGGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACGTAACTGACAGGC TAACTACGGGACTGTCCGCTCCAGACCCTCAGATAACACGA--------------------------------------- ---------------------GAGCGTCCCTTCACCAACAAACTAAAGCGTAAAAGGAGAACTACACCGGGCCTTCACCC AGAGGATTTTATCAAGACAGTGGATTTGACCGTTGTCCAGAAGACTCCTGAGAAGACCATTGAGGGAACTGACCAAACAG AACAGAAC---------GGTCGTGTGATGGATATTGCTAACAGTGGTCACGGGAATGAAGCAAAAGGTGATTAT---GTT CAGAATGAGAAGAGTTCTGACCCAACAGAA------TCACTGGGAGAAGAACCCGCTTTCAGAACTAAAGCTGGACCTAT AAGCAGCAGCATAAGCACCGTGGGACTAGAATTGAATGTCCACGGTTCAAAAGCGCCCAGGAAGACTAAGCGGAGGGAGA AGACTGCTGCCGAGCATACTTATGCACCTGGACTCGGGGTCAGCAAGAGCCCGAGCCCCCCTGCTCACGCCGGACTGCGG ACGGACGGTTGTTCTGGCGGCGAGGAG---GCAAAGATCGGG---AATTCTGGGCAGAGGCCAGCCAGGCGGAGCAGCAA GCTTCCGCTCGAGGAGGGTCAGGAGCCTGCAGCTGGGGCCAGCAAGGGTGACCGGTCAGATGCACCGATGAATAAGAGAC TTGCCAATGATGCTTTTCCGGAACTAAATTTAACAAGCGTATCTGCCGTTTTTACTAATGGTTCAGGTTCTACTAAACTT AAAGAGTGTGTCGATTCTAACCCTCAAGGAGAAGACACAGAAGAGAAC---CGAGGAACAGTTCAAGTGTCTAGTAGCAC CAAAGACCTCAAAGATCTGATATTCAGTGGGGGAAGA---AGTTTGCAA---ACTGACAGATCTGTGGAGAGTCCCAATA TTGNATTGGTACCTGAAACTGACTGTGACACTCAGGATAGCGTCTCCCTGCTGGCACCTGACACCCCAGGG---AAGGCA GAA---ACAGCACCAGGCCAACGTGTGGGTGGGTGCGCAGCTGCTGGAAGCCCGAAGGAACTTATCCGTGATTGT---TC GAAG------------GACACAGAGGGCGTTAAGGATCTCCCGAGATGTGAAGTTCAG---GAGACNNNNNNNNNNNNNN NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNTGCTCTG TGTTCAAATCCAGGNAATCCAGGAAAGGAATGTGCAACCATGTATTCCCACTCCAGGTCCACAAGGAAACGAAGTCCAAA AGTTACTCTTGAATGTGGACGAAAAGAA---CATCAGGGAGAGAAGGAATTTAATATGGAGCGTGCGCAGCCACCTTACA CAACAGCAGGCTTTCCTAAGGGTTGTGAGAAAGACAAAACGCCAGGAGAGTGTGCCACATATACTATGAAAGGAATCTCT AAGCCTTGTCAGTCATCTTCGTTCAGA---GGCAATGAAACTAAACTCACTATTGAAAATAAATATGGGATTTCACAAAA CCCCTATCACATACCACCCATTTCTCCCGTCAGGTCATCTGTTAAAACTACAAGTAAGAAGAAC---CTGTCAGAGGAGA AGTGTGAGGAACATTCAGTGTCACCTGCCAGAGCCGTGGGACGTGAGAACATCATTCAGAGTACAGCGGGCACTCCTAGC CAGAACAAAACCAGGGAAAGTGCCGCCAGAGAAGGCAGCTCGAGC---------------------GGCACTAACGAAGT AGGCTCCAGT------------------------------------------GGTGAAAACGGTCAAGCAGAGCCAGGCA CAAACAGAGCATCAAAATTAAGCGCTCTTCTCAGATCAGGGCTCATGCAACCTGAAGTCTGTAAGCCGAGTCTT---CCT CTGAGTAATTGTGAAGATCCTGAAATAAAAAGGCAA---GAAGATGGGGGAGTAGTGCAGGCTGTTAATGCAGATTTCTC TCCGTGTCAAATTTTAGATAACCTAGAACAA---CCTCTGGGAAGCAGTCCCGCTTCTCGGGTTTGTTCCGAGACCCCAG ACGACCTGTTAAATGATGACAAGGTAAAGGAGGATAACAGCTTTGCTGAAGGCGGCATGAAGGACAGATCTGCTGTTTTT AGCAAAAGCGTCCTGAAA---GAATTCAGAAGGAGCCCCAGTCCCTTAGCCCAC---ACANTCTTGGCTCGGGGTCACCC CAGAAGGGCCAGGAAACTCGAGTCCTCAGAAGAGGA------------- >FalseVamp TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAATTTATTACTGACTGAAGACATAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGTGTTAGCAAGGAGCCAGCAGAGCAGATGGGCTAACAGTAAAGAAA TATGTAATGAT---AGGCAGACTCCCAACACAGAGAAAAGGGTAGTTCTGAATGCTGATCCCTTGAATGAGAGAAAAGAA CTGAATAATCAGAAACCTCTATGCTCTGACAGTCCTAGAGAT---TCCCAAGAC---GTTCCTTGGATAACACGTAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAATACTAACTTCTCATGGTTCACATGATGGGACAGGTG GATCAAATACAGAAGAAGCTGGTGCAGCAGAAATTCTAAAT------GAAGTAGATGGATATTCTGGTTCTTCAGAGAAA ATAGCTTTAATGGCCAGTGATCCTCCTGGTGCTTCAATTTGTGAAAGTGAAAGAGTCTACTCCAAACCAGTAGAAAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTACAGGAGGAAGGCAAGCCTCCCTAACTTGGGCCACAGAGCTGAAAATC TAATTATAGGAGCATCTGCTGTAGAACCTCAGATAATACAA--------------------------------------- ---------------------GAGTGTCTCCTCATAAATAAACTAAAGCGTAAAAGGAGAACTACATCAGCCCTCCATCC TGAGGATTTTATCAAGAAAGTAGATGTGGCAGTTGTTGAAAAGATTCCTGGAGATAGAATCAAGGGAACTGACCAAATAG AGCAGCAT---------GGTCATGTGATGAATATTACTAATTATGGTCATGAGAATGAAACA------------------ ------------AATGCTCACTCAACAGAA------TCACTGGACAAAGAATCTGCTTTTAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGTAATATGGAACTAGAATTAAATATCCACAGTTTAAAAGCACCTAAGAAGAATAGGCTGAGGAGGA AGTCCTCTACAAGGCATATTCATGCACTTGAACTA---GTCAGTAGAAATCCAAGCCCACCTAATCATACTGAACTACAG ATTGATAGTTGTTCTAGCAATGAAGAG---GTGGAGAAGAAA---AACTCTGACCAAATGCCAGCCAGACACAACAAAAA TCTTCAACTTATAGAAGATAAAGAACCTGCAACTAGAGCTAAGAAGAGTAACAAGCCAGATGAACAAATAAATAAGAGAC TTACCAGTGATGCTTTTTCAGAACTAAATTTAACAAACACACCTGGTTTTGTTACCAACAGTTCAAATTCTGATAAACTT AAAGAGTTTGTCAATCCTAGCCTTCAAAGAGAAGAAATAGAAGAGCAT---CTGGGAACGATTAAAGTGTCTAATAGTAC CAAAGACCCCAAAGATCTGATACTAAGTGGAGGAAGA---GGTTTGCAA---ACCGATCGTTCTATGGAGAGTACCAGTA TTTCATTGGTACCTGATACTGATTATGGCACTCAGGCTAGTATGTCATTACTGGAACCTGACACCCCAGGG---AAGGTA AAA---ACAGCACCAAATCGACGTGCAGGTGTGTGTGCTGCAATTGAAAACCCCAAGGAAATTATCCATGGTTGT---TC TAAAGATACTAGAAATGACACAGGGGACTTTAAGGATCCACTAAGAGGTGAAGTTAAC---CACACNNNNNNNNNNNNNN NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN NTTTCAAATCCAGGAAATCCAGAAAAGGAATGTGCAACAGTCTGTGCCCACTCCAGTTCCTTAAGGAACGAAAGTCCAAA GGTCACTCTTGAATGTGGACAAAAAGAAGAAAATCACGGAAAGAAAGAGTCCAAAATCAAGCATGTGCAGGCAGTTCCAA CAACTGCAGGCTTTCCTGTGGTTTGTGAGAAAGAAAAAAATCCAGGAGATTATGCCAAATATACCAAAAAAGGAGTCTCT AGGCTTCGTCAGTCATCTCAGATCAGA---GGCAACAAAACCGAACTCACTGTTACAAATAAACATGGAATTTCTCAAAA CCCATATCATATACCACCCATTTCTTCCATCAGGTCATCTGTTAAAACTATATGTAAGGAAAAC---CTGTCAGAGGAAA ACCTTGAGGCATATTTGGTGTCACCTGAAAGAGCAATGGGAAATGAGAGCATTGTTCAAAGTACAGTGAGCACAGTTAGC CAAAGTAACATTAGAGAAAGCACTTTTAAAGAGGGCAGCTCCAGCAATATTTATGAAGCAGATTCCAGTGCTAATGAAGT AGGCTCTAGTATCAGTGAGGTAAGTTCCAGT---------------------GGTGAGAACATTCAAACAGAACTGGGTA GAAACCAAGGACCAAAATTAAATGCTGTGCTCAGATTAGGTCTCATGCAACCTCAAGTCTATGTGCAAAGCCTT---CCT GTAAGCAATTGTGAACATTCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTAGTTGAGGCTGTTAATGCAGAATTCTC TCCATGTCAAACTTCAGATAACCTGGAACAA---CCTATGGGAAGTAGTCATGCTTCTCAAATTTGTTCTGAGACACCGG ATGACCTGTTAAACGATTATGAAATAAAGGAAAATGTTAGCTTT---------------AAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAAAAAGAAGAATTGAGTAGAAGCCCTAGCCCCTTAATCCAT---ACATGTTTAGCTCAGGGTCACCG AAGA--------------------------------------------- >LeafNose TGTGGCACAAATACTCATGCCAGCTCTTTACATTATGAGCACAGCAGTTTATTACTCACTGAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAGCAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGGAAAATGTAGTTCTGAATACTGATCCCCTGAATGGGAGAAAAGAA CTGAATAAGCAGAAACCTCCATGCTCTGACAGTCCTAGGGAT---TCCCAAGTT---GTTGCATGGATAACACAGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAATATTAACTTCTCATAGCTCATGTTATGGGAGAGCTG AATCAAATACAGAAGTATCTGGTGCAGTAGAAGTTCCACTT------GAAGTAGATGGATTTTCTGGCTCTACAGAGAAA ATAACCTTAATGACCAGTGATCCTCATGATGCTGTAATATGTGAAAGTGGAAGAGTCCACTCCAAACCATTGGAAAGTAC T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTCAGCCACACAACTGAAAACA TAATTATAGGAGCATCTGCTGTAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGTGTCCCCTCACAAACAAACTAAGGCGTAAGAGGCGAACTACGTCAGGCCTTCATCC TGAGGATTTCATCAAGAAAGTAGATTTGACAGTTGTTCAAAAGACTCCTGAAAAGATAATTGAGAGAACTCACCAAACAG AACAGAAT---------GGTCATGTGATGAACATTACTGATAATGGTCATGGGAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGGATAATGCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTACAGCTGAACCTAT AAGCAGCAGTATAAGCCATATGGAACTAGAATTAAATATCCATAGTTCAAAAGCACCTAAGAAAAATAGGCTGAGGAGGA AGTCCTCTACCAGGCCTATTCATGCACTTGAACTAGTAGTCAGTGGAAATCCACGCCCACCTAGTCAGACTGAGCTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---GTGAAGAAAAAA---ATTTCTGACCAAGTGCCAGTCACACACAGCAAAAC GCTTGAACTCATAGAAGATCAAGAACCTGCAAATGGAGTCAAGAAGAGGAACAAGCCAAATGAACAAATAAATAAGAGAC TTACCAGTAATGCTTTTCCAGAACTAAATTTAACAAACATACCTGGTATATTG---AACCGTTCAAGTTCAAATAAACTT CAAGAGTTTGTCAATCCTAGCCTTCAAAGAGAAGAAATAGAAGAGAGC---CTAGGAACAATTCAAGTGTCTAATAGTAC CAAAGGCCTCAAAGATTTGATATCAAATGGGGGAAGA---GGCTTGCAA---ACTGGTCAATCTATGGAAAGTACCAGTA TTTTATTGGTACCTGATACTGATTATGGCAGTCAGGATAGTATGTCATTACTCGAACCTGACACCCCAGGG---AAGGCA AAG---ACTGCACCAAATCAACATGTGGGTGTATGTACAGCAGTTGAAAACCCCGAGGAACTTATCCATGGTGGT---TC TAATGATACTAGAAATGACACAGAGAGCTTTAAGGATTCATTGAGACATGAAGTTAAC---CACGGTCAGGAGACAAGCA TAGAAATGGAAGAGAGTGAACTTGATACACAGTATTTACAGGAAACATTCAAGGTTTCAAAGCGTCAATCATTTGCTCTG TTTTCAAATCCAAGAAATCAAAGAAAGGAATGTGCAACAATCCAGTCCAGGTCC------TTAAGGAAACAAAGTGCAAA AGTCACTCTTGAATGTGGACAAAAAGAAGAAAATCAGGGAAAGAAAGATTCTAAAATCAAGCTTGTACAGGCAGTTCATA CAACTGCAGGCTATCCTGTGGTTTGTGAGAAAGATGAAAATCCAGGAGATTATGCCAAATACAGCACAAAAGGAGTCTCT AGGCTTTGTCAATCATCTCTGTTCAGA---AGCAACGAAACTGAACTCACTATTGCAAATAAACATGGAATTTCTCAAAA CCCACATAATATACCACATATTTCTCCCATCAGGTCATCTGTTAAAACTGTATGTAAGAAAAAC---CTGTCAGAGGAAA ACTTGGAGGAATATCCAGTGTCACCTGAAAGAGCAATGGGAAATGAGAGCATCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAAGCAGCTCAAGCAATNTTTATGAAGCAGATTCCAGTACTAATGAAGT AGGCTCTAGTATCAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTCGGCA GAAACAGAGGACCGAAATTAAATGCTGTGCTCGGATTAGGTCTCGTGCAACCTGAAGTCTATAGGCAAAGCCTT---CCT GTAAGTAATTGTCAACATCCAGAAATAAAAAGGCAGGGAGAAAATGAAGGAATAGTTCAGGCTGTTAGTGCAGACTTCTC TCCATGTCAAATTTCAGATAACCTAGAACAA---CCTACGGGAAGTAGTCATGCTTCTCAGGTTTGTCCTGAGACACCGG ATGACCTGTTAAATGATAACGAAATAAAGGAAAATGACAGCTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAGAAAGGAGAATTCAGAAGGAGCCCTAGCCCCGTAGCTCAC---ACACGTTTGGCTCAGGGTCACCA AAGACGGGCCAGGAAATTAGAGTCCTCAGAAGAGGA------------- >Horse TGTGGCACAAATACTCATGCCAGCTCATTGCAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAAGAGCCAACAGAGCAGACGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGGCCCCCAACTCAGAGGAAAAGCTAGTTCTGAATGCTGATCCGCTGTATGGGAGAGAAGAA CTGAATAAGCAGAAACCTCCACGCTCTGACAGTCCTAGAGAC---TCCCAAGAT---GTTCCTTGGATAACACTGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGAAGTGAGGAAATGTTAACTTCTGATGACTCATGTGACGGAGGGCCTG AATCAAATACAGAAGTAGCTGGTGCAGTAGAAGTTCCAAAT------GAAGTACGTGGATATTCTGGTTCTTCAGAGAAA ATAGACTTAATGGCCGGTGATCCTTCTAGTGCTTTAATATGTGAAAGTGAAAGAGTCCGCTCCAAACCAGTAGAGAATAA T---ATTGAAGATAGAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCTTCCCTAACTTGAGCCACATAACTGAAGATC TAATTATAGGGGCATCTGCTATAGAACCTCAGATTACACAA--------------------------------------- ---------------------GAGCGTCCACTCACAAATAGAGTGAAGCATAAAAGGAGAACGTCATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCAGTTGTCCAAAAGACTCCTGAAAAGATAATTGAGGGAACTGACCAAATAG AGCAGAAC---------AGTCATGTGATGAATATTACTCCTAATGGTCATGGGAATGAAACAAAAGGCGATTAT---GTT CAGAATGAGAAAAACGCTTACCTAACAGAA------TCATTGGAGACAGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGCATAGGCAATCTGGAACTGGAATTAAATATCCACAGTTCAAAAGCACCTAAGAAGAATAGGCTGAGGAGGA AGTCTTGTACCAAGCAGATTCATGCACTTGAACTAGTAGTCAGTAAAAATCCAAGCCCACCTAATCATACTGAACTACAA ATCGATAGTTGTTCTAGTAGTGAAGAG---ATGAAGAAAAAA---AATTCTGACCAAATGCCGGTCAGACACAGCAAAAA GCTTCAACTCATGGAAGATAAAGAACCTGCCACTGGAGCCAAAAAGAGTAACAAGCCAAATGAACAAATAAATAAAAGAC TTGCCAGTGATGCTTTTCCAGAGCTAAAATTAACAAACATACCTGGTTTTTTTACTAACTGTTCAAGTTCTAATAAACTT CATGAGTTTGTCAATCCTAGCCTTCAAAGAGAAGAAATAGAACAGAAC---CTAGGAGCAAATCGACTGTCTAATAGTGC CAAAGACCCCAAAGATCTGATATTAAGTGGAGGAAAA---TGTTTGCAA---GCTGAAAGATCTGTAGAGAGTTCCGGTA TTTCATTGGTACCTGATACTGATTATGGGACTCAGGATAGTATCTCACTGCTGGAAGCTGACACCCTAGGG---AAGGCA AAA---ACAGCACCAAATCAATGTGCAAATCTATGCGCAGCAATTGAAAACCCCAAGGAACTTACCCATGATTGT---TC TAAGGATACTAGAAATGATACACAGGGCGTTAAGGATCCATTGAGACGTGAAGTTAAC---CACACTCAGGAGACAAGCA TAGAAATGGAAGAGAGTGAATTTGATACGCAGTATCTACAGAATATGTTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCAGGAAGTCCAGAAAAGAAGTGTGCAACAGTCAGTGCCCACTCCAGGTCCTTAAGGAAACAAAGTCCAAA AGTCACTCTTAAATGTGGACAAAAAGAAGGAAAGGAGGGAAAGAAAGAGTCTAAAATCAAGAATGTGCAGTCAGTTCACA CAACTGTGGGCTTTCCTGTGATTTGTCAGAAAGATAAGAAGCCAGGTGACTATGTCAAATGTAGCACAAAAGAAGCCTCT AGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAACGAAACNGAACTTATTACTGCAAATAAACATGGAATTTCACAAAA CCCATATTATATACCATCACTTTCTCCCATCAGGTCATCTGTTAAAACTGTATGTCAGAAAAAC---CTGCCAGAGGGAA AGCTTGAGGAACAGTCACTGTCACCTGAAAGAGCAATGGGAAATGAGAGCATTGTTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTAGAGAAAGCACATTGAAAGAAGTCAGCTCAAGC---------------------AGTATTAATGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAACACATTCAAGCAGAACTAGGCA GAAACAGAGGACCTAAATTAAATGCTATTCTCAGATTAGGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT ATAAGTAATTGTAAACATCTGGAAATAAAAAGGCAAGGAGAAAAGGAA---GTAGTTCAGGCTGTTAACGCAGACTTTTC TCCGTGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGTAGTTGTGCTTCTCAGGTTTGTTCTGAGACACCTG ATGACCTGTTAAATGATGACGAAATAAAGGAAAATATCAGCTTTGCTGAAAGTGGCGTTAAGGAAAGATCTGCCGTTTTT AGCAAAAGCGTCCAGAAAGGAAAGTTCAGAAGGAGTCCTAGCCCTATAGGCCGT---ACGTGTTTGGCTCAGGGTCACCA AAGACGGGCCAGGAAATTAGAGTCCTCAGAAGAGAACACGTCTAGTGAG >Rhino TGTGGCACGAATACTCATGCCAGCTCATTGCAGCATGAGAACAGCAGTGTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGTGAACAGCCTGGTTTAGCCAAGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAACTCAGAGAAAAAGCTAGTTCTGAACGCTGATCCYCTGTATGGGAGAAAAGAA CTGAATAAGCAGAAACCTCCATGTTCTGACAGTCCTAGAGAT---TCCCAAGAT---ATTCCTTGGATAACACGGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAAATATTAACTTCTGATGACTCACATGATGGGGGGCCTG AATCAAATACTGAAGTAGCTGGTGCAGTAGAAGTTCAAAAT------GAAGTAGATGGATATTCTGGTTCTTCAGAGAAA ATAGGCTTAATGGCCAGTGATCCTCCTGGTGCTTTAATATGTGAAAGTGAAAGAGTCCACTCCAAACCAGTAGAGAATAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCTTAACTTGAGCCACATAACTGAAGATC TAATTATAGGAGCATCTGCTATAGAATCTCAGATTACACAG--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAACTGAAGCATAAAAGGAGAACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCAGTTGTCCAAAAGACTCCTGAAAAGATAATTGAGGGAACTGACCAAATAG AGCAGAAC---------GGTCGTGTGATGAGTATTGCTAATAATGGTCATGAGAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGGAGAAAGAATCTGCTTTCAGAACGAAAGCTGAACCTAT AAGCAGCAGCATAAGCAATCTGGAACTGGAATTAAATATCCACAGTTCAAAAGCACCTAAGAAGAATAGGCTGAGGAGGA AATCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATCATACTGAACTACAG GTTGATAGTTGTTCTAGCAGTGAGGAG---ATGAAGAAGAAA---AGTCCCAGCCAGGTGCCAGTCAGACATAGCAGAAA GCTTCAACTCACMGAAGATAAAGAACCCGCAGCTGGAGCCAAGAAAAGTAACAAGCCAAATGAACAGATAAATAAAAGAC TCGCCAGTGATGCTTTTCCAGAACTAAAATTAACAAACGTACCTGGTTTTTTTGCTAACTGTTCAAGTTCTAATAAACTT CAAGAGTTTGTCAATCCTAGCCTTCAAAGAGAAGACATAGAACGGAAC---CTAGGAGCAATTCAAGTGTCTAATAGTAC CAAAGACCCCGAAGATCTGATATTAAGTGGAGGAAGA---GGTTTGCAR---GCTGAAAGATCTGTAGAGAGCACCAGTA TTTCATTGGTACCTGATACTGATTATGGCACTCAGGGTAGTATCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAA---ACAGCACCAGATCAACGTGCAAGTCTATGTGCAGCAATTGAAAACCCCAAGGAACTTATCCATGATTGT---TC TAAAGATACTAGAAATGACACAGAGGGCCTTAAGGATCCATTGAGATGTGAAGTTAAC---CACACTCAGGAGACAAGCA TAGAAATGGAAGAGAGTGAACTTGATACACAGTATCTACAGAATACGTTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATTCAGGAAATCCAGAAAAGGATTGTGCAGCAGTCTCTGCCCACACCAGGTCTTTAAGGAAACCAAGTCCGAA AGTCACTCTTGAATGTGGACAAAAAGAAGAAAATCAGGGAAAGAAAGAGTCTAAAGTCAAGCATGTGCAGTCAGTTCATA CAACTGTGGACTTTCCTGTGGTTTGTCAGAAAGATAAGAAACCAGGTGATCATGTCAAATATAGCATAAAAGAACTCTCT AGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAATGAAACTGAACTCATTACTGCAAATAAACGTGAACTTTCACAAAA CCTGTGTTATATACCATCACTTTCTCCCATCAGGTCATCTGTTAAAACTATATGTAAGAAAAAT---GTGTCAGAGGAAA AGCTTGAGGAACATTCAGTGTCCCCTGAAAGAGCACTGGGAAACAAGAGCGTCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAGTCGGCTCAAGCAGTATTAATGAAGTAGATTCCAGTACTAATGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTGAAGCAGAGCTAGGCA GAAACAGAGGACCTAAATTAAATGCTATTCTCAGATTAGGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCA ATAAGTAATTGTAAACATCCGGAAATAAAAAGGCGAGGAGAAAATGAAGGAGTAGTTCAGCCTGTTAATGCAGATTTCTC TCCGTGTCCAATTTCAGATAACCTAGAACAA---CCTGTGGGAATTAGTTGTGCTTCTCAGGTTTGTTCTGAGACACCTG ATGACCTATTAAATGACAACGAAATAAAGGAAAATATCAGCTTTACTGAAAGTGGCATTAAGGAAAGATCTGCTATTTTT AGCAAAAGCGTCCAGAAAGGAGAATTCAGAAGGAGCCCTAGCCCTTTAGCCCGT---ACATGTTTGGCT---------CA AAGAGGGGCCAGGAAATTAGAGTCCTCAGAAGAGAACATGTCTAGTGAG >Pangolin TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATAAATGTAGA AAAGACTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCTGCAAAGCAGATGGGCTGAATGTAAAGAAA CATGTAACGAT---AGGCAGGCTCTCAGCACAGGGAAAAAGGTAGTTCTGAATGCTGATCCCCTGTGTGGGAGAAAAGAA CTGAATAAGCAGAAACCTTCATGCTCTGACAGTCCTAGAGTT---TCTCAAGAC---GTTCCTTGGATAACACTAAATAG TAGCATACRGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAAATGTTAACTTCTGATGATCCATGTGATGGGAGGTCTG AATCAAATACTGAAGTAGCTGGTGCAGTAGAAGTTCCAAAT------GAAGTAGATGAAAATTGTGGTTCTTCAGAGAAA AAAGACTTAATGGCCAGTGATCCTCATGATGCTTTAATACGTGAAAGTAAAAGAGGCCACTCCAAACCAGTAGAGAGTAA T---ACTGAAGATAAAATATTTGGGAAAACCTATCGGAAGAAGGCAAGTCTCTCTCACTTGAGCCACGTAACTGAAAATC TAATTATTGGAGCATTTGCAGTAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGCGTCCCCTCAGAAATAGAGTAAAGCGTAAAAGGAGAACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGAAGTTGTTCAAAAGACTCCTGAAAAGATAATTGAGGGAACCGACCAAATAG AGCAGAAT---------GCTCTTCTGATGAATAGAACAAATAACGGTCATAAGAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGAAAAAAGAACCTGCTTTCAGAACTAGAGCTGAACCTAT AACCAGCAATATAAGCAATATGGAGCTAGAATTAAATATCCATAGTTCAAAAGTACTTAAGAAGAATAGGCTGAGGAGGA AGTCTTCTACCAGGCACATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGCCCCCCTAAACATACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAGTTGATGAAA---------------AACCAAATACCAGTCAAACATNCCAAAAA GCTTCAACTCAAGGAAGATAGAGAACCTGCATCTAGAGCCAAGAAGAGTAACAAGCCAAATGAACAAATAAATAAAAGAT TTACCAGTGACACTTTTCCAGAACTAAATTTAACAAACATACCTGGTTTTTTTAGTAAATGTTCAAGTTCTAATAAACTT CAAGAGTTTGTTGACCCTAGCCTTCAAAGAGAAGAAATAGAAAATAAT---CTAGAAACAATTCTA------------AC CAAAGACCCCAAAGATGTGATATTACATAGAGGA------GGTTTGCAA---ACTGAAAGATGTGTAGAGAGTAACAGTA TTTCATTGGTACCTGATACTGATTACGGCACTCAGGATAGTATCTCATTACTGGAAGCTGACACACTAGGG---AAGACA AAA---ACAGCACCAAATCAATGTGCAAGTCTGTGTGCAGCAATTGAAAACCCCAAGGAACTTACCCATTGTTGT---GC TAAAGATATTAGAAATGACACAGAGAGCTTTAAGGATCTATTGAGACATGAAGATAAC---CACACTCAGGAGACAAGCA TAGGAACGGAAGAGAATAAACTTGATAAGCAGTACTTACAGAATACTTTCAGGGTTTCAAAGCATCAGTCATTTGCTCAG TTTTCAAATATGGGAAATCCAGAAAAAGAATGTGCAGCAGTCAGTGCCCACTCTGGGTCCTTAAGGAAACAAAGTCCAAA AGTCACTCTTGCATATGGACAAAAAGAAGAAAATGAGGGAAAGAAAGAGTCTGAAATCAAGCATGTTCAAACAATTCATA CAACTGCAGGCCCTCTTGTGGTTAGTCAGAAAGATGAGAAGCCAGGTGATTATGTAAAATTTGGCATAAAAGGAGTCTCT ACGCATTGTCAGTCATCTCAGTTCAGA---GGCAATAAAACTGAACTTATTTTTGCAAATAAACCTGGAATTTCACAATA CCCATATCATATATCATCAATTTCTCCCATCAGGTCATCTGTTAAAATAATATGTAAGGAAAAC---CTGTCAGAGGAAA AGTCTGTGGAACATTCGATGTCATTGGAAAAAGCAGTGGGAAACAAGAGCATCATTCAAAGTACTGTGAGCACAATTAGC CAAAATAACATTAAAGGAAACAATTTTAAAGATGGCAGCTCCAGCAGCATTAATGAAATAGGTTCCAGTACTAATGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAAAATTCAAGCAGAACTAGGTA GAAACAGAGGACCTAAATTAAGTGCTGTGCTCAGATTAGGTCTTATGCAACCTGAGGCCTATAAGCAAAATCTT---CTT ATAGGTAATCGTAAACACCCTGAAATTAAAAGACAAGGAGAAAAAGAAGGAGTAGTTCAGGTTGTTAATGCTGATTTCTC TCCATGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGTAGTCATGCTTCTCAGGTTTGTTCTGAGACACCTG AGGACATATTAAATGGTGATGAAATAAAGGAAAATATCGGCTTTGCTGAAAGTGGTATTAAGGAAAGATCTGCTGTTTCT AGCAAAAGTGTCCAGAAAGGAGAATTCAGAAAGAGCCCAAGCCCTGTAGTCCAT---ATGAGTTTGTCTCAGAGTCACCA AAGAAGAGCTGGGATAATAGAGTCTTCAGAAGAGAACATGTTTAGTGAG >Cat TGTGCCACAAATACTCGTGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGGCAGAATGAATGTAGA AAAGGCTGAATTCTGTAATGAAAGCAAACAGCCTGGCGTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTGTGTAGGAGAAAA--- CTGAGTAAGCAGAAATCTCCATGCTCTGACAGTCCTAGAGAT---TCCCAAGAT---GTACCTTGGATAACACTGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAAATGTTAACTTCTGATGACTCACATGATGAGGGATCTG AATCGAATCCTGGAGTAGCTGGTGCA---GAAGTTCCAAAT------GTAGTGGATGGATATTCTGGTTCTTCTGAGAAA ATAGACTTAATGGCCAGTGATCCTCATGATGCTTTGATATGTGAAAGTGAAAGAGTTCACACCAAACCAGTAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTTCCTAACTTGAGCCACACAAGTGAAGATC TCATTATAGGAGCATGTGCTATAGAACCTCAGATAACGCAA--------------------------------------- ---------------------GCCTATCCCCTCACAAATAAAACAAAGCGTAAAAGGAGATCTACCGCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCCATTGTTCAAAAGACTCCTGAAAAGCTAATTGAGGGAACCGATCGAATAG GGCAAAAT---------GGCCATGTGATGAATAGAACAAATAATGGTCCTGAGAATGGGACAAAAGGTGATTAT---GTT CAGAAAGAGAAAAATGCTAAGCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTTAGAACCAAAGCTGAACCAAT AAGCAGCAATATAAGTAATATGGAATCAGAATTAAATAGCCACAGTTCAAGGGCACCTAAGGAGAACAGGCTGAGGAGGA AGTCCTCTACCAGGCACATGCGTGCGCTGGAATTAGTAGTCAATAGAAATCCAAGCCCACCTGATCATACCGAACTACAG ATTGATAGTTGTTCTAGCAGTGAAGAGATGGTGAAAAGAAAA---AGTTCGGAACAAATGCTAGTCAGACACAGCAAAAC ACTTCAACTCGTGGAAAATAAAGAACCTGCACCTGGAGCCAAGAAGCGTAACAAGCCAAGTAAACAAATAAATAAAAGAC TTGCCAGTAACACTTTTCCAGAGCTAAATTTAACAAACATACCTGGGGTTTTTACTAACTGTTCAAGTTCTAATACACTT CAAGAGTTTGTCAACCCTGGCATTCAAAGAGAAGAACTAGAAGAGAGC---CGAGGAACGATTCACGTGTCTGATAGGAC CAGAGATCCCAAAGCGCTGGTATCGAGTGGAGGAAGA---AGTTTGCAA---ACTGAAAGATCTGTAGAGAGTACCAGTA TTTCATTGGTACCTGATGCCGATTATGGCACTCAGGATAGTATCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAA---ACAGCACCAAATCAACGTGTGAGTCTGTGTGCAGCAATTGAAAACCCCAAGGAAGCTATCCGTGGTTGT---TC CAAAGATACTAGAAATGGCACAGAGAGTTTTACAGATCCTCTGAGACGTGAAGATANC---CATACTCAGGAGACAAGTA TAGAAATGGAAGAGAATGAACTTGATACGCAGTGGTTATACAATACGTTCAAGGGTTCAAAACGTCAGTCATTTGCTCTG TTTTCAAATCCAGGAAACCCAGAAAAGGAATGTGCTACAGCCTGGGCCCGTTCCACGTCCTTAAGGAAACAAAGTCCAAA AGTCGCTCTTGAATATGAACAAAAAGAAGAAAATCAGGGAAAGAATCAGTCTGAAATCAAGCATGTGCAGGCAGTCCGTG CAACTGCAGGCTTTTCTGCAGTCAGTCAGAAAGTGGAGAAGCCAGGTGATTATGCCAAATGTAGCATAAAAGGAGTCCCT GGGCTCTGTCAGTCATCTCAGTTCAGA---GGCAATGAAACTGAACTCTTTATTGCAAATAACCATGACATTTCAAAAAA CCCTTATCATATACCACCACTTTCTCCCATCAGATCATCTGTTAATGCTGTATGTAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGCAGCGTTCAATGTCACCTGAAAGAGCAGTGGGAAATGAGAGCGTCATTCAAAGTACAGTGAACACAATTAGC CAAAATAACATTAGAGAAAACACTTTTAAAGAAGTTAGCTCAAGCAGTGTTAATGAAGTAGGTTCCAGTGCTAATGAAGT AGGCTCTAGCATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAACAGAACTGGGTA GAAACCGAGGACCTAAGTTAAGTGCTATGCTCAGGTTAGGTCTTATGCAACCTGAAGTCTATGAGCAAAGTCTT---CCT ATAAGTAATTGTAAATGTCCAGAAATGAAAAGGCAAGGAGAAAATGAAGGAGTAGTTCAGTCTGTTAATGCAGATTTTTC TCCATGTCTAATTTCCGATAATGTAGAACAA---CCTATGGAAAGTAGCCGTGCTTCTCAGGTTTGTTCTGAGACACCCA ATGACCTATTAAATGGTGATGAAATAAAGGGAAAAATCAGCTTTGCTGAAAGT---GCTAAGGAAAGATCTGCTGTTTTT GGCAAGAGTGTCCAGAAAGGAGAATTCAGAAGGAGCCCTAGCCCTTTAGACCAT---ACACATCTGGCTCAGGGTCACCA AACAGAGACCAGGAAGTTAGAGTCCTCAGAAGAGAACGTGTCTAGTGAG >Dog TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAACACAGAATGAATGTAGA AAAGGCTGAAATCTGTAATAACAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGATTCCCAGCACAGAGAAAAAGGTAGTTGTGAATGCTGATCTCCTGTGTGGGAGAAAAGAA CTGAATAAACAGAAACCTCCACACTCTGATAGTCCTAGAGAT---TCCCAAGAT---GTTCCTTGGATAACACTGAATAG TAGCATACGGAAAGTTAATGAGTGGTTTTCCAGAAGTGACGAAATATTAACTTCTGATGATTCACATGACAGAGGATCTG AATTGAATACTGAAGTAGGTGGTGCAGTAGAAGTTCCAAAT------GAAGTGGGTGAATATTCTGGTTCTTCTGAGAAA ATAGACTTAATGGCCAGTGATCCTCAGGATGCTTTCATATGTGAAAGTGAAAGAGTCCACACCAAGCCAGTAGGAGGTAA T---ATCGAAGATAAAATATTTGGAAAAACCTATCGGAGGAAGGCAAGCCTCCCTAAGGTGAGCCACACAACTGAAGTTC TAACTATAGGAGCGTGTGCTATAGAACCTCAGACAATGCAA--------------------------------------- ---------------------ACCCATCCCTTCATGAATAAAGCAGAGCATAAAAGGAGAACTACATCTAGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGAGTTAGGCATTGTTCCAAAGACTCCTGAAAAGCTAATTGAGGGAATCAACCAAATCA AGCGAGAT---------GGTCATGTGATAAATATTACAAATAATGGTCCTGAGAATGAAACAGAAGGTGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTTAGAACCAAAACTGAACCAAT GAGCAGCAGGATAAGCAATATGGAACTGGAATTAAATAGCTCCAGTTCAAAAGCACCTAAGAAGAACAGGCTGAGGAGGA AGTCCTCTGCCAGGCACACTTGTGCCCTTGAATTCGTAGTCAATAGAAATCTAAACCCACCTGATCATAGTGAACTACAG ATTGAAAGTTGTTCTAGCAGTGAAGAG---ATGAAGAAACAG---CATCTGGACCAAGTACCAGTCAGACACAACAAAAC ACTTCAGCTCATGCAAGATAAAGAACCTGCAGGTAGAGCTAAGAAAAGTAGTAAGCCAGGAGAACAAATAAATAAGAGAC TCGCCAGCCATGCTTTTCCAGAGCTAACTTTAACAAATGTATCTGGTTTTTTTGCTAACTATTCAAGTTCTAGTAAGCCT CAAGAGTGCATCAACCCTGGCCTTCGAAGAGAAGAAATAGAAGAGAGC---CGAAGAATGACTCAAGTGTCTGATAGTAC CAGAGATCCCAAAGAGCTGGTATTGAGTGGAGGAAGA---GGTTTGCAA---ACTGAGAGATCTGTAGAGAGTACCAGTA TTTCATTGGTACTTGATACTGATTATGGTACACAGGACAGTATCTCATTACTGGAAGCTGACACCCTGAGG---AAGGCA AAA---ACAGTATCAAATCAACAGGCGAATCTGTGTGCAACAATTGAGAACCCCAAGGAACCTATCCATGGTTGT---TC TAAGGACACTAGAAATGACACAGAGGGTTTTGTAGTTCCATTGACGTGCAAAGATAAC---CACACTCAAGAGACAAGCA TAGAAATGGAAGAGAGTGAACTTGACACGCAGTGCTTACGCAATATGTTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCATATCCAAGAGATCCGGAAGAGGACTGTGTAACAGTCTGTCCCCGCTCTGGGGCCTTTGGAAAACAAGGTCCAAA AGTCACTCTAGAATGTGGACAGAAAGAAGAAAGTCAGGGAAAGAAAGAGTCTGAAATCAGACATGTGCAGGCAGTTCATA CAAATGCAGGCTTTTCTGCAGTTAGTCAGAAAGCTAAGAAGCCAGGCGATTTTGCCAAATGTAGCATAAAGGGAGTCTCT CGGCTTTGTCTGTCATCTCAGTTCAAA---GGCAAGGAAACTGAACTCCTTATTGCAAATTACCATGGAATTTCCCAAAA CCCTTATCATATACCACCACTTTCTCCCATCAGATCATGTGTTAAAACTCTATGTCAGGAAAAC---CTGTCAGAGGAAA AGTTTGAGCAACATTCAATGTCACCCGAAAGAGCAGTGGGAAATGAGAGAGTCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTAGAGAATGTGCTTCTAAAGAAGTCGGCTCAAGCAGTGTTAATGAAGTAGTTTCCAGTACTAATGAAGT AGGCTCTAGTGTTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAGGTA GAAACCGAGGACCTAAATTAAATGCTATGCTCAGATTAGGTCTTATGCAACCTGAAGTCTGTAAGCAAAGCCTT---TCT TTAAGTAATTGTAAACATCCAGAAATGAAATGGCAAGGACAAAGTGAAGGAGCAGTTCTGTCTGTTAGTGCAGATTTCTC TCCATGTCTGATTTCAGATAACCCAGAACAA---CCTATGGGAAGTAGTCGGTCTTCTCAGGTTTGTTCTGAGACACCTG ATGACCTATTAAATGGTGACAAAATAAAGGGAAAAGTCAGCTTTGCTGAAAGTGACATTAAGGAAAAATCTGCTGTGTTT AGCAAAAGTGTCCAGAGTGGAGAGTTCAGCAGAAGCCCTAGCCCTTCAGACCAT---ACACGTTTGGCCCAGGGTTACCA GAGAGGGACCAAGAAATTAGAGTCCTCAGAAGAGAACATGTCTAGTGAG >Llama TGTGGCACAGATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAGGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGTCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGACA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTGGCTCTGAATGCCGATCGCTTATATGGGAGCGAAGAA CTGAATAAGCAGAAACCTGCATGCTCTGACAGTCCTAGAGAT---TCCCAAGAT---GTTCCTTGGATAACACTGAATAG TAGCATACAGAAAGTGAATGAGTGGTTTTCCAGAAGCGACGAAATGTTACCTTCTGATGACTCACATGAAGTGGGGCCTG AATCAAATACTGAAGTAGCTGGTGCAGTGGAAGTTCCAAAT------GAAGTAGATGGCTATTCAGGCTCTTCAGAGAAA ATAGACTTAATGGCCAGTGATCCTCATGGTGCTTTAAAATGTGAAAATGAAAAAGTCCACGCCAAACCAGTAGGGAGTAA C---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGACCCACAGAGCTGAAGATC TAATTCTAGGAGCATCTGTTCTAGAGCCTCAGATAACACAA--------------------------------------- ---------------------GAGCGCCCCTTCACAAATAAACTAAAGCGTAAAAGGAGAACTCTACCAGGTCTTCATCC TGAGGATTTTATCAAGAAAGTCGATTTGGCCGTTGTTCAAAAGTCTCCTGAAAAGATAATTGAGCGAACAGACCAAACAG AGCAGAAT---------GGTCATGGGATGAATATTACTAGTAATGGTCATGAGAATGAAACAAACGACGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAAAACTAAGGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTGAATACCCATAGTTCAAAAGCACCTAAG---AATAGGCCGAGGAGGA AGTCCTCTACCAGGCAGATTCATGCACTTGAACTAATAGTCAGTAGAAACCCAAGCCCACCTAATCACACTGAACTACAG ATTGAGAGTTGTTCCAGCAGTGAAGAG---ATGAAGGAAAAA---CATTCTGACCAAATGCCAGTCAGGCACCGCAAAAA GCTTCAGCTCACGGGAGATAAAGAACCTACGACTGGAACCAAGAAGAGTAACAAGCCACATCAACAAATAAATAAAAGAC TTGTCGGTAACACTTTTCCAGAACTAAATTTAACAAACACACCTGGTTTTTTTACTAAGTGTTCAAGTTCTAATAAACTT CAAGAGTTTGCCAATCCTAGCCTTCAAAGAGAA---------GAGAAC---CCAGGAGCAATTCAAGTATCGAACAGTAC CAAAGACCCCAAAGTTCTGATATTAAGTGGAGGGAGA---GGTTTACAA---ATTGAAAGATCTGTAGAGAATACCAGTA TTTCCTTGGTACCTGAAACTGATTATGACACTCAGGACAGTGTCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAA---ACAGCACCAAATCAATGTGTGAGTCTCTGTGCAGCAACTGAAAACCCCAAGGAACTTATCCATAGTTGT---TC TAAAGATACTAGAAATGGCACAGCAGGCTTTAAGGATCCATTGGGATGTGATGTTAAT---CACACTCAGGAGGCAAGCA TGGAAGTGGAAGAGAGTGAACTTGATACTCAGTATTTGCAGAATACGTTCAAGGTTTCAAAGCGTCAGACATTTGCTCTG TTTTCAAATCCAGGAGATTCAGAAAAGGAATGTGCAACGATCTATGCCCACTCTGGGCCCTTAAGGGAACAAAGTCCAAA AATCACTCTTGAATGTGGACAAAAAGAAGAAAATCAGAGAAAGAGCAAGTCTGAAATCAAGCATATGCAGGCAGTTCATA CAACTTTGAACTTTCCTGTGGTTGGTCAAAAAGATAAG---CCGAGTGATTATGCCAAATATAGCCCAAAAGGAATATCT AGGCTTTGTTGGCCATCACAGCTCAGA---GGCAATGAATCTGAGTTCATTATTGCAAATAAACATGGGATTTTACAAAA CCCATATCTTATAACATCACTTTCTCCTAACAGGTCATCTGTTAAAACTATGTGTAAGAAAAAC---CCGTCCGAGGAAA AGCTTGAGAAATGTGTCATATCACCTAAAAGAGTGATGGGAAACGAGAGCACCATTCAAAGTATAGTGAGTGCAATGAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAGTCAGCTCAAGCAGTGTCAATGAAGTAGGTTCCAGTACTAATGAAGT AGGTTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGCGAAAACATTCAAGCAGAACTAGGTA GAAACATGGGACCTAAATTAAATGCTATGCTCAGATTAGGTCTTTTGCAACCTGAAGTCTGTAAGCAAAGTCTT---CCT GTAAGTAATTGTAAACAACCTGAAATAAAAAGGCAAGGAGAAAATGAAGGCATCTTTCAGGCTGTTAATACCGACTTCTC TCCATGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGCAGTCATGCTTCTCAGGTTTGTTCTGAGACACCTG ATGACCTGTTATATGATGACGAAATAAAGGAAAATACCAGCTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAAAAA---GAATTCAGAAGGAGCCCTAGCCCTTTAGTGCAT---ACGCATTTGGCTCAGGGTCACCA AAGAGGGGCTAGGAAATTAGAGTCCTCAGAAGAGAATGTGTCTAGTGAG >Pig TGTGGCACAGATACTCATGCCAGCTCGTTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTTTGTAATAAAAGCAAGCAGCCTGTCTTAGCAAAGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGGCA CATGTAATGAT---AGGCAGACTCCTAACACAGAGAAAAAGGTAGTTCTGAATACTGATCTCCTGTATGGGAGAAACGAA CTGAATAAGCAGAAACCTGCGTGCTCTGACAGTCCTAGAGAT---TCCCAAGAT---GTTCCTTGGATAACATTGAATAG TAGCATACAGAAAGTTAATGAGTGGTTTTCTAGAAGCGATGAAATGTTAACTTCTGACGACTCACAGGACAGGAGGTCTG AATCAAATACTGGGGTAGCTGGTGCAGCAGAGGTTCCAAAT------GAAGCAGATGGACATTTGGGTTCTTCAGAGAAA ATAGACTTAATGGCCAGTGACCCTCATGGTGCTTTAATACGTGAACGTGAAAGAGGGCACTCCAAACCAGCAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACGTAATTGAAGATC TAATTTTAGGAGCATCTGCTGTAGAGCCTCAAATAACACAA--------------------------------------- ---------------------GAGCGCCCCCTCACAAATAAACTAAAGCGGAAAAGGAGAGGTACATCAGGCCTTGATCC CGAGGATTTTATCAAGAGAGCCGATTCGGCAGTTGTTCCAAAAACTCCTGAAAAGACAATTGAGGGAACTGATGAGACAG AGCAGAAT---------GGTCAGGGGATGAATATTACCAGTAAAGGTCATGAGAATGAAACAAAAAGTGGTAAT---GTT CAGAAAGAGAAAAATGTTAACCCAACTGCA------TCGTTGGGAAAAGAACCTGCTTTCAGAGCTAGGGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTGAATATCCATGGTTCGAAAGCACCAAAG---AACAGGCTGAGGAGGA AGTCCTCTACCAGGCAGATCCATGCACTTGAACTAGTAGTCAATAGAAACCCAAGCCCACCTAGTCATACTGAGTTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATGAAGAAGAGA---AATTCCGACCAAGTGCCAGTCAGGCACAGCAAAAA GCTTCACCTCAGGGGAGATAAAGAACCTACAACTGGAGCCAAGAAGAATAATAGGCCACATGAACAAGTAAATAAAAGAC CTATCAGTGATGCTTTTCCAGAACTAAATTTAACAAATGTACCTGTTTCTATTACTAACTGTTCAAATTCTAATAAGCTT CAAGAATCTGTCAGTCCTAACCTTCAAAGAGAA---------GAGAAC---CTAGGGACAATTCAAGTGTCGAATAGTAA CAAAGACCCCAAAGATGTGATGTTAAGTGGAGGAAAA---GGTTTTCAA---ATTGAAAGATCTGTAGAGAATACCAGTA TTTCCCTGGTACCTGATACTGATTATGGCACTCAGGACAGTATCTCATTACTGGAAACTGACACCCTAGGA---AAGGCA AAA---ACAACACCCAATCAACATGTGAGGCTGTGTGCAGCAACTGAAAACCCCAAAGAACTTAGCCTTGGTTGT---TC TAAAGGTGTTAGAAACGACACAGGGGACTTTAAGGATCCCCTGGCTCATGATGTTAAC---CACACTCAGGAAGCAAGCA TAGAAGTGGAAGAGAATGAACTTGATACACAGTATTTGCAGAGTATGTTCAAGGTTTCAAAGCGTCAGACATTTGCTCTG TTTTCAAATCCAGCAAATCCAGAAAAGGAATGTACAACCGTCCATGCCCACTCCAAGTCCTTAAGAGAACAAAGTCCAAA AGTCACTCATGAAGGTGGACAAAAAGATGAAAATCAGGGAAAGAGTGAGTCTAAAGTCAAGCATGGGCAGTCAGTTCATA CAACTGTGGACTTTCTAGTGGTTGGTCAAAAGGATAAGAAGCCGAGTGATTTTGCCAAATGTGGTGCAAAAGGAGTAACT GGGCTTTATCAGACATCACAGTTCAGA---GGCCACAAAACTGAGTTCATTAATGCAAATAAACCTGGGATTTCACAAAA CCCATATGTCATACCATCCCTTTCTCCCATCAGGTCGTCTGTTAAAACTATATGTAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACCTAAAATGTCACCCGAAAGAACAATGGGAAACGAGAGCATCATTCCAAATACAGAGAGCACAGTTAGC CAAAATAACATTCAAGAGAGAACTTTTAAAGAAGGCAGCTCAGGCAGTCCTAATGAAGTCGGGTCCAGTACCAACGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGC---------------------GGTGAAAACGTTCGAGCAGAACCAGGTA GAAACAGAGGACCTAAATTAAGTGCAATGCTCAGATTAGGTCTCATGCAACCCGAAGTTTATAAGCAAAGTCTT---CCT GTAAGTAATTGTAACCACACAGAAATAAAAAGGCAAGGAGAAAATGAAGGCATATTTCAGGCTGTTAATGCAGATTTCTC CCCATATCTAATTTCAGATAACCCCGAACAA---CCTATGGGAAGTAGTCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGACCTGTTAAATGATGACAAAATAAAGGAAAATCTCAACTTTGCTGAAAGTGACGTTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGCCCAAGAAGGGGAATTTAAAAGGAGCCCTAGCCCTTTAGCCCAC---AGACGTTTGGCTCAGGGTCACCA AAGATGGGCTAGGAAATTAGAGTCTTCAGAAGAGAGTGGGTCTAGTGAG >Cow TGTGGCACAGATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTGCTCACTGAAAACAGACTGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGTCTTAGTAAAGAGCCAGCAGAGCAGATGGGCTGAAAGTAAGGGCA CATGTAAGGAT---AGGCAGATTCCCAGCACTGAGAAAAAGATAGTTCTGAATACTGATCCCCTGTACAGAAGAAAAGAA CTGCGTAAGCAGAAACCTGCATGCCCTGACAGTCCTGGAGAT---TCCCAAGAT---GTTCCTTGGGTAACCCTGAATAA TAGCATACAGAAAGTTAATGACTGGTTTTCCAGAAGTGATGAAATATTAACTTCTGATGACTCGTGCGATGGGGGGTYTG AATCAAATAATGAAGTAGCTGGTGCAGTGGAAATTCCAAAT------AAAGTAGATGGATATTCAGGTTCTTCAGAGAAA ATCAACTTAATGGCCAGTGATCCTCATGGTACTTTAATACAC------GAAAGAGTCCACTCCAAACCCGTAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGTCAAGTCTCCCTAACTTCAGCCACATAGCTGAAGATC TAATTCTAGGCGCATTTACTGTAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGCAGCCCCTCACAAATAAACTAAAATGTAAAAGGAGAGGTACATCAGGCCTTCAGCC TGAGGATTTTATCAAGAAAGTCGATTTGACAATTGTTCCAAAGACTCCTGAAAAGATGACGGAGGGAACTGACCAAACAG AGCAGAAA---------TGTCATGGGATGAATATTACTAGTGATGGTCATGAGAATAAAACAAAACGTGATTAT---GTT CAGAAAGAGCAAAACGCTAACCCAGCAGAA------TCATTGGAAAAAGAATCTGTTTTCAGAACTGAGGCTGAACCTAT AAGCATCAGTATAAGCAATATGGAACTAGAATTGAATATCCACCGTTCAAAAGCACCTAAG---AATAGGCTGAAGAGAA AGTCCTCTACCAGGAAAATTCCTGAACTTGAACTAGTAGTCAGTAGAAACCCAAGTCTACCTAATCATACTGAGCTACCA ATTGATAGCAGTTCTAGCAATGAAGAG---ATGAAGAAAAAA---CATTCTAGCCAAATGCCAGTCAGGCAGAGCCAAAA GCTTCAACTCATTGGAGATAAAGAACTTACTGCTGGAGCC---AAGAATAACAAAACATATGAACAAATAAATAAAAGAC TTGCTAGTGATGCTTTTCCAGAACTAAAGTTAACAAACACACCTGGTTATTTTACTAACTGTTCTAGT------AAACCT GAAGAGTTTGTTCATCCTAGCCTTCAAAGAGAG---------GAGAAC---CTAGGAACAATTCAAGTGTCGAATAGTAC CAAAGACCCCAAAGATCTGATATTAAGAGAAGGAAAA---GCTTTGCAA---ATTGAAAGATCTGTAGAGAGTACCAATA TTTCCTTGGTTCCTGATACTGATTATAGCACTCAGGATAGTATCTCATTACTAGAAGCTAAAACCCCAGAA---AAGGCA AAG---ACTGCACCAAATCCATGTGTGAGTCTGTGTACAGCAACCAAAAACCTCAAGGAACTTATCCATAGGGAT---TT TAAAGATACCAAAAACAACACAGAGGGCTTTCAGGATCTACTGGGACATGACATTAACTACGTCATTCAGGAGACAAGCA GAGAAATGGAAGACAGTGAACTTGATACACAGTATTTGCAGAATACATTCAAGGCTTCAAAGCGTCAGACATTTGCTCTG TTTTCCAATCCAGGAAATCCACAAAAGGAATGTGCCACAGTCTTTGCCCACTCGGGGTCCTTAAGGGATCAAAGTCCAAG AGACCCCCTCAAATGCAGACAAAAAGAAGACAGTCAGGGAAAGAGTGAGTCTAAAAGCCAGCACGTGCAGGCCATTTGTA CAACAGTGCACTTTCCTGTGGCTGATCAGCAAGATAGGACGCCAGGTGACGATGCCAAATGTAGCGCAAAAGAAGTAACT AGGGTTTGTCAGTCATCACAGTTGAGA---GGCCACAAAACTGAACTTGTTTTTGCAAATAAACAAGGGGTTTCAGAAAA ACCAAATCTTATACCATCACTTTCTCCCATCAAGTCATCTGTTAAAACCATATGTAAGAAAAGC---CCATCAGAG---A AGTTTGAGGAACCTGTAACGTCACCTGAAAAAACATTGGGGAGTGAGAGCATCATTCAAAGTGCAGTGAGCACAATCAGC CAAAATAACATTCAAGAAAGCACTTTTAAAGAAGTCAGCTCAAACAGTGTAAATGAAGTAGGTTCCAGTACTAATGAAGT AGGCTCTAGTGTTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACCAGGTA GAAACAGAGAACCTAAATTAAGAGCTTTACTCGGATTAGGTCTTACGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT GTAAGTAACTGTCACCATCCTGAAATAAAAAGGCAAGGAGAAAATGAGGACATGCCTCAGGCTGTTAAGGCAGATTTCTC CCCATGTCTAATTTCAGATAACCTCGAACAA---CCTACGGGAAGCCGTCATGCTTCTCAGGTTTGTTCTGAGACACCTG ACAACTTGTTAAATGATGATGAAATAAAAGAAAATAGCCACTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTGTTTTT AGTGAAAGTGTCCAAAAAGGAGAATTCAGAGGGAGCCCTGGCCCTTTCACCCAT---ACACATTTGGCTCAGGGTCACCA AAGAGGGGCTGGCAAACTAGAG---TCAGAAGAGACTGTGTCTAGTGAG >Hippo TGTGGCACAGATACTCGTGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGCAGA AAAGGCTGAATTCTGTAATAAAAGCNAACAGCCTCTCTTAGCAAAGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGACA CATGTAATGAT---AGACAGACTCCCAGCACAGAGAAAAAGGTAGTTCTGAACGCTGATCCCCTATACGGGAGAAAAGAA CTGAATAAGCAGAAACGTGCATGCTCTGACGGCCCTAGCGAT---TCCCAAGAT---GTTCCTTGGATAACACTGAATAG TAGCATACAGAAAGTTAATGAATGGTTTTCCAGGAGTGGCGAGATGTTAACTTCTGACGACTTATGTGTTAAGGGGTGTG AATCAAATACTGAAGTAGCTGGTGCAGCGGAAGTTCCAAAT------GAAATTGATGGGTGTTTGGGTTCTTCAGAGAAA ATAGATTTAATGGCCCGTGACCCTCGTGGTGCCTTAATACGTGAAAGTGAAAGAGTCCACGCCAAACCAGTAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACATAGCTGAAGATC TAATTAAAGGAGCATCTGCTGTAGGACCTCAGATAACACAA--------------------------------------- ---------------------GAGCGCCCCCTCACAAATAAACTAAAGCGTAAAAGGAGGCGGACATCGGGCCTTCATCC TGAGGATTTTATCAAGAAAGTCGATGCGGCAGTTGTTCCAAAGACTCCTGAAAAGATAATTGAGGGAACTGATCAAACAG AGCAGAAT------------------------------------GGTCATGAGAATGAAATGAAAGGTGATTAT---GTT CAGAAAGAAAAAAATGCTAACCCAACAGAA------TCTTTGGAAAAAGAATCTGCTTTCAGAACTAAGGCTGAACCTAT AAGCATCAGTATAAGCAATATGGAACTAGAATTGAATATCCACAGTTCAAAAGCACCTAAG---AGTACGCTGAGAAGGA AGTCCTCTACCAGGCAGATTCATGCACTTGAACTAGTAGTCAGTAGAAACCCGAGCCCACCTAATCGTACTGAACTACAA ATCGATAGTTGTTCTAGCAGTGAAGAG---ATGAAGAAAAAA---CATTCCTGCCAAATGCCAGTCAGGCACAGCAAAAA GCTTCAATTCATGGGATATAAAGAACCCGCAACTGGAGTCAGGAAGAGTAATAAACCACACGAACAAATAAATAAAACAC TTGCCAGTGGTGCTTTTCCAGAACTAAATGTAACAAACATACCTGGTTTTTTTACTAACTGTTCTAGTTCTAGTAAACTT CAAGAGGTTGTTAATCCTGGCCTTCCAAGAGAG---------GAGAAC---CTAGGAACAATTCAAGTGTCAAATAGTAC CAAAGACCCCAAAAATCTGATATTAAGTAGAGGAAAA---GGTTTGCAA---ATTGAAAGATCTATAGAGAGCACCAGTA TTTCCTTGGTACCTGATACTGATTATGGCACTCAGGACAGTATCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAG---ACAGCAACAAATCAACGTGTGGGTCTGTGTGCAGCAACTGAAAACCCCAAGGAACTTATCCATGGTTAT---TC CAAAGATACTAGAAACGACATGGACGGCGTCCAGCATCCATTGGGACAGGATGTTAAC---CACACTCAGGATGCAAGCA TAGAAGTGGAAGACAGTGAACTTGATACACAGTATTTGCAGAATACATTCAGGGTTTCAAAGCGTCAGACATTTGCCCTG TTTTCAAATCCAGGAAATCCAGGAAAGGAACGTGCAACAGTCTGTGCTCATTCCGGGTCCTTAAGGGAACAAAGTCCAAG AGTCCCTCTTGAATGCGGACAAAAAGAAGAAAATCAGGGCAAGAGTGAATCTAAA---------ATGCAGGCAATTTATA CAACTGTGGACTTTGCTGTGGCTGGTCAAAATGATAGGAAGCCGAGTGATTACACCAAATGTAGCACTAAAGGAGTAACT AGGCTTTGTCCCTCATCACAGTTTGGA---AGCAACAAAACTGAGCACATTATTGCAAATAAATATGGAATTTCACAAAA CCCATATGTTATACCATCACTTTCTCCCATCAGGTCATCTGTTAAAACTATACGGAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACCTGTAGTGTCAGCTGAAAGAGCAATGGCAAATGAGAGCATCCTTCAAAGTACAGTGAACACAATTAGC CAAAATAACATTCGAGAAAACACTTTTAAAGAAGTCAGTTCAAGCAGTATTAATGAAGTAGTTTCCAGTACTAATGAAGT AGGCTCTAGTATCAGTGAAGTAGGTTCTAGT---------------------GGTGGAAACATTCAAGCAGAACTAGACA GAAAGAGAGGACCTAAACTAAGTGCTTTGCTTAGATTAGGTCTTATGCAACCTGAAGTATATAAGCAAAGTCTT---CCT GTAAGTAATTGTCAACATCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGCATACTTCAGGCTGTTAATGCAGATTTCTC CCCGTGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGCAGTCATGCTTCTCAGGTTTGTTCTGAAACACCTG ATGATTTGTTAAATGATGACGGAATAAAGGAAAATAGCAACTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTGTTTTT AGCAAAAATGTCCAAAAAGGAGAATTCAGAAGGAGCCCTGGCCCTTTAGCCCAT---ACACGTTTGGCTCAGGGTCACCA AAGAAGGGCTGGGAAATTGGAGTCCTCAGAAGA---------------- >SpermWhale TGTGGCACAGATACTCATGCCAGCTCATTACAGCATGAAAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGTCTTAGCAAAGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGACA CATGTAATGAT---AGGCAGACTCCCAGTACAGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTATATGGGAGAAAAGAA CTGAATAAGCAGAAATCTGCATGCTCTGACAGTCCTAGAGAT---TCCCAAGAG---TTTCCTTGGATAGCAGTGAATAG TAGCATACAGAAAGTTAATGAATGGTTTTCCAGAAGTGATGAAATGTTAACTTCTAACGACTTACGTGATGGGGGATTTG AATCAAACCCTGAAGTAGCTCGTGCAGTGGAAGTTCCACAG------GAAGTTGATGGATATTTGGGTTCTTCAGAGAAA ATAGACTTAATGGCCAGTGATCCTCATGGTGCTTTAATACGTGAAAGTGAAAGAGTCCACTCCAAACCAGTAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACGTAGCTGAAAATC TAATTATAGGAGCATCTACTGTAGGACCTCAGATTACACAA--------------------------------------- ---------------------GAGCGCCCCCTCACAAATAAACTAAAGCGTAAAAGGAGAAGTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTCGATTTGGCAGTTATTCCAAAGACTCCTGAAAAAATAATTGAGGGAACTGACCAAACAG AGCAGAAT---------GGTCATGGGGTGAATATTACTAGTAATGGTCATGAGACTGAAATGAAAGGTGATTGT---GTT CAGAAAGAGAAAAATGCTAACCTAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTAAGGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTGAATATCCATAGTTCAAAAGCACCTAAA---AATAGGCTGGGGNGGA TGTCCTCTTCCAGGAAGATTCATGCACTTGAACTAGTAGTCAGTAGAAACCCAAGCCCACCTAATCATACTGAACTACAA ATTGATAGTTGTTCTAGCACTGAAGAG---ATGAAGAAAAAA---CATTCCAGCCAAATGCCAGTCAGGTGCGGCAAAAA GCTTCAATTCATTGGAGATAAAGAACCTACAACTGGAGCCAAGAAGAGTAACAAGCCACATGAACAAATAAATAAAAGAC TTGACAGTGACACTTATCCAGAACTAAATTTAACAAACATACCTGTTTTTTTTACTCACTGTTCTAGTTCTAATAAACTT CAAGAGTTTGTTAATCCTAGCCTTCAAAGAGAG---------GAGAAC---CTAGGAACAATTCAAGTGTCGAATAGTAC TGAAGACCCCAAAGATCTGACATTAAGTGGAGGAAAA---GGTTTGCAA---ATTGAAAGATCTGTAGAGAGTTCCAGTA TTTCCTTGGTACCTGATACTGATTATGGCACTCAGGATAGTATCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAG---ACAGCACCAAATCAACATGTGAGTCTGTGTGCAGCAATTGAAAGCCCCAAGGAACTTATCCACGGTTGT---TC TAAAGATATTAGAAACGACACAGAGGACTTTAAGGATCCACTGGGACATCACGTTAAC---CACATTCAGGAGGCGAGCA CAGNNNNNNNNNNNNNNNNACTTGATACTCAGTTTTTGCAGAATATGTTCAAGGTTTCAAAGCGTCAGACGTTTGCTCTG TTTTCAAATCCAGAAAATCCAGAAAAGGAATGTGCAACAGTCTGTGCCCACTCTGGGTCCTTAAGAGAACAAAGTCCAAG AGTCCCTCTTGAATGCAGACAAAAAGAAGAAAATCAGGGAAAGAGTGAGTCTAAAATCAAGCATGTGTGGGCAATTAATA CAACTGTGGACTTCCCTGTTGCTGGTCAAAAAGATAAG---CCGAGCGATCATGCCAAACGTAGCCCCAAAAGAGTAACT AGGCTTTGTCAGTCATCACAGTTCAGA---AGCAACAAAACTGAGCTCATTATTGCAAATAAACATGGGATTTCACAAAA CCCATATCTTATACCATCACTTTCTCCCATCAGGTCATCTGTTAAAACTATATGTAAGAAAAAC---CTGTCAGAGGAAA AGTTTGCGGAACCTGTAATGTCACCTGAAAGAGCAATGGAAAACGAGAGCATCATTCAAAGTACAGTGAGCACAATCAGC CAAAATAACATTCGAGAAAGCACTTTTAAAGAAGTCAGCTCAAGCAGTACTAATGAAGTAGGTTCCAGTACCAATGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAAAACTAGATA GAAACAGAGGACCTAAATTAAGTGCTTTGCTCAGATTAGGTCTCATGCAACCCGAAGTCTATAAGCAAAGTCTT---CCT GTAAGTAACTGTCAACTTCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGCACACTTCAGGCTGTTAATGCAGATTTCTC CCCATGTCTAGTTTCAGATAACCTAGAACAA---CCTATGGGAAGAAGCCATGCTTCTCAGGTTTGCTCTGAGACATCTG ATGAGTTGTTAAATGATGACAAAATAAAGGAAAATAGCAACTTTGCTGAAAGTGACATTAAGGAAAGATCT---GTTTTT AGCAAAAGTGTCCAAAAAGGAGAATTCAGAAAGAGCACTGGCCCTTTAGCCCATCATACATGTTTGGCTCAGGGTCACGA AAGAGGGGCT---------GAGTCCTCAGAAGAGAAAGTGTCTAGTGAG >HumpbackW TGTGGCACAGATACTCATGCCAGCTCATTACAACATGAAAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGTCTCAGCAAAGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGACA CATGTAATGAT---AGGCAGACTCCCAGTACAGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTGTATGGGAGAAAAGAA CTGAATAAGCAGAAACCTGCATGCTCTGACAGTCCTAGAGAT---TCCCAAGAG---TTTCCTTGGATAACAGTGAATAG TCGCATACAGAAAGTTAATGAATGGTTTTCCAGAAGTGATGAAATGTTAACTTCTAACGACTCACGTGATGGGGGATTTG AATCAAACACTGAAGTAGCTTGTGCAGTGGAAGTTCCAAAG------GAAGTTGATGGATATTTGGGTTCTTCAGAGAAA ATAGACTTAATGGCCAGTGATCCTCATGGTGCTTTAATACGTGAAAGTGAAAGAGTCCACTCCAAACCAGTAGAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACATAGCTGAACATC TAATTATAGGAGCATCTACTGTAGAACCTCAGATAACACAA--------------------------------------- ---------------------GAGCGCCCCCTCACAAATAAACTAAAGCGTAAAAGGAGAAGTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTCGATTTGGCAGTTATTGCAAAGACTCCTGAAAAAATAACTGAGGGAACTGACCAAACAG AGCAGAAT---------GGTCATGGGATGCATGTTACTAGTAATGGTCCTGAGACTGAAATGAAAGATGATTAT---GTT CAGAAAGAGAAAAATGCTAACCTAACAGAG------TCATTGGAAAAACAATCTGCTTTCAGAACTAAGCCTGAACCTAT AAGCAGCAGTATAGGCAATATGGAACTAGAATTGAATATCCATAGTTCAAAAGCACCTAAA---AATAGGCTGAGGAGGA AGTCCTCTACCAGGAAGATTCATGCACTTGAACTAGTAGTCAGTAGAAACCCAAGCCCACCTAATCATACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATGAAGAAAAAA---CATTCCAGCCAAATGCCAGTCAGGCACGGCAAAAA GCTTCAATTCATGGGAGATAAAGCACCTACAACTGGAGCCAAGAAGAGTAACAAGCCACACGAACAAATAAATAAAAGAC TTACCAGTGACGCTTATCCAGAACTAAATTTAACAAACATACCTGGTTTTTTTACTCACTGTTCTAGTTCTAATAAACTT CAAGAGTTTGTTAATCCTAGCCTTCAAAGAGAG---------GAGAAC---CTAGGAAAAATTCAAGTGTCGAATAGTAC CAAAGACCCCAAAGATCTGACATTAAGTGGAGGAAAA---GGTTTGCAA---ATTGAAAGATCTGTAGAGAGTACCGGTA TTTCCTTGGTACCTGATACTGATTATGGCACTCAGGATAGTATCTCATTACTGGAAGCTGACACCCTAGGG---AAGGCA AAG---ACAGCACCAAATCAACATGTGAGTCTGTGTGCAGCAATTGAAAGCCCCAAGGAACTTATCCATGGTTGT---TC TAAAGATATTAGAAACGACACAGAGGACTTTCAGGATCCACTGGGACATCACGTTAAC---CACATTCAGGAGGCGAGCG CAGAAATGGAAGAGAATGAACTTGATACACAGTATTTGCAGAATATGTTCAGGGTTTCAAAGCGTCAGACGTTTGTTCTG TTTTCAAATCCA---------GAAAAGGAATGTGCAACAGTCTGTGCCCGCTCTGGGTCCTTAAGAGAACAAAGTCCAAG AGTCCCTCTTGAATGCAGACAAAAAGAAGAAAATCAGGGAAAGAGTGAGTCTAAAATCAAGCATGTGCGGGCAATTAATA CAACTGTGGACTTCCCTGTTGCTGGTCAAAAAGATAAGAAGCCGAGCGATCATGCCAAACGTAGCCCAAAAAGAGTAACT AGGCTTTGTCAGTCATCACAGTTCAGA---AGCAACAAAACTGAGCTCATTATTGCAAATAAACATGGGATTTCACAAAA CCCATATCTTATACCATCACTTTCTCCCATCAGGTCATCTGTTAAAACTATATGTAAGAAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACCTGTAAGGTCACCTGAAAGAGCAATGGAAAACGAGAGCATCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTCGAGAAAGCACTTTTAAAGAAGTCAGCTCAAGCAGTATTAATGAAGTAGGTTCCAGTACTAATGAAGT AGGCTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAAAACTAGRTA GAAACAGAGGACCTAAATTAAGTGCTTTGCTCAGATTAGGTCTTATGCAACCCGAAGTCTATAAGCAAAGTCTT---CCT ATAAGTAACTGTCAACTTCCTGAAATAAAAAGGCAAGGAGAAAGTGAAGGCACACTTCAGGCTGTTAATGCAGATTTCTC CCCAAGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGAAGCCATGCTTCTCAGGTTTGTTCTGAGACATCTG ACGAGTTTTTAAATGATGACAAAATAAGGGAAAATAGCAACTTTGCTGAAAGTGACATTAAGGAAAGATCT---GTTTTT AGCAAAAGTGTCCAAAAAGGAGAATTCAGAAGGAGCCCTGGCCCTTTAGCCCATCATACATGTTTGGCTCAGGGTCACGA AAGAGGGGCT---------GAGTCCTCAGAAGAGAATGTGTCTAGTGAG >Mole TGTGGCATAAATACTCATGCCAGCTTATTACAGCATGAAAACAGCAGTTTATTACTCACTGAAAACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGACTTAGCAAAGAGCCAGCAGAACAGATGGGCTGAAAGTAAAGAAA CATGTAATGAT---AGGCAGACTTCCAGCCCAGAGAAAAGGGTAGACCCGAATGCTGATCCCATGTATGGGAGAAAAGAA CTGAATAAGCAGAAACCTCCATGCTCTGACAGCCCCAGAAAT---TCCCAAGGT---GTTGCCTGGATAACACTGAACAG TAGCATTCGAAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAAATATTAACTTCTGATGAATCTCGTGATGGGGAGTCCC CATCAAATATTGAAATGACTGGTGAAGTAGAAGTTCCAAAC------ACAGTAGATGGATTTTCTGGTTCTTCAGAGAAA ATAGACTTAATGGCCAGCGATCCTCCC---GCTTTAATGTGTAAAAGTGAAAGAGTCCGCACCAAACCAGTAGAAAGTAA T---ATTGAAGATAAGATATTTGGGAAAACCTATCGGAGGAAGACAAGCCTCCCAAACTTGAGCCACGTGGCTGAATATT TAATTACAGGGACATCTGTCACAGAACCTCAGATAGTTCAA--------------------------------------- ---------------------GAGCGTCCCTTCACAAATAGATTAAAACGTAAAAGGAGAACTCTACCAGGCCTTTGTCC TGAGGATTTTATCAAGAAAGTAGATTTGGAAGTTGTTCAGAACACCCCTGAAAAGATAAGTGCGGGAACTGATCAAATGG ATCAGAGT---------GGTCAAGTGATGGATGTTGCTTATAATGGTCATGGGAATGAAACAAAACGTGATTAT---GTT CAGAAAGAGAAAAGTGCTAACCCAGCCGAA------TCTTTAGAAAAAGAATCTACCTTCAGAATTAAAGCTAAGCCCAT AAGTAGCAGTATAAGCAATATGGAACTAGAATTAAATATCCACTCTTCAAAAGCACCTAAGAAGAACAGGCTGAGGAGGA AGTCTTCTACCAAGCATATTCATGCACTTGAACTGGTGGTCAAAAGAAATCCAAGCCCACCTAATCATACAGAACTACAA ATTGATAGCTGTTCTAGCAGTGAAGAG---ATGAAGGAAAGA---AATTCCAACCAAATGCCAGTAAGACACAGCAAAAG GCTTCAACTCATGGAAGATAAAGACCCCGCAACTGAAGCCGTCAAGAGTAACAAGCCAAAAGAACAAATT---AAAAGAT TTGCCAGTGATACTTTGCCAGAACTAAATTCAACACATGTACCTGGCTTTCTTAGCAACTATTCAAGTTCTAATAACCTT GAAGAGTTTTCCAATCCTAGCCTTCAAAGAGAAGAACTAGAAGAGAAC---CTAGGAACAAATCAAGTGTCAAATAATAC CAAAGACCCCAAAGATCCGATACTTAGTGGACAAAGA---GATTTGCAA---GCTGAAAGATCTGGCGAGAGTACCAATA TTTCATTGGTACCTGAGACTGATTTTGGCAGTCAGGATAGTGTCTCATTACTGGAAATTGACATCCTAGGC---AAGGCA AAA---AAAGTGCCGAATCAGTGTGCAAGTCTGTGTACAAAAATTGAAAACTCTAAGGAACTTATTCATAGTTGT---TC TAAAGATACTAGAAATGACACACAGGGCTGTAAGGATCCACTGAAATACGAAGTTAAC---CACACTCAGGAGATAAGGA TAGAGATGGAAGAGAATGAACTTGATACACAGTATTTACAGTCTACGTTCAAAGCTTCAAAGCGTCAATCATTTGCTCTG TCTTCACATCCGGGAAATTCAGAAAAGAAATATCCCCCAGTCTCTGCCCCTTCCAGGTCCTTGAAGAGACAAACTCCAAA AATCACTCTGGAATGTGAACTGAAAGAAGAAAATCAAGGGAAGAAAGAGTCTAAAACCGAGCATGTACAGGCAGTACATA GAATTGCAGACTTAACTCTGGCTTGTCAGAACGATAAG---CCACATGATTCTGCCAAATGTAGCATAAAAGGAGTCTCT AGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAACGAAACAGAAGTCATTGTTGCAAATAAACGTTTAATCTTACAAAA CCCATATCTTATTCCACCACTTTCTTCAATTCAGTCATCTGTTAAAAGTGCTTGTAAGAAAAAC---CTGGCGGAGGAAA AGCTTGAGGAACACTCATGGTCACCCGAAAGAGAAACAGGAAACGGGAGCATCATTGAAAGTACAGTGAGCAGGGTTAGC CAAAATAACAATAGAGAAAATGCTTTTAAGGAAGTCAGCTCAAGTAGTATTAACGAAGTAGGTTCCAGTACTAACGAAGC TGACTCTAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAATATTCAAGCTGAACTAGATG GAAGCAGAGGACCTAAATTAAATGCTATGCTCAGATTGGGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT AAAAGTACTTGTAAACATCCTGAAGTTAAAAGGCAAGGAGAAAATGAAGGCATAGTTCAGGCTGTTCATACAGATTTCTT TCCATGTCTGATTTCAGATAACCAAGAACAA---CCTATGGGAAGTAGTCATGCTTCTCAGGTTTGTTCTGAGACGCCTG AGGACCTGTTAAATGACGAAATAAAGGATAATAACATCAGCTTTGCTGACAGTGGCATTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTTTCCAGAAAGGAGAATTC---AGGAGCCCTAGCCTTTTAGACCAT---ACATGTTTGGCTCAGGACCACGA AAGTGGG------AAATTGGAGTCCCCAGAAGAGACTATGTCTAGTGAG >Hedgehog ---------------------------------CGTGAGAACAGCAGTTTATTACTCACTAAAGGCAAAATGAATGTAGA AAAGGCTGAATTCTGTAGTAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAATATGGAAA CATGTAATGAT---AGGCAGACTCCTAGCCCAGACAAAGAAGTAGATCTGAATGCTGATTCCTTATATGAGAGAAAAGAA CTAAACGAGCAGATCTCTTCATACTCCAGCAGTCCTAAAGAT---TCCAAAGAC---ATTTCTTGGGTAGCACTGAAT-- -AGCATACAGAAAGTGAATGAATGGCTTTCCAGAAGTGATGAACTGTTAACTTCTGATGACTCATATGATAAGGGATCTA AATCAAAAACTGAAGTAACTGTAACAACAGAAGTTCCAAAT------GCAATAGATAGRTTTTTTGGTTCTTCAGAGAAA ATAAACTTAACAGCCAGTGATCCTCATGTTGCTTTAATACGTGAAGGTGAAGGAGTCCACTTCAAACCAGTAAAGAATAA T---ATTGAAGATAAAATATTTGGGAAAACCTATGGGAGGAAGGCAAGCCTTCTTAATTTGAGCCACGTAACTGAAGATG TAATTATAAGG------------GAACCTCAGGTAGCCAAA--------------------------------------- ---------------------GAGCCTCTCCTTGCAACTAAATTAAAACGTAAAAGGAGAACTGATGTAGGTCTTTGTCC TGAGGATTTTATCAAGAAAGTAGATGTGGCAATTGTTCAGAAGACTCCTGAAAAGATAATCAAGAGACCTGGCCAACTGG ATCAAAGT---------GACCAAGTAATGAATATTGCTACTAATGGTCATGAAACTGAAACAAAGAGTGATTAT---GTT CAGAAGGAGAAAAATGCTAACCCAGCAGAA------TCACCAGACAAAGAATCTGCTTTTAGAAATAAACCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATGTCCACAGTTCAAAAGCACCTAAGAAGAATAAACTCAGGAGAA AGTCATCTGCCAGGCATATTCATGCACTTGAAGTAGTAGTCAATAAAAACTCAAGCCCACCTAGCCATACAGAACTACAA ATTGATAGCTGTTCTAGCAATGAAGAG---ATGAGGGGGGTG---AAAGCTGATCAAATGCCAGTCAGGCACAGCAAACA ACTTCCACTCATTGAAGACAAAGAACCTGCAACTAGAGCCTTTAGTAGTAGCAAGTCAAATGAACAAATAAGTAAACAGT TTGTTGGTGAGACTTTTTCAGACCTAAATTTAACAAACATCCCTGGTTTTCTTACCAGCTGTTCAAGTTCCAATAAACAT CAGGAATGTATTAATCCTAACCTACAAAGAGAGGTATTAGGAGAAAGC---CGTGGGACTATTCAAGTGTGTAATAATAC CAAAAACCCTAAAGATCTGATAGTAAGTGGAAAGAGA---GGTTTACAA---ATTGAAAGATCTGTAGAGAGTCCT---- --------GTAGAGAACACTGACTATGGCAGTCAGGATAGTATCTCATTACTGGAAACTGATACTCTAGGG---AAGGCA AAA---AAGACACCAAATCAATGTGTAAGTCTGTGTGTAGCAACTGAAACCCCCAAAGAACTTAGCCATAGTTGT---TC TAAAGATACTAGAAATGACACTGAAGGCTTTAGGGATTCACTCAGATGTGAAGTTGAT---CACACGCAGGAGACAAGCA CAGAACTGGAGGAGAGTGAACTTGATACACAGTATTTACAGAATACCTTCAAGGTTTCAAAACGTCGGTCATTTGCTCTG TTTTCAAGTCCAGAGAATTCAGAAAAGGCATGTACAAGAGGCTCTGTCCATTCTAAGTCTTTAAGGAAACAAAGTCCAAA AGTTATTCTGGACTGTGAACAAAAAGAAGAAAATCAAGGAGAGAAAGCATCTGAAATCAAGTATTTGCCATCAGAACATA CAACTACAGGCTTTCCTGTGGTGTGTTATAAAGATACA---TCAGGTGATTATGCCAAATGTAGCGTAAAAGGAGTCTCC AGGCATTGTCAGCCATCTCGGTTCAGA---GGCAGTGAACCTGAACTCATTGTTGCAAATAAAAATTTAATTTTACAAAA CCTATATCATATACCAACACTTCCTACCATCATGTCATCTACTAAGAGTATATGCAAGAAAAAC---CTGTCTGAGGAAA AT------------TCACTGTCGCCTGAAAGAGCAGTGACAAACAAAAGCATCATCCAAAGTACAGTGAACACCATTAGC CAGACTAATGCCAGTGAAAATGCTCTTAAAGATGTCAGTTCAAGCAGTGTTAATGATATGGGTTCCAGTACTAATGAAGT AGGTTCCAGT------------------------------------------AGTGAAAACATTCAAGCTAAACTATGTA GAAACAGAGGACCCAGATTAAATGCTACTCTTAGAGCAAGTCTTATGCAACCTGAAGTCTTTGAGCAGTGTCTT---CTG ATGAGTAACTGTAAACATTCTGAAATGAAAAGACAAGGAGAAAATGAAGGTGTTATTCAGACTGTTAATATAGAATCCGA TTCATGCCCAAGACCAGATAACTTAGAACAA---CCGGTGGAAACTAGTCAT---------GTTTGTTCTGAAACACCTG ACGATCTGTTAAATGACGATGAAATGAAGGAAAATACAAGCTTTACTGAAAGTGGCATTAAGGAACAATCTGCTGTTTTT GGCAAAAGTACCCAGAAAGGT---TTCAGAAGGAGCCCTAGCCCTTTAGGCCAC---ACATGTTTGAGACAGGATCAGCA AAGAGGGGCACAAAAATCAGAGCCCTCTGAAGATTTCATGTCTAGTGAG >TreeShrew TGTGGCATAAATACTTATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAGGACAGAATGAATGTAGA AAAGGCTGAATTGTGTAATAAAAGCCAACAACCTGACTTAGCAAGGAGCCAGCAGAGCAGATGGACTGAAAATAAGGAAA CATGTAATGAT---AGGCAGATTCCCAGCACAGAAAAAAAGGTAGATCTAAATGCTGATCCCCTGTGTGGGAAGAAAAAA CAAGCTAAGCAGAAACYGCTATGTTCTAACAGTCCTAGAGAT---GACCAAGAT---TCTCCTTGGATAACTCTAAATAG TAGCATTCAGAAAGTTAATGAATGGTTTTCCAGAAGTGATGAAATGTTAACTTCTAACGACTCACATGATGGTGAGTCTG AA------------ATAGCTGGTGCATTYGAAGYTCCAAAT------AAAGTAGATGAATATTCTGGTTCTTCAGAAAAA ATAGACTTAATGGCCAACAATCTTCATGATGCTTTAATAAGTAAAAGTGAAGGAATCTACTCCAAACCAGTAGAGGGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAAGCAAGTCTTCCTAACTTGAGCCGTGTAACTGATGATC TAATTAGAGGGGCATTTGTTACAGAGCCTGAGATAACTCGA--------------------------------------- ---------------------GAGCGTCCCTTCACAAATAAATTAAAGCGGAAAAGGAGAACTATATCAGGCCTTCATCC TGAAGATTTTATCAAGAAAACAGATTTGGCAGTTGTTCAAAAGACTCCTGAAAAGATAAATCAGAGAACTGACCAAATAG AGCATAAT---------GGTCAGGTGATGAGTATTGCTAATAGTGGTCATGAGAATGAAACAAAAGGTGATTAT---ATT TCGAAAGAGAAAAATGCTAACCCAATGGAA------TCATTAGAAAAAGAATCTGCTCTCAGAACTAAAGCTGAGCCCAT AAGCAGCAGTGTAAGCAATATGGAACTAGAAATAAATAACCACAGTTCAGAAGCACCTAAGAAGAATAGGCTGAGGAGAA AGTTTTCTGCTAGGCATATTCGCACACTTGAACTAGTAGACAATAAAAGTCCAAGCCCACCTAATCGTACTGAACTACAA ATTGACAGTTATTCTAGCGGTGAAGAG---AGAAAGAAAAAG---GGT---GAGCAAATGCCAGTTGGACACAGCAGAAA GTTTCAACTTGAGGAAGAGAAAGAACCTACAACTGGAGCCAAGAAAAATAACCAGCCAAATACAGAAATAAGTGAAAGAC ATGCCAGTGGTGTTATCCCAGATCTGAAGTTAACAAACATACCTGGTTTTTTCACAAACTCTTCGAGTTCTAATAAACTT CCAGAATTTGTCCATCGTAGCCTTCAAAGAGAAAAA---GAAGAGAAC---CGAGAAACAATTCAAATATCCAGTAGTAC C---------AAAGATCTGGTATTAAGGGGAGAAAGG---GGTTTGCAA---GATGTAAGGTCTGCAGAGAGTACCAGTA TTTCTTTGGTACCTGATACTGATGATAACACCCAGGATAGCATCTCATTACTAGATGCTAACCCCCTAGCTAGGAAGGCA AAA---ACAGCACCAAATCAATGTGTAAATCAGAGTGCAACAACTGAAAACCCCAAGGAACTTATACACAGTTGT---TC TAAAACTACTAGGAAT------GAAGGCTTCAAGGATCCATTGAAAAGTGAAGTTAAT---CATATTCAGGAGATGAGTG TAGAAATGGAGGAGAGTGAACTTGATACTCAGTATTTACAGAATACATTCAGGAGTTCAAAGCGTCAGTCATTTGCTCTG TCTTCAAATCCAGGAAATCCAGAAAAGGAACATGTCTGTGTT-------------------------------------- -------------------------------AAAGAAAGTCTGAAAGAGTCTAACATCCAACATATACAGGCAGTTAGTA CC---------------ATGGTTTTTCAGAAAGATAAG---CTAGGTGATTTTGCTACATCTGGCATTAAAGAAGTCCCT AGACTTTGTCCATCATCTCAGTTCAGA---GGCAATGAAACTGATCTCATTACTGCAAATAAACCTGAAGTTTCACAAAA CCCGTATCATATGCCATTACTTTATCCTGTCAAGTCACCTATTATAACTAAAAGTAAGAAAAGC---CTGTCAGAGGAAG GGTTTGAGGAACAGGCAATGTCACTTGAAAGAGCAATGGAAAATGAGAACATCATTCAAAGTACAGTGAGCACAATTAGC CAAGATAACATTAGAGAAGGTGCTTTTAAAGAAGCCAGCTCAAGCAGTATTAATGAAATAGGTCCTAGTACTAATGAAGG AAGCTCTAGTATTAATGAGGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAGGTA AAAAGAGAGGATCCAAATTAAATGCTGTGCTTAGATTAGGTCTTATGCAACCCGAAGTCTATAAGCAAAGTCTT---CCT TTAAGTAATCATAATGATCCTGAAATGAAAAGACAAGAAAAAAATGAAGGAGGAGTTCAGGCTATTAAA---GATTTACC TCCATGTCTAATTTCAGATAATCAAGAGCAT------ATGGGAAGTAGCCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGATCTGTTAGATGATGATGAAGGAAAAGAAAAT---AGCTTTGCTGAGGTTGATGTTAAGGAAAGATCTGCTGTTTTT GGCAAAACTGTCCAGAGAAGAGAGTTAAGAAGGAGCTCTAGCCCTTTAACTCGT---GCATGTTTGACTGAGGGTCAGCA AACAGGAGCCCAGAAATTAGATTCATCAGAAGAGAACCTATCTAGTGAG >FlyingLem TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACGCACTAAAGACAGAATAAATGTTGA AAAGACTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGGAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACGCCCAGCACAGAGAAAAAGATAGATCTAAATGCTGATTCCCAGCATGGGAGAAAAGAA CGGAATATGCAGAAACCTCCATACCCTGAGAGTCCTAGAGAT---ACCCAAGAT---GTTCCTTGGATAACACTAAACAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAAATTTTAGCTTCTGATGACTCACGTGACAGGGTGTCTG AATCAAATGCCAAAGTAGCTGGTGCATTAGAAGTTCCAAAT------GATGTAGATGGATATTCTGATTCTTCAGAGAAA GTTGATTTAATGGCCAGTGATCCTCATGATGCTTTAATATGTAAAAGTGAAAGAATCCACTCCAGACCAGTAGAGAGTAA T---ATCAAAGATAAAATATTTGGGAAAACCTATCAGAGGAAGACAAGCCTCCCTAACTTGAGCCACGTAAATGAAGATC TAATTATAGGAGCATTTGTTACAGAACCACAGATAACACAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAGGTAAAGCCTAAAAGGAGAACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGACTTGGCAGTTGTTCAAAAAACTCCTGAAAAGATAAATCAGGGAATTGACCAAATGG AGCAGAAT---------GATCGAGTGATGAATATTATTAATAGTGGTCATGAGAATGAAACAAAGGATGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTAAAGCAGAACCTAT AAGCAGCAGTATAAGCAATATGGAAATAGAATTAAATATCCACAATTCAAAACCATCTAAGAAGAATAGGCTGAGGAAGA TGTCCTCTACTAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGCCCACCTAATTATACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAA---ATAGAGAAAAAA---AATTCCAGCCAAATGCCAGTCAGGCACAGCAGAAA GCTTCAACTCATGGAAAATAAAGAACCTGCAACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAAATAAGTAGAAGAC ATTCCAGTAATGCTTTCCCAGAACTGCGGTTAACAAATGTACCTGTTTTTTTTGCTAACTGTTCAAGTTCTAATAAACTT CAAGAATTTATCGATCCTAGCCTTCAAAGAGAAGAAATAGAAGAGAAC---CTAGAAACAATTCATGTGTCTAATAGTGC CAAAGACCCCAAAGATTTGGTGTTAAGTGGGGAGAAG---GGTTTGCAA---ACTGAAAGATCTGTAGAGAGTACCAGTA TTTCATTAGTACCTGATACTGATTATGGCACTCAAGACAGTATCTCAATATTAGAAGCTAACATCCTAGGG---AAGGCA AAA---ACAGCACCAAGTCAACATGCAAATCAGTGTGCAGCAATTGAAAACCCCAAAGAACTTATCCATGGTTGT---CC TAAAGGTACTAGAAATGACACAGAGGATTTTAAGGATCCATTGAGATGTGGAGTTGAC---CACATTCAGAAGACAAGCA TAGAAATGCAAGAGAGTGAACTTGATACTCAGTATTTACAAAATATATTCAAGGTTTCAAAACGTCAGTCATTTGCTCTC TTTTCAAATCCAGGAAATCCAGAAAAGGAGTGTGCAACAGTCTATGCCCACTCCAGGTTGTTAAGGAAACAAAGTCCAAA AGTCACTCCTGAATGTGAACAAAAAGAAGAAAATGAGGGAAATAAAGAGTCTAAAATCAAGCACATACAGGCAGTTAATA CCACTGTGGGCTTTTCTGTCCTTTGTCAGAATGTTAAGAAGCCAGGTGATTATGCCAAATTTAGCATTAAAGGAGTCTCT AGGCATTGTTCATCATCTCAGTTCAGA---GGCAATGAAACTGAACTCATTACTGCAAATAAACATGGAATTTTACAAAA CTCATGTCATATGTCATCACTTTCCCCCATCAGGTCATCTGTTAAAATTAAATGTAAGAAGAAC---CTGTCAGAGGAAA GGTTTGAGGAACATTCAGTGTCACCTGAAAGAGCAATGGCAAACAAGAGAATCATTCAAAGTACAGTGAACACAATTAGC CAAAATAACATTAGAGACAGTGCTTCTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGTTCCAGTACTAATGAAGT AGGCTCCAGTATTAATGAAGTAGGTCCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGACCTAAATTAAGTGCTATGCTTAGATTAGGCCTCATGCAACCTGAAGTTTACAAGCAAAATCTT---CCT TTAGGTAATTGTAAACATCCTGAAATA---AGGCAAGAAGAAAATGAAGGAATAGTTCAGGCTGTTAATACAAATCTGTC TCTGTGCCTAATTTCACATAACCTCGAACAA---CCTATGGAAAGTAGTCATGCTTCCCAGGTTTGTTCTGAGACACCTG ATGACCTGTTAGATGGTGATGAGATAAAGGAAAACACCAGCTTTGCTGAAAGTGACAGTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAGAGAGGAGAGTTAAGCAGGAGCCCTAGCCCTTTTGCCCAA---ACATGTTTGGCTCAGGGTCACCA AAGAGGAGCCAGGAAATTAGAGTCTTCTGAAGAGAACGTATCTAGTGAG >Galago TGTGGCAAAAATACTCATGCCAGCTCATTACAGCATGAGAGCAGCAGTTTATTACTCACTAAAGACAAAATGAATGTAGA AAAGGCTGAATTTTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATCGGCTCAAAGTAAGGAAA CATGCAATGAT---AGGCACACTTGCAGCCCTGAGCAAAAGGTAGATCTGAATACTGCTCCCCCATATGGGAGAAAAGAA CAGAATAAGGAGAAACTTCTATGCTCCAAGAATCCTAGAGAT---AGCCAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAATGGTTTTCTAGAAGTGATGAAATGTTAACTTCTGATGACTCACATGATGAGGGTTCTG AATCACATGCTGAAGTAGCTGGAGCCTTAGAAGTTCCAAGT------GAAGTAGATGGATATTCCAGTTCCTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATTATCCTATAATATGTAAAAGTGAAAGAGTTCACTCCAAACCAATAAAGAGTAA A---GTTGAAGATAAAATATTTGGGAAAACTTATCGGAGGAAGGCAAGCCTCCCTAACTTAAGCCATGTAACTGAAAATC TAATTATAAGAGCAGCTGCTACTGAGCCACAGATAACACAA--------------------------------------- ---------------------GAGTGTTCCCTCACAAATAAATTAAAACGTAAAAGGAGAACTACATCAGGTCTTTGTCC TGAGGATTTTATCAAGAAGGCAGATTTGGCAGTTGTTCAAAAGACACCTGAAAAGAGAATTCAGGGAACTAACCAAGTGG ATCAGAAT---------AGTCACGTGGTAAATATTACTAATAGTGGTTATGAGAATGAAACAAAAGGTGATTAT---GTT CAGAATGAAAAAAATGCTAACTCAACAGAA------TCATTGGAAAAAGAATCTTCTCTCGGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGTAATATGAAATTAGAATTAAATATTCACAATTCAAAAGCAAGTAAAAAGAAAAGGCTGAGGAAGA AGTCTTCTAGCAGGCATATTCGTGCACTTGAACTAGTAGTCAATAAAAATCCAAGCCCTCCTAATCATACCAACCTACAA ATTGACAGTTGTTCTAGCAGTGAAGAA---ATAAAGGATAAA---AGTTCTGACCAAATACCAGTCAGGCATAGCAGAAA GCCTGGACTCATGGAAGATAGAGAACCTGCAACTGGAGCCAAGAAAAGTAACAAGCCAAATGAGCAAATAAGTAAAAGAC ATGTCAGTGATACTTTCCCAGAAGTGGCATTAACAAATATATCTAGTTTTTTTACTAACTGTTCAGGTTCTAATAGACTT AAAGAATTTGTCAATCCTAGCCTTCAAAGAAAAAAAACAGAAGAGAACTTAGAAGAAACAATTCAAGTGTCTAATAGTAC CAAAGGTCCGGTGTTAAGTGGAGAAAGGGTTTTGCAA---ATTGAAAGT---GAAGAAAGATCTATAAAAAGCACCAGTA TTTCATTGGTACCTGATACTGATTATGGTACTCAGGACAGTAACTCGTTACTGAAAGTTAAAGTCTTACGG---AAGGTG AAA---ACAGCACCAAATAAACATGCAAGTCAGGGTACAGCCACTGAAAACCCCAAGGAACTAATCCATGGTTGC---TC TAAAGATACTGGAAATGACACAGAGGGCTATAAGGATCCATTGAGACATGAAATTAAC---CACATTCAGAAGATAAGCA TGGAAATGGAAGACAGTGAACTTGATACTCAGTATTTACAGAATACATTCAAGTTTTCAAAGCGTCAGTCGTTTGCTCTG TTTTCAAACCTAGGA---------AAGGAATGTGCAACAGTCTGTGCCCAGTCTCTCTCTGCGTCCTTAAGAAAAGGTTC AAAAGTCATTCTTGAATGTGAACAAATAGAAAATCCAGGAATGAAAGAGCCTAAAATCAAGCATATACAGGGAAATAATA TCAATACAGGCTTCTCTGTAGTTTGTCAGAAAGATAAGAAAACAGATGATTATGCCAAATACAGCATCAAAGAAGCATCT AGGTTTTGTTTGTCAAATCAGTTTCGA---GACAATGAAACTGAATCCATTACTGTAAATAAACTTGGAATTTTACAAAA CCTCTATCATATACCACCACTTTCTCCTATCAGGCTATTTGATAAAACTAAATGTAATACAAAC---CTGTTAGAGGAAA GGTTTGAAGAACATTCAGTGTTACCTGAAAAAGCAGTAGGAAACGAGAACACCGTTCCAAGTACAATGAATACAATTAAC CAAAATAAC---AGAGAAAGTGCTTATAAAGAAGCCAGTTCAAGCAGTATCAATGAAGTAAGCTCGAGTACTAATGAAGT GGGCTCCAGTGTTAACGAAGTAGGCCCCAGT---------------------AGTGAAAACATTCAAGCAGAACTAGATA AAAACAGAGGACCTAAGTTGAATGCTGTGCTTAGATTAGGTCTTATGCAACCTGAAGTCTATAAACAAAATCTT---CCT ATAAGTAATTGTGAACATCCTAAAATAAAAGGGCAAGAAGAAAATGGA---GTAGTTCAACCTGTTAATCCAGATTTTTC TTCATGTCTAATTTCAGATAACCTAGAACAA---CCTACGAGAAGTAGTCATGCTTCTCAGCTTTGTTCTGAGACACCTG ATGACTTATTAGTTGATGATGAACTAAAGGAAAATACCAGTTTTGCTGAAAATAACATTAAGGAAAGATCTGCTGTTTTT AGCAAAAATGTCATGAGAAGAGAGATTAGCAGGAGCCCTAGCCCTTTAGCCCAT---ATACATTTGACTCAGGCTCACCA AAGAGAGGTTAGGAAATTAGAGTCCTCAGAAGAGAACATGTCTAGTGAA >HowlerMon TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTGTTACTCACTAAAGACACACTGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGAAAGTGAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGCATGGGAGAAAAGAA TGGAATAAGCAGAAACCTCCGTGCTCTGAGAATCCTAGAGATGATACTGAAGAT---GTTGCTTGGATAATGCTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAACTTCTGATGACTCACATGATGGGGGGTCTG AATCAAATGCCAAAGTAGCTGAAGCATTGGAAGTTCTAAAT------GAGGTAGATGGATATTCTAGTTCTTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATGATCATTTGATATGTAAAAGTGAAAGAGTTCACTGCAAATCAGTAGAGAGTAG T---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACGTAACTGAAAATC TAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA--------------------------------------- ---------------------GAGCATCCTCTCACAAATAAATTAAAGCGTAAAAGGAGAGTTACATCAGGACTTCATCC TGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAAAGATAAATCAGGGAACTAACCAAACAG AGCGGAAT---------GATCAAGTGATGAATATTACTAACAGTGGTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAACAATCCTAACCCAGTAGAA------TCACTGGAAAAAGAATCA---TTCAAAAGTAAAGCTGAACCTAT AAGCAGTAGTATAAGCAATATGGAATTAGAATTGAATGTCCACAATTCCAAAGCATCTAAAAAGAATAGGCTGAGAAGGA AGTCTTCTACCAGGCATATTCATGAGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATTATACTGAAGTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AATTACAACCAAATGCCAGTCAGGCACAGCAGAAA GCTACAACTCATGGAAGATAAAGAACGTGCAGCTAGAGCCAAAAAGAGTAGCAAGCCAAATGAACAAACAAGTAAAAGAC ATGCCAGTGATACTTTCCCAGAACTGAGGTTAACAAACATACCTGGTTCTTTTACTAACTGTTCAAATACTAATGAATTT AAAGAATTTGTCAATCCTAGCCTTCCAAGAGAACAAACAGAAGAGAAA---CTAGAAACAGTTAAACTGTCTAATAATGC CAAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGT---GTTTTGCAA---ATTGAAAGATCTGTAGAGAGTAGCAGTA TTTTGTTGATACCTGGTACTGATTATGGCACTCAGGAAAGTATCTCATTACTGGAAGTTAGCACTCTGGGG---AAGGCA AAA---ACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCATGGTTGT---TC TAAAGATACTAGAAATGGCACAGAAGGCTTGAAGTATCCATTGGGACCTGAAGTTAAC---TACAGTCAGGAAACAAGCA TAGATATGAGAGAAAGTGAACTTGATACTCAATATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTG TTTTCAAATCCAGGAAATCCAGAAAAGGAATGTGCAACATTCTCTGCCTGCTCTAGGTCCTTAAAGAAACAAAGTCCAAA GGTCACTCCTGAATGTGAACAAAAGGAAGAAAATCAAGGAGAGAAAGAGTCTAATATCGAGCTTGTAGAGACAGTTAATA CCACTGCAGGCTTTCCTATGGTTTGTCAGAAAGATAAG---CCAGTTGATTATGCCAGATGT---ATCGAAGGAGGCTCT AGGCTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTATTCCAAATAAACATGGACTTTTACAGAA CCCATATCATATGTCACCGCTTATTCCCACCAGGTCATTTGTTAAAACTAAATGTAAGAAAAAC---CTGCTAGAAGAAA ACTCTGAGGAACATTCAATGTCACCTGAAAGAGCAATGGGAAACAAGAACATCATTCCAAGTACAGTGAGCACAATTAGC CATAATAAC---AGAGAAAATGCTTTTAAAGAAACCAGCTCAAGCAGTATTTATGAAGTAGGTTCCAGTACTAATGAAGC AGGTTCTAGTACTAATGAAGTAGGCTCCAGTATTAATGAAGTAGGTTCCAGTGATGAAAACATTCAAGCAGAGCTAGGTA GAAACAGAAGGCCAAAATTGAATGCTATGCTTAGATTAGGGCTTCTGCAACCTGAGATTTGTAAGCAAAGTCTT---CCT ATAAGTGATTGTAAACATCCTGAAATTAAAAAGCAAGAACATGAAGAA---GTAGTTCAGACTGTTAATACAGACGTCTC TCTATGTCTGATTTCATATAACCTAGAACAG---CATATGGGAAGCAGTCATACATCTCAGGTTTGTTCTGAGACACCTG ACAACCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTGAATATGGCATTAAGGAGACTTCTACTGTTTTT AGCAAAAGTGTCCAGAGAGGAGAGCTCAGCAGGAGCCCTAGCCCTTTCACCCAT---ACACATTTGGCTCAGGTTTACCA AAGAGGGGCCAAGAAATTAGAGTCCTCGGAAGAGAATTTATCTAGTGAG >Rhesus TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAAC---AGTTTGTTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTGGCAAGGAGCCAACATAACAGATGGACTGGAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTAGATCTGAATGCTAATGCCCTGTATGAGAGAAAAGAA TGGAATAAGCAAAAACTGCCATGCTCTGAGAATCCTAGAGAC---ACTGAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAAGTTCTGATGACTCACATGATGGGGGGTCTG AATCAAATGCCAAAGTAGCTGATGTATTGGACGTTCTAAAT------GAGGTAGATGAATATTCTGGTTCTTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATGAGCCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAGTTCAGTAGAGAGTAA T---ATTAAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAACCTTCCCAATTTAAGCCATGTAACTGAAAATC TAATTATAGGAGCACTTGTTACTGAGTCACAGATAATGCAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAACTACATCAGGTCTTCATCC TGAGGATTTTATAAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAATAATAAATCAGGGAACTAACCAAATGG AGCAGAAT---------GGTCAAGTGATGAATATTACTAATAGTGCTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAAAAATCCTAACCCAATAGAA------TCACTGGAAGAAGAATCTGCTTTCAAAACTAAAGCTGAACCTAT AAGCAGCAGTATAAACAATATGGAACTAGAATTAAATATCCACAATTCAAAAGCACCTAAAAAAAATAGGCTGAGGAGGA AGTCTTCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAACTGTACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AATTACAACCAAATGCCAGTCAGGCACAGCAGAAA CCTACAACTCATGGAAGATAAAGAATCTGCAACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGAC ATGCCAGTGATACTTTCCCAGAACTGAAGTTAACAAAGGTACCTGGTTCTTTTACTAACTGTTCAAATACTAGTGAA--- AAAGAATTTGTCAATCCTAGCCTTTCAAGAGAAGAAAAAGAAGAGAAA---CTAGAAACAGTTAAAGTGTCTAATAATGC CAAAGACCCCAAAGATCTCATCTTAAGTGGAGAAAGG---GTTTTACAA---ACTGAAAGATCTGTAGAGAGTAGCAGTA TTTCATTGGTACCTGGTACCGATTATGGCACTCAGGAAAGTATCTCATTACTGGAAGTTAGCACTCTAGGG---AAGGCA AAA---ACAGAACGAAATAAATGTATGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCATGGTTGT---TC TGAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGAAGTGAAGTTAAC---CACAGTCAGGAAACAAGCA TAGAAATAGAAGAAAGTGAACTTGATACTCAGTATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCCTTTGCTCTG TTTTCAAATCCAGGAAATCCAGAAGAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAA AGTTACTTCTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAAACAGTCTAATATCAAGCCTGTACAGACAGTTAATA TCACTGCAGGCTTTTCTGTGGTTTGTCAGAAAGATAAG---CCAGTTGATAATGCCAAATGTAGTATCAAAGGAGGCTCT AGGTTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTGTTACAAAA CCCATACCATATACCACCACTTTTTCCTGTCAAGTCATTTGTTAAAACTAAATGTAACAAAAAC---CTGCTAGAGGAAA ACTCTGAGGAACATTCAGTGTCACCTGAAAGAGCAGTGGGAAACAAGAACATCATTCCAAGTACAGTGAGCACAATTAGC CATAATAACATTAGAGAAAATGCTTTTAAAGAAGCCAGCTCGAGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGT GGGCTCCAGTATTAATGAAGTAGGTTCCAGT---------------------GATGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGGCCAAAATTGAATGCTGTGCTTAGATTAGGGCTTTTGCAACCTGAGGTCTGTAAACAAAGTCTT---CCT ATAAGTAATTGTAAGCATCCTGAAATAAAAAAGCAAGAACATGAAGAA---TTAGTTCAGACTGTTAATACAGACTTCTC TCCATGTCTGATTTCAGATAACCTAGAACAG---CCTATGGGAAGTAGTCATGCGTCTGAGGTTTGTTCTGAGACTCCTG ATGATCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTGAAAATGACATTAAGGAGAGTTCTGCTGTTTTT AGCAAAAGCATCCAGAGAGGAGAGCTCAGCAGGAGCCCTAGCCCTTTCACCCAT---ACACATTTAGCTCAGGGTTACCG AAAAGAGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Orangutan TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAAAAAAAGGTAGACCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAA TGGAATAAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGAT---ACTGAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGACGAACTGTTAGGTTCTGATGACTCACATGATGGGAGGTCTG AATCAAATGCCAAAGTAGCGGATGTATTGGACGTTCTAAAT------GAGGTAGATGAATATTCTGGTTCTTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATGAGGCTTTAATTTGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGTAA T---ATTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAACTTAAGCCATGTAACTGAAAATC TAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAATGATAAATCAGGGAACTAACCAAATGG AGCAGAAT---------GGTCAAGTGATGAATATTACTAATAGTGGTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAAAAATCCTAACCCAATAGAA------TCACTCGAAAAAGAATCTGCTTTCAAAACAAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTCGAATTAAATATCCATAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGA AGTCTTCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATTGTACTGAATTGCAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AAATACAACCAAATGCCAGTCAGGCACAGCAGAAA CCTACAACTCATGGAAGATAAAGAACCTGCAACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGAC ATGACAGCGATACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACCAGTGAGCTT AAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAA---CTAGGAACAGTTAAAGTGTCTAATAATGC CAAAGACCCCAAAGATCTCATGTTAAGTGGAGGAAGG---GTTTTGCAA---ACTGAAAGATCTGTAGAGAGTAGCAGTA TTTCATTGGTACCTGGTACTGATTATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGG---AAGGCA AAA---ACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCATGGTTGT---TT CAAAGATACTAGAAATGACACAGAAGGGTTTAAGTATCCATTGGGACATGAAGTTAAC---CACAGTCAGGAAACAAGCA TAGAAATGGAAGAAAGTGAACTTGATACTCAGTATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTG TTTTCAAATCCAGGAAATCCAGAAGAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAA AGTCACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCTGTACAGACAGCTAATA TCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAG---CCAGTTGATTATGCCAAATGTAGTATCAAAGGAGGCTCT AGGTTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTCACAAAA CCCATATCATATACCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAAC---CTGCTAGAGGAAA ACTCTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAACGAGAAC---ATTCCAAGTACAGTGAGCATAATTAGC CGTAATAACATTAGAGAAAATGTTTTTAAAGAAGCCAGCTCAAGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGT GGGCTCCAGTATTAATGAAGTAGGTTCCAGT---------------------GATGAAAACATTCAAGCAGAACTAGGTA GAAGCAGAGGGCCAAAATTGAATGCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTTTT---CCT GGAAGTAATGGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAA---GTACTTCAGACTGTTAATACAGACTTCTC TCCATGTCTGATTTCAGATAACCTAGAACAG---CCTATGAGAAGTAGTCATGCATCTCAGGTTTGTTCTGAGACACCTA ATGACCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTT AGCAAAAGCGTCCAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCAT---ACACATTTGGCTCAGGGTTACCG AAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Gorilla TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAACAAACAGCCTGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAA CATGTAATGAT---AGGCGGACTCCCAGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAACGAA TGGAATAAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGAT---ACTGAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGTTCTGATGACTCACATGATGGGGGGTCTG AATCAAATGCCAAAGTAGCTGATGTATTGGACGTTCTAAAT------GAGGTAGATGAATATTCTGGTTCTTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGTAA T---ATTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAGCTTAAGCCATGTAACTGAAAATC TAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAATGATAAATCAGGGAACTAACCAAATGG AGCAGAAT---------GGTCAAGTGATGAATATTACTAATAGTGGTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAAAAATCCTAACCCAATAGAA------TCACTAGAAAAAGAATCTGCTTTCAAAACGAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTCGAATTAAATATCCACAATTCAAAAGCGCCTAAAAAGAATAGGCTGAGGAGGA AGTCTTCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATTGTACTGAATTGCAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AAGTACAACCAAATGCCAGTCAGGCACAGCAGAAA CCTACAGCTCATGGAAGATAAAGAACCTGCAACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGAC ATGACAGCGATACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACCAGTGAACTT AAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAA---CTAGAAACAGTTAAAGTGTCTAATAATGC CGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG---GTTTTGCAA---ACTGAAAGATCTGTAGAGAGTAGCAGTA TTTCATTGGTACCTGGTACTGATTATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGG---AAGGCA AAA---ACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCATGGTTGT---TC CAAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAAGTTAAC---CACAGTCGGGAAACAAGCA TAGAAATGGAAGAAAGTGAACTTGATGCTCAGTATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTG TTTTCAAATCCAGGAAATCCAGAAGAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAA AGTCACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCTGTACAGACAGTTAATA TCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAG---CCAGTTGATTATGCCAAATGTAGTATCAAAGGAGGCTCT AGGTTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAA CCCATATCATATACCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAAC---CTGCTAGAGGAAA ACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAAC---ATTCCAAGTACAGTGAGCACAATTAGC CGTAATAACATTAGAGAAAATGTTTTTAAAGAAGCCAGCTCAAGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGT GGGCTCCAGTATTAATGAAGTAGGTTCCAGT---------------------GATGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGGCCAAAATTGAATGCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTT---CCT GGAAGTAATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAA---GTAGTTCAGACTGTTAATACAGATTTCTC TCCATGTCTGATTTCAGATAACTTAGAACAG---CCTATGGGAAGTAGTCATGCATCTCAGGTTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTAAAAATGACATTAAGGAAAGTTCTGCTGTTTTT AGCAAAAGCGTCCAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCAT---ACACATTTGGCTCAGGGTTACCG AAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Human TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAA CATGTAATGAT---AGGCGGACTCCCAGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAA TGGAATAAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGAT---ACTGAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGTTCTGATGACTCACATGATGGGGAGTCTG AATCAAATGCCAAAGTAGCTGATGTATTGGACGTTCTAAAT------GAGGTAGATGAATATTCTGGTTCTTCAGAGAAA ATAGACTTACTGGCCAGTGATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGTAA T---ATTGAAGACAAAATATTTGGGAAAACCTATCGGAAGAAGGCAAGCCTCCCCAACTTAAGCCATGTAACTGAAAATC TAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGACCTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAATGATAAATCAGGGAACTAACCAAACGG AGCAGAAT---------GGTCAAGTGATGAATATTACTAATAGTGGTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAAAAATCCTAACCCAATAGAA------TCACTCGAAAAAGAATCTGCTTTCAAAACGAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTCGAATTAAATATCCACAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGA AGTCTTCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATTGTACTGAATTGCAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AAGTACAACCAAATGCCAGTCAGGCACAGCAGAAA CCTACAACTCATGGAAGGTAAAGAACCTGCAACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGAC ATGACAGCGATACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAAGTGTTCAAATACCAGTGAACTT AAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAA---CTAGAAACAGTTAAAGTGTCTAATAATGC TGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG---GTTTTGCAA---ACTGAAAGATCTGTAGAGAGTAGCAGTA TTTCATTGGTACCTGGTACTGATTATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGG---AAGGCA AAA---ACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCATGGTTGT---TC CAAAGATAATAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAAGTTAAC---CACAGTCGGGAAACAAGCA TAGAAATGGAAGAAAGTGAACTTGATGCTCAGTATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCCG TTTTCAAATCCAGGAAATGCAGAAGAGGAATGTGCAACATTCTCTGCCCACTCTGGGTCCTTAAAGAAACAAAGTCCAAA AGTCACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCTGTACAGACAGTTAATA TCACTGCAGGCTTTCCTGTGGTTGGTCAGAAAGATAAG---CCAGTTGATAATGCCAAATGTAGTATCAAAGGAGGCTCT AGGTTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAA CCCATATCGTATACCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAAT---CTGCTAGAGGAAA ACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAAC---ATTCCAAGTACAGTGAGCACAATTAGC CGTAATAACATTAGAGAAAATGTTTTTAAAGAAGCCAGCTCAAGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGT GGGCTCCAGTATTAATGAAATAGGTTCCAGT---------------------GATGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGGCCAAAATTGAATGCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTT---CCT GGAAGTAATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAA---GTAGTTCAGACTGTTAATACAGATTTCTC TCCATATCTGATTTCAGATAACTTAGAACAG---CCTATGGGAAGTAGTCATGCATCTCAGGTTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTT AGCAAAAGCGTCCAGAAAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCAT---ACACATTTGGCTCAGGGTTACCG AAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Chimpanzee TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAA CATGTAATGAT---AGGCGGACTCCCAGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAA TGGAATAAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGAT---ACTGAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGTTCTGATGACTCACATGATGGGGGGTCTG AATCAAATGCCAAAGTAGCTGATGTATTGGACGTTCTAAAT------GAGGTAGATGAATATTCTGGTTCTTCAGAGAAA ATAGACTTACTGGCCAGCGATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGTAA T---ACTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAACTTAAGCCATGTAACTGAAAATC TAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA--------------------------------------- ---------------------GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT---CAAAAGACTCCTGAAATGATAAATCAGGGAACTAACCAAATGG AGCAGAAT---------GGTCAAGTGATGAATATTACTAATAGTGGTCATGAGAATAAAACAAAAGGTGATTCT---ATT CAGAATGAGAAAAATCCTAACCCAATAGAA------TCACTCGAAAAAGAATCTGCTTTCAAAACGAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTCGAATTAAATATCCACAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGA AGTCTTCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAATTGTACTGAATTGCAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAAAA---AAGTACAACCAAATGCCAGTCAGGCACAGCAGAAA CCTACAACTCATGGAAGATAAAGAACCTGCAACTGGAGTCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGAC ATGACAGCGATACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACCAGTGAACTT AAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAGAAGAAGAGAAA---CTAGAAACAGTTAAAGTGTCTAATAATGC CGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG---GTTTTGCAA---ACTGAAAGATCTGTAGAGAGTAGCAGTA TTTCATTGGTACCTGGTACTGATTATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGG---AAGGCA AAA---ACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCATGGTTGT---TC CAAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAAGTTAAC---CACAGTCGGGAAACAAGCA TAGAAATGGAAGAAAGTGAACTTGATGCTCAGTATTTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTG TTTTCAAATCCAGGAAATCCAGAAGAGGAATGTGCAACATTCTCTGCCCACTGTAGGTCCTTAAAGAAACAAAGTCCAAA AGTCACTTTTGAACGTGAACAAAAGGAACAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCTGTACAGACAGTTAATA TCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAG---CCAGTTGATTATGCCAAATGTAGTATCAAAGGAGGCTCT AGGTTTTGTCTATCATCTCAGTTCAGA---GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAA CCCATATCATATACCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAAC---CTGCTAGAGGAAA ACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAAC---ATTCCAAGTACAGTGAGCACAATTAGC CGTAATAACATTAGAGAAAATGTTTTTAAAGAAGCCAGCTCAAGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGT GGGCTCCAGTATTAATGAAGTAGGTTCCAGT---------------------GATGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGGCCAAAATTGAATGCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTT---CCT GAAAGTAATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAA---GTAGTTCAGACTGTTAATACAGATTTCTC TCCATGTCTGATTTCAGATAACTTAGAACAG---CCTATGGGAAGTAGTCATGCATCTCAGGTTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGGTGAAATAAAGGAAGATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTT AGCAAAAGCGTCCAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCAT---ACACATTTGGCTCAGGGTTACCG AAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Jackrabbit -------------------------------------------------------------------------------- -AAGGCTGAATTCTGTAATAAGAGCAAACAGCCTGGCTTAGCAAGAAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTAGTTCTGAATGTTGACTGCCTGTATGGGAGAAAACAA CAGGATAAGCAGAAACCTCCATGCCCTGAGACCTCTGGAGAT---AACCAAGAT---GTTTCTTGGATAACAGTAAATAG CAGCATTCGGAAAGTTAACGAGTGGTTCTCCAGAAGTAATGAAATGTTAACTCCTGATGACTCACTTGACCGGCGGTCTG AATCAAATGCCAAAGTGGCTGGTGCATTAGAAGTCCCAAAG------GAGGTAGATGGATATTCTGGTTCTACAGAGAAA ATAGACTTACTGGCCAGTGATTCCCATAATGCTTTAATATGTGAAAGCAAAAGAGTCCATTCCAAACCAGTAGAGAATAA T---ATCAAAGATAAAATATTTGGGAAAACCTACCACAGGAAGACAAGCCTCCCTAACTTGAGCCACATAACTGAAGATC TAACTATAGGAGCATTTGCTGCGGAACCACTGGTA--------------------------------------------- ---------------------CCATGTCCCCCCGCAAATAAATTAAAGCGTAAAAGAAGAACTTCTTCAGGCCTTCAACC TGAAGATTTTATCAAGAAGGTAGATTTGGCAGTTGTTCCAAAAACCACTGCACAGATAAATCAGGGAACTGATCAAACGG TGGACAGT---------GATCAGGTGATGAATATTACTAATTGTGGTAATGAGAATGAAACAGAAGGTGACTAT---ATT CAGAAAGAGACAAATGCTAACCCAACAGAA------TCCCTAGAAAAAGACTCTTCCTTCAGAACTAAAGTTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATGTCCATAATTCAAAAAAACCCAAGAAGAATAGGCTGAGGAGGA AGTCCTCTACCAGGCGTGTTCATGCACTTGAACTAGTAGTCAATAAGAAACCGAGCCCACCTAATCATGCTGAACTACAA ATTGACAGTTGTACTAGCAGTGAAGAA---ATG------------AATTTTGACCAAATACCAGTCAGTCACAGCAGACA GGCTCAAGTCATAGAAGATACAGAACCTCCAACTGGAGCTAGGAAG---AACAAGCCAAGTGAACAAATAAGTAAAAGAC ATGCCAGTGATGTTTTCCCAGAAGTCAAATTAACAAACATACCTGGTGTTTTTACAAACTGTTCAAGTTCTAATAAACTT CAAGAATCTGTCGATCCTAGCCTTCAAAGAAGGGAAATAGAAGAGAAC---CTAGACACAGTTCAAGTGCCTAACAGTGC CATAGACCCCAAAGATCTCCTGTTATGTGGAGAAAGG---GGCTTGCAA---ACTGAAAGGTCTGCGGAGAGTACCAGTA TTTCACTGGTACCTGATACTGAATATGGTACCCAGGACAGCATCTCATTACTGGGCGCTAACACCCTTGGG---AAGGCA AAA---ACAGCAGCAAATGGACATGTGAGTGAGAGCACAACAATTGGAAATCCCAAGGAACTTAGCCATGATTGT---TT GAAAGATACTGGAAATGACCCAGACAACTGTAAGGATCCACTGAGAAGTGAAGTTGAC---------CAAGAGACAAGCG TAGAAATGGAAGAGAGTGAGTTTGATACTCAGTATTTACAGAATACATTCATGGGTTCAAAGCGTCGTTCATTTGCTCTG TGTTCAAAACCAGGAGATCCAGAAAAGGAATGTGCAGCAGTCTGTACCCGCTCCAACTCCTCAAGGAAACAAAGTCCAGA AGTCACTCTTGAACGTGAACAAAAAGAA---AGTCAGAGAAAGGAAGAGTGGAAAATCAGTCATGTCCAGGCAGCTGATA GCACTGTGGGCTTTCCTGTGGTGTGTCAGAAAGAAAAG---GCAGGTGATTGTGCCAAATGGAGCACTAAAGAGATCTCT AGGCTTTGTCTGTCATCTCAGTCCAAA---GGCAGTGAAACTGAGCTCATTGCTGTAAGTAAACATGGGATGTCACAAAA CCCATATCATATACCACCAATTTCTCCCATCAAGACATCTGTTAAAGCCACACGCCAGGTACAC---CTGTCAGGGGAAA GGTCTGAGGAGCATTCCGTGTCATCTGAAAGAGCAGTGGGAAGCGAGAGCATCATTCAAAGCACAGTGAGCACAATTAGC CAAGAGTACATTAGAGAAAGTGCTTTGAAAGGATTCAGCTCAAGCAGTATTAATGAAGGGGGCTCTAGTGCTAACGAAGT ATGCTCCAGTGTGAATGAAGTAGGATCCAGT---------------------GGTGAAAACATGCAAGCACAACCAGGCA GAAGCAGAGCACCTGAGTTAAATGCTGTGCTGAGAATAGGTCTTCTGCAGCCTGAAGTCTCTGAGCAAAGCCTT---CCT ATAAGTAATTCTGAACTTCCCAAACTACAAAGGCAAGGAGAAAACGAAGGAGTAGTTCAGGCTGTGAATAGAGATTTCTC TTCGTATCTGGTTCCTGATAGCCAAGAGCAG---TCTATGGGAGGAAGGCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGACCTGTTAGATATTTATGAAATAAAGGAAAATACCAGCTTTGCTGAGAGCGGCATTAAGGAAAGATCTGCTGTGTTT AGTAAAAGTGTTCAGAGGGGAGAGTGCAGTAGAACCCCTAGCCCTTCAGGCCAT---GCATGTTTGGCTCAGAGTCA--- ------------------------------------------------- >FlyingSqu TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGCAGAGCAGATGGGCTAAAAGTAAGGAAA CCTGTAATGAT---AGGCAAATTCCCAGCTCAGAGAAAAAGGTAGATTTGAATGCTGATCCCCAATATGAGAAAAAAGAA CCAAGTAAGCAGAAACATCCATGCTCTGAGAATTCCAGAGAT---ACCCAAGAT---GTTCCTTGGATAACACTAAATAG CAGCATTCGGAAAGTTAACGAGTGGTTTTCCAGAAGTGACGAAATGTTAACTTCTGATGACTCAGATGATGGGGGTTCTG AATCAAATGCTGAAATAGCTGGTATATTAGAAATTCCAAAT------GAAGTAGATGGATTTTCTGGTTCTTCGGAGAAA ATAGACTTGTTGGCCACTGATCCGCATAATGCTTTAATTTCTAAATGTAAAAGAGTCTGCTCCAAAGCAGTCAAGAGTAA T---ATTGAAGATAAAATATTTGGGAAAACCTATCAGAGGAAGGCAAGCCTCCCTAACTTGAGCCATATAACTGAAAATC TAATTATAGGAGCATTTGCCAGAGAACCACAAATAACACAAGAGCTTGCCAGAGAACCACAAATAACACAACAGCTTGCC AGAGAACCACAAATAACACAAGAGCGTCCCCTCACAAATAAATTAAGACGTAAAAGGAGAACTACATCATGCCTTCATCC TGAGGACTTTATCAAGAAAACAGATTTGGCAATTGTTCAAAAGACTCCTGAAAAGATAAATCAGGGAACTGACCAAATGG AACACAAT---------GATCAAGTAATGAATATTACTAATAGTGGTCAAGAGAATGAAACAAAAGTTGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAGTTGAA------TCATTGGAAAAAGAGTCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGTAGTATAAGCAACATGGAACTAGAATTAAATATCCACAATTCAAAAGCACCTAAGAAAAATAGGCTGAGGAGGA AGTCTTCTACTAGGTACATTCATGTGCTTGAACCAGTAGTTAATAGAAATACAAGTCCACCTCATCACACTGAATTGCAA ATTGATAGTTGTACTAGTAGTGAAGAA---ATAAAGACAAGA---AATTCCAACCAAATGTCAGTCAGGCATGGCAAAAA GCTTCAGTTCATGGAAGATGCAGAACCTGCAACTGATGTCAGAAAAAGTAACAAGCCAAATGAACAAGTAAATAAGAGAC ATACCAATGATGCTTTCCCAGGACTGAAGTTAACAGGCATATCTGGTATTTTTACTAACTGCTCAAGTTCTAGTAAAGTT GAAGAATTTATCAATCCTAACCTTCAGAAAGAAGGAACAGAAGAGAAC---ATAGAAATAATTCAAGTGTCTGATAATAC CCAAGACCCCAAAGATACGGTGTTAAGTGGAGAAAGG---GTTTTGCAA---ACTGAGAGATCTGTAGAGAGTACCAGTA TTTCATTGGTACCTGATACTGATTATGGCACTCAGGACAGTATCTCATTACTGGAAGCTAACACCTTTGGA---AAAGCA AGA---ACAGCATCAAATCAACATGTTACTCAGTATGTGGCAATTGAAAATCCCAAAGAATTTGTCCATGGTCAT---TC TAAAGATACTAAAAATGACCCAGAGGGTTTCAAGGATTCATTGAGATGTGAAGTTAAC---CACATTCAAGAGACAAATG TAGAAATGGAAGAAAGTGAACTTGATACTCAGTCTTTAGAGAATACATTCCAAGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCAGGAAATCTAGAAAAGGAATGTGCAGCCAACTATACCTCTTCCAAGTCCTTACGGAAACAAAGCCCAGA CATGACTCTTGAATGTGAGCAAGAAGAAGAAAATTGGGGAAAGAAAGAGTCTAAAATTAGGCATGTACAGGCAATTAATG CTACT------------ATGGTTTGTCAGAAAAGTAAG---CCAGATGATGATGCCCAATGTAGTGTTACAGAAGTCTCT AGAATTTTTCCATCATTTCAGTTCAGA---GACAATCTAACTAAACTCATAACTGCAGATAAACATGGAATTTCACAAAA CCCATATCATATGCCATCCATTTCTCCCAGCAGGTCATCTGTTAACACTAAATGTAGGAGAAAC---CTATCAGAAGAAA AGTTTGAGAAAGATTCAAGGTCACCTAAAGAAGCGGTGGGATATAAGAGAATCATTCAAAGTACAGTGAGCACAATTAGC CAAAATAACATTAGACAAAGTGCCTTTAAAGAAGCGAACTCAGGCAGTATTAATGAAGTAGGCTCTAGTACTAATGAAGT AGGCTCCAGTATTAATGAAGTAGGTTCCAGT---------------------GGTGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGGACCCAAATTAAATGCTGTGCTTAGATTAGGTCTTATGCAACCTGAAGCCTGTAAACAAAATCTT------ CTAAGTAATTGTAAATACCCTGAAATAAAAAGACAAGGAGAAGATGAA---GTAGTTCAAGTTGTTAATGCAGATTTCTC TCCATGTCTAATTTCAGATGACTTAGAACAA---CCTATGGGAAATAGTCATGTCACTCAGGTTTGTTCTGAGACTCCTG ATGACCTGTTAGATGATGATGAAATACAGGAAAATACCAGCTTTGCTGAAGGTGGTATTAAGGAAAGATCTGCTGTTTTT AGTAAGAGTGTCCAAAGAGGAGAGTTTAGCAGGAGCCCGAGTCCTTTATCCCAT---ACGTCTTTGGCTCGGAGTCATCA AAAAGGGGCCAGGAAATTAGAGTCCTCAGAAGAGAGCATCTGTAGTGAG >OldWorld -------------------------------------------------------------------------------- ----------------------------------------------AGCCAACAGAGCAGGTGGGCTGAGAGCAAGGAGA GGTGCCATGAC---AGGCAGGCTCCTGGCACAGAGCAGAAGGTAGAGCTGACTGCTGAGCCCCTCCACGAGAGAAAAAGA CGGAAAAAGCAGAACCCTCCGAGCTCCGAGGCTCATGGAGGG---ACCCAGGAT---GTTCCTTGGATCACACTAAATAG CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGACAAAGCAGTGACTTCTGACAGCACCTGTGACAGGGAGTCCA AGTCAGATGCTGAAGAAGCTGGTGTGGCAGAAGATCCAAAA------GGCCTAGAT---------GGCTCTTCAGAGAAA GTAGGCTTGCTGACCAGCAGTCCTCAAAAAGCTATAATCTGTGCCAGGGAGAGGGTCTGCTCCAAAGCACTGAAGAGTGA C---ATGGAAGATAAAGTATTTGGGAAAACCTATCAGAGGAGGGTGAGCCTCCGCAGCTCAAGCCACGGAGCTGAAAGTC TGACCGTGGGAGCGTTTGTTAGAGAGCCGCAGCTGACACTG--------------------------------------- ---------------------GAGCGCTCCTTCACAAATAAAATAAAGCGCAAGAGGAGAACCACATCGTGCCTTCATCC TGAGGACTTTGTCAAGAAAGCAGATTTGACAGTGGCTCAAAAGACTCCTGAAAAGGTAAATCAGGGAACGAAGCAAATGG AGGAGAGT---------AGTCAAGTGACAAATACTACTAATAACAGTCATGAGAATGAGACAAAAATGGGCAAT---GTT CAG------AGAAACCCTAACCCAGTACTA------TCAGTGGAAAAAGAGTCTGCTTCTGGTACTAACGCAGAGCCCTC GAGCAGCAGCATAAACAACAGGGAACTAGAATTAAACACCCCTCTTGCAAAAGAACCTAAGGAAGACAGGCTGAGGGGGA CGTCCTCAAGCAGACACAGACCT------------------------AATCCACACCTGCTGGATCACACAGAGCTGCAA AGTGGCAATTCTATCAGCAGTGAAGAA---ATAAAGGAAAAA---AGCTCCCCCCAAATGCCCATCAGGCACAGCAGGAC GCTGCACCTCACAGGGGCTGTGGAAGCTTCTATTGGAGCCAGGGAGAGTAAGAAGCCAGCTGGACAAGTAAGGGAGAGAC ATGCCGGGGACTCTTTCCTAGAACCAAGATTAGCAAGACTACCTGCT---TTTACTAACAGTTCAAGCCCTGATAACCTT AAAGAATTTGTCAACCTTAGTGCACAGACAGAAGAGATGGAAGAGAAC---CCAGAAACAGTACAAGTGTCCGAAAGTAC CAGAGACTCCAAAGGTCCTGTGTTAAGTGGGGAAAAG---GGAGTGCAA---ACCGAGAGGTCTATGGAGAGCACTAGCA TTTCACTAGTCCCTGACACTGACTGTGGCACTCAGGACAGTGTCTCCTTACTGGAAGCCGACAGCCTCAGG---AAGGCA CGG---AGAGCATCGCATCAGTGTATGGCTCAATATGTGGCGGTTGCGAAGCCCAAGGAACTTCTGCCTGCTTGT---TC TGAAGACACTGGAAACGGCACAGACAGCTTAAATGATCCATTGAGATGTGGAGGGAGC---CACATCCAGGAGGCAAATA TAGAAATGGAAGATAGTGAACTCGACACTCAGTATTTACAGAATACATTCCAGGCTTCAAAGCGTCAGTCATTTGCTCTC TTTTCAAATCCAGGAAACTCAAAAAAGGAATCAACAACAGTCTGTGCCCACTCTGAGTCCTTTAAGAAACAAAATCCAGA AGTCATTCCTGAATGTGAACAAACAGAAGAAAATTGGGGCAAGAAAGTGCCTAAAATTAGTTGTGTGCAAGAGAGCGCC- --------------CCTCTGGTTTCTCAGAGGGATCAG---CCAGGCACCAGCATCATATGTAGCGGCACAGGAGTCTCG AGGCTCTGTCTCTCGTCTTGGTTCACA---GGCAGCAAAACTGAACTCGTCACAGCTGACAAACATGGAATTTCACAAAA TCCATATCACATGCCATCAATTTCTCCCATCAGGCCATTTGTTAAAACTCCATGTAAGAAAACC---------------- -----------CGTTCCTCATCACCTGGAGAAGCCACAGGTAACCAGATCATCCTTCAGAGCACC--------------- ------------AGCCATCGCGCTTGCAGAGAAGCCAGCTTGGGCAGTGGGAACGAAGGGGGCTCCAGT----------- ----------------------------------------------------GGGGAGCACATTCAAGCAGAACCCAGTA GACACCAAGAGCCTGAACTA------------AGATTAGGTCTGACGCAGCCCGAAGTCTACCAGCAAAGTCTT---CCT GTAGGTGACTGTAGACATCCCGAAATACAAACACGAGGAGAAAATGGAGTGGTAGCTCAGGCTGTCCATGCAGATTTCTC TCCGTGTCTAATTTTAGATAACGTGGAACAG---CCTATGGGAAATAATCCTGCTTCTCAGATCTGTTCTGAGACGCCCG ATGACCTGTTAGATGATGAGAACAAAAAGGAAGATGCCAGCTTTGCCGAAGGTGGCATTAAGGAAACTTCTGCCATTTTT AGCAAGAGTGTCCAGACAAGACGATTCAGCAGGAGCCCCAGCCCTGTAACCAAT---ACCACTTTGGCTCAGGGTCACCG AAGAAGGGCGAGAAAACTCGAGTCTTCTGAGGAGAGCATGTCAAGTGAG >Mouse TGTGGCACAGATGCTCATGCCAGCTCATTACAGCCTGAGACCAGCAGTTTATTGCTCATTGAAGACAGAATGAATGCAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCATAGCAGTGAGCCAGCAGAGCAGATGGGCTGCAAGTAAAGGAA CATGTAACGAC---AGGCAGGTTCCCAGCACTGGGGAAAAGGTAGGTCCAAACGCTGACTCCCTTAGTGATAGAGAGAAG TGGACTCACCCGCAAAGTCTGTGCCCTGAGAATTCTGGAGCT---ACCACCGAT---GTTCCTTGGATAACACTAAATAG CAGCGTTCAGAAAGTTAATGAGTGGTTTTCCAGAACTGGTGAAATGTTAACTTCTGACAGCGCATCTGCCAGGAGGCACG AGTCAAATGCTGAAGCAGCTGTTGTGTTGGAAGTTTCAAAC------GAAGTGGATGGGGGTTTTAGTTCTTCAAGGAAA ACAGACTTAGTAACCCCCGACCCCCATCATACTTTAATGTGTAAAAGTGGAAGAGACTTCTCCAAACCAGTAGAGGATAA T---ATCAGTGATAAAATATTTGGGAAATCCTATCAGAGAAAGGGAAGCCGCCCTCACCTGAACCATGTGACTGAA---- --ATTATAGGCACATTTATTACAGAACCACAGATAACACAA--------------------------------------- ---------------------GAGCAGCCCTTCACAAATAAATTAAAACGTAAGAGA------AGTACATCCCTTCAACC TGAGGACTTCATCAAGAAAGCAGATTCAGCAGGTGTTCAAAGGACTCCTGACAACATAAATCAGGGAACTGACCTAATGG AGCCAAAT---------GAGCAAGCAGTGAGTACTACCAGTAACTGTCAGGAGAACAAAATAGCAGGTAGTAAT---CTC CAGAAAGAGAAAAGCGCTCATCCAACTGAA------TCATTGAGAAAGGAACCTGCTTCCACAGCAGGAGCCAAATCTAT AAGCAACAGTGTAAGTGATTTGGAGGTAGAATTAAACGTCCACAGTTCAAAAGCACCTAAGAAAAATAGGCTGAGGAGGA AGTCTTCTATCAGGTGTGCTCTTCCACTTGAACCA---ATCAGTAGAAATCCAAGCCCACCTACTTGTGCTGAGCTTCAA ATCGATAGTTGTGGTAGCAGTGAAGAA---ACAAAGAAAAAC---CATTCCAACCAACAGCCAGCCGGGCACCTTAGAGA GCCTCAACTCATCGAAGACACTGAACCTGCAGCGGATGCCAAGAAG---AACGAGCCAAATGAACACATAAGGAAGAGAC GTGCCAGCGATGCTTTCCCAGAAGAGAAATTAATGAACAAAGCTGGTTTATTAACTAGCTGTTCAAGTCCTAGAAAATCT CAAGGGCCTGTCAATCCCAGCCCTCAGAGAACAGGAACA---GAGCAA---CTTGAAACACGCCAAATGTCTGACAGTGC CAAAGAACTCGGGGATCGGGTCCTAGGAGGAGAGCCC---AGTGGCAAAACCACTGACCGATCTGAGGAGAGCACCAGCG TATCCTTGGTACCTGACACTGACTACGACACTCAGAACAGTGTCTCAGTCCTGGACGCTCACACTGTCAGA---TATGCA AGA---ACAGGATCCGCTCAGTGTATGACTCAGTTTGTAGCAAGCGAAAACCCCAAGGAACTCGTCCATGGC------TC TAACAATGCTGGGAGTGGCACAGAGGGTCTCAAGCCCCCCTTGAGACACGCGCTTAAC---CTCAGTCAGGAGAAA---G TAGAAATGGAAGACAGTGAACTTGATACTCAGTATTTGCAGAATACATTTCAAGTTTCAAAGCGTCAGTCATTTGCTTTA TTTTCAAAACCTAGAAGTCCCCAAAAGGACTGTGCT------------CACTCTGTGCCCTCAAAGGAACTGAGTCCAAA GGTGACAGCTAAAGGTAAACAAAAAGAA---CGTCAGGGACAGGAAGAATTTGAAATCAGTCACGTACAAGCAGTTGCGG CCACAGTGGGCTTACCTGTGCCCTGTCAAGAAGGTAAG---CTAGCTGCTGATACAATGTGT------GATAGAGGTTGT AGGCTTTGTCCATCATCTCATTACAGA---AGCGGGGAGAATGGACTCAGCGCCACAGGTAAATCAGGAATTTCACAAAA CTCACATTTTAAACAATCAGTTTCTCCCATCAGGTCATCTATAAAAACTGACAATAGGAAACCT---CTGACAGAGGGAC GATTTGAGAGACATACATCATCAACTGAGATGGCGGTGGGAAATGAGAACATTCTTCAGAGTACAGTGCACACAGTTAGC CTGAATAAC---AGAGGAAATGCTTGTCAAGAAGCCGGCTCGGGCAGTATTCATGAAGTATGTTCCACT----------- ----------------------------------------------------GGTGACTCCTTCCCAGGACAACTAGGTA GAAACAGAGGGCCTAAGGTGAACACTGTGCCTCCATTAGATAGTATGCAGCCTGGTGTCTGTCAGCAAAGTGTT---CCT GTAAGTGAT---AAGTATCTTGAAATAAAAAAGCAGGAG---------------GGTGAGGCTGTCTGTGCAGACTTCTC TCCATGTCTATTCTCAGACCATCTTGAGCAA---TCTATG---AGTGGTAAGGTTTTTCAGGTTTGCTCTGAGACACCTG ATGACCTGCTGGATGATGTTGAAATACAGGGACATACTAGCTTTGGTGAAGGTGACATAATGGAGAGATCTGCTGTCTTT AACGGAAGCATCCTGAGAAGGGAGTCCAGTAGGAGCCCTAGTCCTGTAACCCAT---GCATCGAAGTCTCAGAGTCTCCA CAGAGCGTCTAGGAAATTAGAATCGTCAGAAGAGAGCGACTCCACTGAG >Rat TGTGGCACAGATGCTCGTGCCAGCTCATTACAGCGTGGGACCCGCAGTTTATTGTTCACTGAGGACAGACTGGATGCAGA AAAGGCTGAATTCTGTGATAGAAGCAAACAGTCTGGCGCAGCAGTGAGCCAGCAGAGCAGATGGGCTGACAGTAAAGAAA CATGTAATGGC---AGGCCGGTTCCCCGCACTGAGGGAAAGGCAGATCCAAATGTGGATTCCCTCTGTGGTAGAAAGCAG TGGAATCATCCGAAAAGCCTGTGCCCTGAGAATTCTGGAGCT---ACCACTGAC---GTTCCTTGGATAACACTGAATAG CAGCATTCAGAAAGTGAATGAGTGGTTTTCCAGAACTGGTGAAATGTTAACTTCTGACAATGCATCTGACAGGAGGCCTG CGTCAAATGCAGAAGCTGCTGTTGTGTTAGAAGTTTCAAAT------GAAGTGGATGGATGTTTCAGTTCTTCAAAGAAA ATAGACTTAGTTGCCCCTGATCCCGATAATGCTGTAATGTGTACAAGTGGAAGAGACTTCTCCAAGCCAGTAGAGAATAT T---ATCAACGATAAAATATTTGGGAAAACCTATCAGAGAAAGGGAAGCCGCCCTCACTTGAACCATGTGACTGAA---- --ATTATAGGCACATTTACTACAGAACCACAGATTATACAA--------------------------------------- ---------------------GAGCAGCCCTTCACAAATAAATTAAAACGCAAAAGA------AGTACATGCCTTCATCC TGAGGACTTTATCAAGAAAGCAGATTTAACAGTTGTTCAAAGGATTTCTGAAAATTTAAATCAGGGAACTGACCAAATGG AGCCAAAT---------GACCAAGCAATGAGTATTACCAGTAACGGTCAGGAGAACAGAGCAACAGGTAATGAT---CTT CAGAGAGGGAGAAATGCTCATCCAATAGAA------TCATTGAGAAAGGAACCTGCTTTCACAGCTAAAGCCAAATCTAT AAGCAACAGTATAAGTGATTTGGAGGTAGAATTAAATGTTCACAGTTCAAAAGCACCTAAGAAAAATAGGCTGAGGAGGA AGTCT---ACCAGGTGTGTTCTTCCACTCGAACCA---ATCAGTAGAAATCCGAGCCCACCTACTTGTGCTGAACTTCAG ATCGAGAGTTGTGGTAGCAGTGAAGAA---ACAAAGAAAAAC---AATTCCAACCAAACCCCAGCCGGGCACATTAGAGA GCCTCAACTCATCGAAGACACAGAACCCGCAGCTGATGCCAAGAAG---AACGAGCCAAATGAACACATAAGGAAGAGAA GTGCCAGTGATGCGTTCCCAGAAGAGAAATTAATGAACAAAGCTGGTTTATTAACTAGCTGTTCAAGTCCTAGAAAGCCT CAAGGACCTGTCAATCCTAGCCCTGAGAGAAAAGGAATA---GAGCAA---CTTGAAATGTGCCAGATGCCTGATAATAA CAAAGAACTCGGGGATTTGGTCCTGGGAGGAGAGCCC---AGTGGGAAACCTACTGAACCATCTGAGGAGAGCACCAGTG TGTCCTTGGTACCCGACACAGACTACGACACCCAGAACAGTGTCTCAATACTGGAAGCGAACACTGTCAGA---TATGCA AGA---ACAGGATCAGTTCAGTGTATGACTCAGTTTGTCGCAAGTGAAAACCCCAAGGAACTTGTCCATGGT------TC TAACAATGCTGGAAGTGGCTCGGAGTGCTTCAAGCACCCATTGAGACATGAACTTAAC---CACAATCAAGAGACA---A TAGAAATGGAAGACAGTGAACTTGATACTCAGTATTTGCAGAATACATTTCAAGTTTCAAAGCGTCAGTCATTTGCTTTA TTTTCAAAACTTAGAAGTCCCCAAAAGGACTGTACTCTGGTAGGTGCCCGCTCTGTGCCCTCAAGGGAACCAAGTCCAAA GGTGACTTCTAGAGGTGAACAAAAAGAA---CGTCAGGGACAAGAAGAGTCTGAAATCAGTCATGTACAGGCAGTCACAG TCACAGTAGGCTTACCTGTGCCCTGTCAGGAAGGTAAG---CCAGGTGCTGTTACAATGTGT------GCTGATGTTTCT AGGCTTTGTCCGTCATCTCATTATAGA---AGCTGTGAGAATGGACTCAACACCACAGATAAATCTGGAATTTCACAAAA CTCACATTTTAGACAATCAGTTTCTCCCCTCAGGTCATCTATAAAAACTGACAATAGAAAAACT---CTGACAGAGGGNC GATTTGAGAAACAT---------ACTGAAAGGGGRATGGGAAATGAGACTGCTGTTCAAAGTACAATACACACAATTAGT CTAAATAAC---AGAGGAGATGCTTGTCTAGAAGCCAGCTCAGGCAGTGTTATTGAAGTACATTCCACT----------- ----------------------------------------------------GGTGAAAACGTCCAGGGGCAACTAGATA GAAACAGAGGGCCTAAGGTAAACACCGTGTCTCTATTAGATAGTACACAGCCTGGTGTCTCTAAGCAGAGTGCT---CCT GTAAGTGAT---AAGTATCTTGAAATA---AAGCAGGAG---------------AGTAAGGCTGTCAGTGCAGACTTCTC TCCATGTCTGTTCTCAGATCATCTTGAAAAA---CCTATGAGAAGTGATAAGACTTTTCAGGTTTGCTCTGAGACACCTG ATGACCTGTTGGATGATGTTGAAATACAGGAAAATGCTAGCTTCGGTGAAGGTGGCATAACGGAAAAGTCTGCTATTTTT AATGGAAGTGTCCTGAGAAGAGAGTCCAGTAGGAGCCCTAGCCCTGTAACCCAT---GCATCGAAGTCGCGGAGTCTCCA CAGAGGGTCTAGGAAATTAGAATTCTCAGAAGAGAGCGACTCCACTGAG >NineBande TGTGGCACAAATACTCATGCCAACTTATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGCGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAGCATAGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGTATGGGCGAAAAGAA CTGAATAAGCAGAAACCTCCATGCTCTGAGAGTCATAGAGAT---ACCCAAGAT---ATTCCTTGGATAATGCTGAATAG TAGCATTCAGAAAGTTAACGAGTGGTTTTCCAGAGGTGATGACATATTAACTTCTGATGACTCACACGATAGGGGGTCTG AATTAAATGCAGAAGTAGCTGGTGCATTGAAAGTTTCAAAA------GAAGTAGATGAATATTCTAGTTTTTCAGAGAAG ATAGACTTAATGGCCATTAATCCTCATGATACTTTACAATTTGCAAGTGAAAGAGTCCAATTGAAACCAGCAGAGAGTAA C---ATCAAAGATAAAATATTTGGGAAAACCTATCATAGGAAGGCAAGCCTCCCTAACTTGAGCCACATAACCCGAAACC TTTTTATAGGAGCTATTGCTGCAGAGCCCAAGATAACACAA--------------------------------------- ---------------------GAGCATTCCCTCCAAAATAAAATAAAGCGTAAAAGGAGAACTGCATCAGGCCTTCGTCC TGAGGATTTATCCAAGAAAGTAGATTTGACAGTTGTTCAAAAAACCCCTGAAAAGATAAATCAGGGAACTGACCAAATGG AGCAGAAT---------GATCCAGTGATGAATATTGCTAATAGTGGTCATGAGAATGAAACAAAAGGTGATTGT---GTT CAGAAAGAGAAAAATGCTAATCCGACAGAA------TCATTGGGAAAAGAATCTGCTTTCAGAACTAAAGGCGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATTTTAAATTCAAAAGCATCTAAGAAGAATAGGCCGAAGAGGA TGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTCGGCAGTAGAAATCCAAGCCCACCTAATCATACTGAACTACAA ATTGATAGTTGTTCTAGCATTGAAGAG---ATAGAGAAAATA---AATTCTAACCAAAAGCCAATCAGACACAACAGAAT GCTTCAACTCACGAAAGAAAAAGAAACCACAACTGGAGCCAAAAAGAACAATAAGCCAAATGAACAAATAAGTGAAAGAC ATGCCAGTGATGCTTTCCTAGAACTTAAA------AATGTAACTGATTTTCTTCCTAAATGTTCAAGTTCTGATAAACTT CAAAAATTT---AATTCTAGCCTGCAAGGAGAAGTAGCA---GAGAAC---CTAGAAACAATTCAAGTGTCTGATAGTAC CAGGGACCCTGAAGATCTGGTGGTAAGTGGAGAAAAG---TGTTTGCAA---ACTGAAAGATCTGCAGAGAGTACCGGTA TTTCAGTGGTACCTGATACTGATTATGGCACTCAAGACAGTATCTCATTACTGGAAGCTGACACCCTGGGG---AAGGCA AAA---ACAGCACTAAATCAACATGTGAGTCAGTATGTAGCAATTAGAAATGCCACTGAACTTTCCCATGGTTGT---TC TAAAGACACTAGAAATGACACTGAAGATTTTAAGGATTCATTGAGACATGAAGTTAAC---CACACTCAGGGGACAAATG TTGAAATAGAAGAGAGTGAACTTGATACTCAGTATTTGCAGAATACATTCAAGATTTCAAAGCGCCAGTCATTTGCTCTG TTTTCGAATCCA---------GAAAATGAATGTGCAACAGTCTGTGCCCACTCCAGGTTCTTAGGGAAACAAAGTCCAAA AGTCACCTTTGAATGTAGACATAAAGAAGAAAATCAGGGGAAGAAAGAGTCTAAAATCAAACATGTGCAGGTAATTCACA CAACTGCAGGCTTTCCTATAGTTTGTCAGAAAGATAAG---CCAGGTGATTATGCCAAAGGTAGCATTCAAGGAGTCTCT AGGCTTTGTCAGTCCTCTCAGGCCAGA---GGCAATGAATCTGAACTCATTAATTCAAATGAACATGAAATTTCACAAAA CCCAGATCAAATGCCATCACTTTCTCACATGAAGTCATCTGTTAAAACTAAATGTAAGGAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACTTACAGTGTCACTTGAAAGAACAATGGTAAATGAGAACATCATTCAAAGTACAGTAAGCACAATTAGC CACAGTAACATTAGAGAAAACACTTTTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGGTCCAGT----------- ----------------------------------------------------GATGAGAACATTCAAGCAGAAGTAGGTA GAAACAGAGCACCTAAATTAAATGCTATGCTCAGATTAGGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT ATAACCAATTGTAAATATCCTGAAATAAAAAGTCAAGGAGAAAATGAAGAAGCAATTCGGGCTGTTGATATAGACTTCTC TCCATGTCTAATTTCAGATAACCTACAACTA---CCTATGGGAAATAGTTGTGCTTCCCAGATTTGTTCTGAGACACCTG ATGACTTGTTAGATGATGATGAAATAAAGGAAAATAACTGCTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTATTTTT AGCAAAACTGTCCAGAAAAGAGAGTTCAGAAGGAGCCCTAGCCCTTTAGTCCAT---ACAAGTTTTGCTCAGGGTCACCA AAGAAAGCCCAGGAAATTAGACTCCTCAGAAGAGGACGTATCTAGTGAG >HairyArma TGTGGCACAAATACTCATGCCAACTTATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAGTGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGCGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCCAACACAGAGAAAAAGGTAGATGTGGCTGCTGATTCCCTGTATGGGCGAAAAGAA CTGAATAAGCAGAAACTTCCATGCTCTGAGAGTCCTAGAGAT---ACCCAAGAT---ATTCCTTGGATAACGCTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGACCTATTAACTTCTGATGACTCACACGATGGGGGGTCTG AATCAAAAGCAGAAGTAGCTGGTGCATTAAAAGTTCCAAAT------GAAGTAAATGGATATTCTAGTTCTTCAGAGAAG ATAGACTTATTGGCCAGTGATCCTCATAATGCTTTAATATTTGCAAGTGAAAGAGTCCAATCCAAACCAGCAGAGAGTAA C---ATCAAAGATAAAATATTTGGGAAAACCTATCACAGGAAGGCAAGCTTTCCTAACTTGAGCCACATAACTGAGGATC TTTTTATAGGAGCTATTGCTACAGAACCCAAGATAATACAA--------------------------------------- ---------------------GAGCATTCCCTCACAAATAAAATAAAGCGTAAAAGGAGAACTACGTCATGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGACAGTTGTTCAAAAGACGCCTGAAAAGATAAATCAGGGAACTGACCAAATGG AGCAGAAT---------GATCAAGTGATGAATAGTGCTAATAGTGGTCATGAAAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGGAAAATGCTAACCCAATAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTAAAGGTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATCTATAATTCAAAAGCATCTAAGAAGAATAGGCTGAGGAGGA TGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGGCAATAGAAATCCAAGCCCACCTAAACATACTGAACTACAA ATTGATAGTTGTTCTAGCACTGAAGAG---ATAGAGAAAATA---AATTCTAACCAAAAACCAATCAGACACAACAGAAT GCTTCAACTCATGAAAGAAAAAGAAAACACAACTGGAGCTAAAAAGAATAACAAGCCAAATGAACAAATAAGTGAAAGAC ATGCCAGTGATGTTTTCCCAGAACTAAAATTAACAAATGTAACCGATTTTCTTCCTAAATGTTCAAATCCTGATAAACTT CAAGAATTTGTTAATTCTAGCCTGCAAGGGGAAGTAGCA---GAGAAC---CTAGAAACAATTCAAGTGTCTGATACTAC CAGGAATCCTGAAGATCTGGTGTTAAGTGAAGGAAAG---AGTTTGCAA---ACTCAAAGGTCTGCAGAGAGTACCAGTA TTTCAGTGGTACCTGATACTGATTATGGCACTCAAGACAGTGTCTCATTACTGGAAGCTGACACCCTGGGG---AAGGCA AAA---ACAGCACTAAATCAACCTATGAGTCAGTATGCAGCAATTAAAAATGCCACTGAACTCTCCCATGGTTGT---GC TAAAGACACTAGAAATGACACTGAGGATTTTAAGGATCCGTTGAGACATGAAGTTACC---CACACTCAGGAGACTAGTG TAGAAATGGAAGAGAGTGAACTTGATACTCAGTATTTACAGAATACATTCAAGATTTCAAAGCGTCAGTCATTTGCTCTG TTTTCGAATCCA---------GAAAATGAATGCGCAACAGTGTGTGCCCACTCCAGGTTCTTAGGCAAACAAAGTCCAAA AGTCATTTTTGAATGTAGGCAAAAAGAAGAAAATCAGGGGAAGAAAGAGTCTAAAATCAAACATGTGCAGGCAGTTCATA CAACTGCAGGCTTTCCTGTAGTTTGTCAGAAAGATAAG---CCAGGTGATTATGCCAAATGTAGCATTCAAGAAGTCTCT AGGCTTTGTCAGTCCTCTCAGTTCAGA---GGCAATGAATCTGAACTCATTACTGCAAATGAACATGAAATTTCACAAAA CCCAGATCAAATGCCATCACTTTCTCACATCAGGTCATCTGTTAAAACTAAATGTAAGGAAAAC---CTGTCAGAGGAAA AGTTTGAGGAACTTACAATATCACTTGAAAGAACAGTGGGAAATGAGAACATCGTTCAAAGTACAGTAAGCACAATTAGC CACAATAACATTAGAGAAAACGCTTTTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGTTCTAGT----------- ----------------------------------------------------GGTGAAAACATTCAAGCAGAACTAGGTA GAAACAGAGCACCTAAATTAAATGCTATGCTCAGATTAGGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT ATAACTAGTTGTAAACATCCTGAAATAAAAAGGCAAGGAGAAAATGAAGAAGCAATTCAGGCTGTTGATACTGATTTCTC TCCACATCTAATTTCAGATAACCTAGAACTA---CCTATGGGAAATAGTCATGTTTCTCAGATTTGTTCTGAGACGCCTG ATGATTTGTTAGATGATGATGAAATAAAGGAAAATAACAGCTTTGCTGAARATGGCATTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAGAAAAGAGAGTTCAGAAGGAGTCCTAGCCCTTTAGGCCAT---ACAAGTTTGGCTCAGGGTCACCA AAGAAGGGCCAGGAAATTAGACTCCTCAGAAGAGGACGTATCTAGTGAG >Anteater TGTGGCACAAATATTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAAAGAATGTAGA AAAGGCTGAATTCTGTGATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCACACTACCAGCACAGAGAAAAAAGTAGATGTGGATGCTGATCCCCTGCATCGGAGAAAAGAA CTGAAGAAGTGGAAATCTCCATACTCTGAGAATCCTAGAGGT---ACCCAAGAT---ATTCCTTGGATAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGACATATTGACTTCTAATGACTCATGCAATGAGGGGTCTG AATTAAATGCAGAAGTAGCTGATGCATCAAAAGTTCCAAAT------GACGTAGATAGATATTCTGGTTCTTCAGAGAAA ATAGACTTAATAGCCAGTGACCCTCATAATGCTTTAATATGTGCAAGTGAAAAAGTCCAGTCCAAACCAACAGAGAGTAA T---ATCAAAGATAAAATATTTGGGAAAACCTATCACAGGAAGGCAAGCCTCCCTAACTTGAGCCGTATAGCTAAAGATC TTTTTATAGGAGCTGTTGCTGCAGAACCTAAGATAACACAA--------------------------------------- ---------------------GAGCTCCCCCTGACAAATGAAATGAAGCTTAAAAGGAGGACTACATCAGGACTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGACAGTTGTTCAAAAGAAGCCTAAAATGATAAATCAGGGAACTAACCAAATAG AGCAGAAT---------TGTCAACTGATGAATATTGCCAATAGTGGTAATGAAAATGAAACAAAAGGTGATTTT---GTT CAGAAGGAGAAAAGTGCTAACCCAACAGAA------TCATCAGAAAAAGAATCTGCTTTCAGAACTAAAGGTGAACCTAT AAGTAGTAGTATAAGCAATATGAAACTAGAATTAAATACCTGCAATTCAAAAGCATCTAAGAAGAATAGGCTGAGGAGAA TGTCCTCTACCAGGCATATGCATGCACTTGAACTAGTAGCCAATAGAAAGCCAAGCCAGCCTAATCACACTGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAA---ATAAAGAAAAAA---AAATCTGACCAAAAGCCAATAAGACACAGCAGAAC AGTTCAATTCATGAAAGATAAAGAAACTGCAATTGGAGCCAAGAAGAGTAACAAGCGAAATGAACAAATAAATAAAAGAC ATGCCAGTGATGCTTTCCCAGAACTAAATTTAACAAACGTAACTGGTTTTCCTACTAAATGTTCAAATTCTGATAAACTT CAAGAATTTGTCAATTCTAGCCTGCAAGGAGAAGCAGCA---GAGAAC---CTAGAAACAATACAAGTGTCTGATACTAC CATGGACCCTGAAGGTCTGGTATTAAGTGAAGGAAAG---AATTTGCAA---ACTGAAAGATCTGTAGAGAGCACCAGTA TTTCATTGGTGCCTGACACTGATTATGGCACTCAAGATAGTATCTCATTACTAGAAGCTGCTACCCTAGGG---AAAGCA AAA---GCAGCACCAAATCAACATGTGAGTCTGTGTGCAGCAGTTGGAAATGCCACTGAACTTGTCCATGGTTGT---TC TAAAGATACTAGAAATGACACTGAAGATTTTAAGGATTCATTGAGACATGAAGTTAAC---CACACACAAGGGACAGTCA TAGAAAAGGAAGAGAGTGAACTTGATACTCAGTATTTNNNNNNNNNNTACAAGATTTCAAAGCGTCAGTCATTTGCTCTG TATTCAAATTCT---------GAAAAGGAATGTGTAACAATCTGTGCCCACTCCAGGTCCGTACGGAAACAAAGTCCAAA AGTAACTTTTGACTATAGACAAAAAGAAGAAAATCAAGGAAAGAAAGAGTCTAAGATCAAACATGTGCAGGCAGTTCATA CAACCGCAGGCTTCTCTGTAGTTTGTCAGAAAGATAAGAAGCCCCATGATTATGCCAAGTGTAGCATTCAGGGAGTCTCT AAGCTTTGTGAGTCATCTCAGTTCAGA---GGCAATGAATCTGAACTCATTACTGCAAACGAACATGGAATTTCCCCAAA TCCAGATCAAATGCCATCACTTTCTCCCAACAGGTCATCTGTTAAAACTAAATATAAGAAAAAC---TTGTCAGAAGAAA GGTTTGAGGAACATACAGTGTCACTTGATAGAGCAGTGGGAAATGAGAGCATCATTCAAAGTACAGTAAGCACAATTAGC CAAAATAACATTAGAGAAAGCACTTTTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGTTCCAGTATTAATGAAGT GGGTTCCAGTATTAATGAAGTGGGTTCCAGT---------------------GGTGAGAACGTTCAAGCAGAGCTAGGTA GAAACAGA---CCTAAGCTAAATGCTATGCTCAGATTAGGTCTTATGCAACCTGAAGTCTATGAGCAAAATCTT---CCT ATAACTAATTTTAAACTTTCTGAAATTAAAAAACAAGGAGAAAATGAAGAAGTAGTTCAGGCTGTTAATACAGATTTCTC CCCATGTCTAATTTCAGATAACCTAGAACTG---CCTATGGGAAGTAGTCGTGTTTCTCAGATTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGATGAAATAAAGGAAAATAACAGCTTTGCTGAAAGTGGCGTTAAGGAAAGATCTGCTGTTTTT AGCAAAAGTGTCCAGAGAAGAGAATTCAGAAGGAGCCCTAGCCCTTTAGCCCAA---ACAAGTGTGGCTCAGGGTCACCA AAGAGGGGCCAGGAAATTAGCCTCCTCAGAAGNGGACNAGTCTAGNGAG >Sloth TGTGGCACATATACTCATGCCAGCTCATTACAGCGTGAGAACAGCAGTTTATTACTCACTAAAAACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAGCAGCCTGGCTTAGCAAGGAGCCAACAGAACAGATGGGCTGAAAGTAAGGAAA CACGTAATGAT---AGGCAGACTCCCAGCACAGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGTATGGGCGAAAAGAA CTGAATAAGCAGAAACCTCCATGCTCTGAGAGTCCTCAAAAT---ACCCAAGAT---ATTCCTTGGATAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGACATACTAACTTCTGATGACTCACACAATGGGGGGTCTG AATCAAATGCAGAAGTAGTTGGTGCATTGAAAGTTCCAAAT------GAAGTAGATGGATATTCTGGTTCTTCAGAGAAG ATAGACTTAATAGCCAGTGATCCTCACAATGCTTTAATATTTGCAGGTGAAAGAGTCCAGTCCAAACCAACAGAGACTAA C---ATTGAAGATAAAATATTTGGGAAAACCTATCACAGGAAGGCAAGCCTCCCTAACTTGAGCCACATAGCTGAAAATC TTTTTATAGGAGCCATTGCTACAAAACCTAAGATAACACAA--------------------------------------- ---------------------GAGCACCCCCTGACAAAGAAAATAAAGCATAAAAGTAGGACTACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTCGATTTGACAGTTGTTCAAAAGATGCCTGAAAAGATAAATCAGGGAACTGACCAAATGG AGCAGAAG------AATAGTCAAGTGATAAATATTGCTAATAGTGGTCATGAGAATGAAACAAAAGATGATTAT---GTT CAGAAAGAGAAAAATGCTAACCCAACAGAA------TCATTGGAAAAAGAATCTGCTTTCAGAACTGAAGGTGAACCCAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATCTTCAATTCAAAAGTATCTAAGAAGAATAGGCTGAAGAGGA TGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGCCAATAGAAATCCAAGCCAACCTAATCATAATGAACTACAA ATTGATAGTTGTTCTAGCAGTGAAGAG---ATAAAGAAAGAA---AATTCTGTCCAAAAGCCAATAAGGCACAGCAGAAT GCTTCAACTCCTGAAAGGTAAAGAAACTCCAACCGTAACCAAGAAGAGTAACAAGCGAAATGAACAAATAAGTAAAAGGC ATTCCAGTGATGCTTTCCCAGAACTAAATTTAACAAATGTAACTGGTTTTCTTACTAAATGTTCAAGTTCTGATAAACTT CAAGAATTTGTCAATTCTAGCCTGCAAGGAGAAGTAGCA---GAGAAC---CTAGAAACAATTCAAGTGTCTGATAGTAC CAGAGACCCCGGAGCTCTGGTGTTAAGTGGAGGAAAG---GGTTTGCAA---ACTGAAAGATCTCTAGAGAGTACCAGTA TTACAATGATACCTGAAACTGATTATGACACTCAAGACAGTATCTCCTTACTGGAAGCTGACACCCTAGGG---AAAGCA AAA---GCAGCACCAAATCAACATGTGAGTCAGTATGCAGCAATTGGAAATGCCACTAAACTTTTCCATGGTTGT---TC TAAAGATACTAGAAGTGACACTGAGAATTTTAAGGATCCATTGAGACATGAAGTTAAC---CACACACAGGAGACATTTG TAGAAATGGAAGAGAGTGAACTTGATACTCAGTATTTACAGAATACATTCAAGATTTCAAAGCGTCAATCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATGCGCAACAGTCTCTGCCCACTCCAGGCCCTTAGGAAAACAAAGTCCAAA AGTCACTTTTGACTGTAGACAAAAAGAA---GATCAGGAGAAGAAGGAGTCTAAAATCAAACACGTGCAGGCAGTTCATA CAACTGCAGACTTTCCTGTAGTTTGTCAGAAAGATAAG---CCAGGTGATTATGCTAAATGTAGCATTCAAGGAGTCTCT AAGCTTTGTCAGTTATTTCAGTTCAGA---GGCAATGAATCTGAACCCATTACTGCAGATGAACATGAAATTTCACAAAA TCCAGATCAGATGCCATCACTTTCTCCCATGAAGTCATCTGTTAAAAGTAAATTTAAGGAAAAC---CTGTCAGAGGAAA GATTTGAGGAACATACAGTATCACTTGAAAGAGCAGTGGGAAAGGAGCACATCATTCAAAGGACAGTGAGTCCAATTAGC CAAAATAACATTAGAGAAAGCGCTTTTAAAGAAGCCAGCTCAAGCAGTATCAATGAAGTAGGTTCCAGTGTTAATGAAGT AGGTTCCAGTGTTAATGAAGTAGGTTCCAGT---------------------GGTGAGAACACTCAAGCAGAGCTAGGTA GAAACAGAGGATCTAAATTAAGTGCTATGCTCAGATTAAGTCTTATGCAACCTGAAGTCTATAAGCAAAGTCTT---CCT ATAACTAATTGTAAGCATCCTGAAATTAAAAAGCAAGGAGAAAATGAAGAAGTAGTTCAGGCTGTTAAAACA-------- ----TGTCTAATTTCAGATAACCTAGAACTA---CCTATGGGAAGTAGTCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGATCTGTTAGATGATGGTGAAATAAAGGAAAATAACAGCTTTGCTGAAAGTGGCATTAAGGAAAGATCTGCTGTTTTT ACCAAAAGTGTCCAGAAAAGAGAGTTCAGAAGGAGCCCGAGCCCGTTAGCCCAA---ACAAGGGTCACC---------AA AAGACGGGCCAGGAAATTAGACTCCTCAGAAGAGGATGTGTCTAGTGAG >Dugong TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAATAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTCATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCTAGCACAGAGAAAAAGGTAGATATGAATGCTAATCCATTGTATGAGAGAAAAGAA GTGAATAAGCAGAAACCTCCATGCTCCGAGAGTGTTAGAGAT---ACACAGGAT---ATTCCTTGGCTAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTTCAGAAGTGATGGCCTG---------GATGACTTGCATGATAAGGGGTCTG AGTCAAATGCAGAAGTAGCTGGTGCTTTAGAAGTTCCAGAA------GAAGTACATGGATATTCTAGTTCTTCAGAGAAA ATAGACTTAATGGCTAGTGATCCTCATAGTGCTTTAATATGTGAAAGTGAAAGAGTCCTCTTCAAACCAGCAGAAAGTAA C---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAATTCTCCCTCATTTGAGCCATGTAACTGAAGATC TAATTATAGGAGCTGTTGCTACAGAACCTCAGATAGCACAA--------------------------------------- ---------------------GAACGTCCTCTTACAAATAAATTAAAGCGTAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTCATCAAGAAAGTAGATTTGGCAGTTGTTCAAAAGACTCCTGAAAAGATAAATCAAGAAACTGACCAAGTGG AGCAGAAT---------GGTCAAGTGATGAATATTGCTAATGGTGGTCATGGAAATGAAACAAAAGATGATTAT---GTT CAGAAAGAGAAGAATGCTAACCCAACAGAA------TCACTGACAAAGGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATGCATAATTCAAAAGCACCGAAGAAGAACAGGCTGAGGAGGA AGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGTCCACCTACTCATACTGAACTACAA ATTGATAGTTGGTCTAGCAGTGAAGAG---ATAAAGAAA------AGTTCTGAGCAAAAGCCAGTCAGACACAACAGAAA CCTTCAACTCATGAAAAACCAAGAAACCACAACTGGAGCCAAGAAAAGTAACAAGCCAAAGGAACAAATAAGTAAAAGAC ATGCCAGTGACGCTTACCCAGAACTAAATTTAACAAGCACAACTGGCTTAATTACTAACTGTTCAAGTTCTCATAATTAT CAAGAATTT---AATCCTAGCCTTCAGGGAGAAGAAATAGAAGAAAAT---CTGGGAACAATTCAAGTGTCTAATAGAAC CAGAGACCCCGAGGATCTAGTGTTAAATGGAGGAAGA---GGTTTGCAA---ACTGAAATATCTGTTGAGAGTACCAGTA TCTCAGTGATACCTGATACTGACTATGGCAGTCAGAACAGCATCTCATTACTGGAAGCTGACACCCTCAGG---AAGGCA AAA---ACAGCACCAAATCAATGTGCAAGTCAGTGTGCAGCAATTGAAAACCCCAATGAACTTATCCATGGTTGT---CC TAAAGATACTAGAAATGACACAGAGGATTTTAAGGATCTGTTGAGATGTGAAGTTAAC---CACATTCAGGAGACGTGCG TAGAAATGGAAGACAGNGAACTTGATACTCAGTATTTGCAGAGTACATTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATGTGCAACAATTTGTGCCCACTCCAAGTCCTTAAGGAAACAAAGTCCAAA AGTCACTCCTGAGTATGGAGAAGAAGAAGAAAATCAAGGGAACAAAGAGTCTAAAATCAAGCATGAGCAGGCAGTTCATA CGACTGCGGGCTATCCTGAGGATTGTCAGAAAGAGAAGAAGCCAAGTGATTATACCAAAAGTAGCATCAAAGGAGTCTCT AGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAGTGAATCCCAACACATTACTGCAGGTGAACATGGAATTTCACAAAA TCCAGATCAAATGCCATTGCTTTCTCCCATCAGGGCATCTGTTAAAAGT------AAGAAAAAC---TTGTCAGAAGAAA GGTTTGAGGAACATACAATATCACTTGAAAGAGCAGTAGGAAATGAGAGCATCGTTCAAAGTACAGTGAGCACAGTTAGC CAAAATGACATTAAGGAAAGTGCTTCTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGTTCTAGT----------- ----------------------------------------------------GGCGAAAACATTCGAGCAGAGCTAGGTA GGAACAGAGGACCTAAATTAAATGCTGTGCTCAGATTAGGTCTTATGCAACCTGAAGTCTATAAACAAAGTCTT---CCT GTAAGTAACTGTAAACGTCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTAGTTCAGGATGTTAATATGGATTTCTC TCCATGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGTAGTCGTGCTTCTCAGATTTGTTCTGAGACTCCTG ATGACCTGTTAGATGATGATGAAATAAAGGAAAATATCAGCTTTGCTGAAAGTGGCATTAAGGAAAGATCTGCTGTTTTT AGTAAA---GACCAGAGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTATCCCAT---TCAGGTTTGGCTCAGGGTCACCT AAGAGGGGCCAGGGAATTAGAGTCCTCAGACGAGAACATATCTAGTGAG >Manatee TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAATAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTCATAAAAGCAAACAGCCTGGCTTAACAAGGAGCCAGCAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCTAGCACAGAGAAAAAGGTAGATATGAATGCTAATCCATTGTATGAGAGAAAAGAA GTGAATAAGCAGAAACCTCCATGCTCCGAGAGTGTTAGAGAT---ACACAAGAT---ATTCCTTGGCTGACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTTCAGAAGTGATGGCCTG---------GATGACTTGCATGATAAGGGGTCTG AATCAAATGCAGAAGTAGCTGGTGCATTAGAAGTTCCAGAA------GAAGTACATGGATATTCTAGTTCTTCAGAGAAA ATAGACTTAATGGCCAGTGATCCTCATAGTGCTTTAATATGTGAAAGTGAAAGAGTCCTCTCCAAACCAGCAGAAAGTAA C---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAATTCTCCCTCATTTGAGCCATGTAACTGAAGATC TAATTATAGGAGCTGTTGCTACAGAACCTCAGATAGCACAA--------------------------------------- ---------------------GAACGTCCCCTTACAAATAAATTAAAGCGTAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTCATCAAGAAAGTAGATTTGGCAGTTGTTCAAAAGACTCCTGAAAAGATAAATCAGGAAACTGACCAAGTGG AGCAGAAT---------GGTCAAGTGATGAATATTGCTAATGGTGGTCATGAAAATGAAACAAAAGATGATTAT---GTT CAGAAGGAGAAGAATGCTAACCCAACAGAA------TCACTGACAAAGGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATGCATAATTCAAAAGCACTGAAGAAGAATCGGCTGAGGAGGA AGTCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGTCCACCTACTCATACTGAACTACAA ATTGATAGTTGGTCTAGCAGTGACGAG---ATAAAGAAA------AGTTCTGAGCAAAAGCCAGTCAGACACAACAGAAA CCTTCAACTCATGAAAAACCAAGAAACCACAACTGGAGCCAAGAAAAGTAACAAGCCAAAGGAACAAATAAGTAAAAGAC ATGCCAGTGACACTTACCCAGAACTAAATTTAACAAGCACAACTGGCTTAATTACTAACTGTTCAAGTTCTCATAATTAT CAAAAATTTGTTAATCCTAGCCTTCAGGGAGAAGAAATAGAAGAAAAT---CTGGGAGCAACTCAAGTGTCTAATAGAAC CAGAGACCCCGAGGATCTAGTGTTAAATGGAGGAAGA---GGTTTGCAA---ACTGAAATATCTGTTGAGAGTACCAGTA TCTCAGTGATACCTGATACTGATTATGGCAGTCAGAACAGCATCTCATTACTGGAAGCTGACACCCTCAGG---AAGGCA AAA---ACAGCACCAAATCACTGTGCAAGTCAGTGTGCAGCAATTGAAAACCCCAATGAACTTATCCATGGTTGT---CC TAAAGATACTAGAAATGACACAGAGGATTTTAAGGATCTGTTGAGATGTGAAGTTAAC---CACGTTCAGGAGACATGCA TAGAAATGGAAGACAGTGAACTTGATACTCAGTATTTGCAGAGTACATTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATGTGCAACAATTTGTGCCCACTCCAAGTCCTTAAGGAAACAAAGTCCAAA AGTCACTCCTGAGTATGGAGAAAAAGAAGAAAATCAAGGGAACAAAGAGTCTAAAATCAAGCATGAGCAGGCAGTTCATA CAACTGCTGGCTATCCTGAGGATTGTCAGAAAGAGAAGAAGCCAAGTGATTATACCAAATGTAGCACTAAAGGAGTCTCT AGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAGTGAATCCGAACACATT---ACAGGTGAACATGGAATTTCACAAAA TCCAGATCAAATGCCATTGCTTTCTCCCATCAGGGCATCTGTTAAAAGT------AAGAAAAAC---TTGTCAGAAGAAA GGTTTGAGGAACATACAATATCACTTGAAAGAGCAGTAGGAAATGAGAGCATCGTTCAAAGTACAGTGAGCACAGTTAGC CAAAATAACATTAAGGAAAGTGCTTCTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTAGGTTCCAGTGTTAATGAAGT AGGTTCTAGT------------------------------------------GGCGAAAACATTGAAGCAGAGCTAGGTA GGAACAGAGGACCTAAATTAAATGCTGTGCTCAGATTAGGTCTTATGCAACCTGAAGTGTATAAACAAAGTCTT---CCT GTAAGTAACTGTAAACATCCTGAAATAAAAAGGCAAGGAGAAAATGAAGGAGTCGTTGAGGATGTTAATATGGATTTCTC TCCATGTCTAATTTCAGATAACCTAGAACAA---CCTATGGGAAGTAGTCGTGCTTCTCAGATTTGTTCTGAGACTCCTG ATGACCTGTTAGATGATGATGAAATAAAGGAAAATATCAGCTTTGCTGAAAGTGGCATTAAGGAAAGATCTGCTGTTTTT AGTAAA---GACCAGAGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTATCCCGT---TCAGGTTTGGCTCAGGGTCACCT AAGAGGGGCCAGGGAATTAGAGTCCTCAGAAGAGAACATATCTAGTGAG >AfricanEl TGTGGCACAAATACTCATGCCAGCTCATTACAGCATAAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAACAAAAGCAAACAGCCTGGCTTAGCAAGAAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCTAGCACAGAGAAAAAGGTAGATGTGAATGCTGATCCCTTGTATGAGAGAAAAGAA GTGAATAAGCAGAAACCTCCACGCTCTGAGAATCTTAGAGAC---ACCCAAGAT---ATTCCTTGGATAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTTCAGAAGTGACGGCCTG---------GATGTCTTAAATGATGAGGGGCCTG AATCCAGTGCAGAAGTAGCTGGTGCATTAGAAGTTCCAAAT------GAAGTACAT------TCTAATTCTTCAGAGAAA ATAGACCTAATGGCCAGTGATCTGCATGGTGCTTTAATATGTGAAAGTGAAAGAGTCCCCTCCAAACCAGCAGAAAGTAA C---ATCGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAGGTATGTCTCATTTGAGCCACATAACTGAAGATC TGATTATGGGAGCTATTGCTTCAGAACCTCAGATAGCACGA--------------------------------------- ---------------------GAACATCCTTTTACAAATAAATTAAAGCGTAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCAGTTGTTCAAAAGATCCCTGAAAAGATAAATCAGGAAACTGACCATGTGG AGCAGAAC---------GGTCAAGTGATGAATATTGCTAATGGTGGTCGTGAGAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGAAGAATGCTATCCCAACAGAA------TCATTGGCAAAAGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATGCATAATTCAAAAGCACCAAAGAAGAATAGGCTGAGGAGGA AGTCGTCAACCAGACATATTCATGCACTTGAACTAGTAGTCAATAGAAATCCAAGTCCACCTACTCATACTGAACTACAA ATTGACAGTTGGTCTAGCAGTGAAGAG---ACAAAGAAAAAA---AGTTCTGAGCAAAAGCCAATCAGACACAACAGAAA CCTTCAACTCATGAAAAATCAAGAAACCGCAACTGGAGCCAAGAAGAGTAACAAGCCAAAGGAACAAATAAGTAAAAGAC ATGGCGCTGACTCTTACCCAGAACTACATTTAACAACCACAGCTGGCTTTATTACTAAGTGTTCAAGTTCTGATAATCTT CAAGAATTTGTCAATCCTAGCCTTCAAGGAGAGAAAACAGAAGAAAAC---CTGGAAACAATTCAAGTGTCTAATATTAC CAAAGAGCCCAAGGATCTAGTGTTAAATGGAGGAAGA---GATTTGCAA---ACCAAAAAATCTATTGAGAGTACCAATA TCTCAGTGATACCTGATACTGTTTATGGCACTCAGGACAGCGTCTCATTGCTGGGAGCTGACACCCCAGGG---AAGGCA AAA---ACAGCACCAAATCGATGTGCAAGTCAGTGTACAGCAATTGAAAACCCAAGTGAACTTACCAACAGTTGT---CC TAAAGATACTAGAAATGACACAGAGGGTTTTAAGGATCTATTGAGATGTGAAGCTAGC---CACATTCAGGAGACATGCA TAGAAATAGAAGAGAGTGAACTTGATACTCAGTATTTGCAGAGTACATTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGAAATGTGCAACAATTTGTGCCCACTCCAAGTCCTTAAGGAAGCAAAGCCCAAA AGTCACTCTTGTGTGTGGAGAAAAAGAAGAAAATCAAGGGAACAGAGAGCCTAAAATCAAGCATGAGCAGGCAGTTCATA TGCCTACAGGCTATCCTGAGGCTTGTCAGAAAGAGAAG---CCAAGTGACTATACCAAATATAGCATTAAAGGAGTCTCT GGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAGTGAATCTGAACTCATTACTGCAGATGGACATGGAATCTCACAAAA CCCAGATCAAATACCATCACTTTCTCCCACCAGGTCATCTGTTAAAACTAAATGTAAGACAAAC---CTGTTGGAAGAAA GGTTTGAGGAACATACAATATCACTTGAAAGAGCAATGAGAAATGAGAACGTCATTCAAAGTACAGTGAGCACAGTTAGC CAAAATAACATTAGGGAAAGTGCTTCTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTATGTTCCAGTATTAATGAAGT AGGTTCTAGT------------------------------------------GGTGAAAACATTCAAGCAGAAATAAGTA GGAAGAGAGGACCTAAATTAAATGCTGTGCTCAGATTAGGTCTTATGCAACCTGAAGTTTATAAACAAAGTCTT---CCT ATAAGTGACTGTAAACATCCTGGAATAAAAACGCAAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAATACAGATTTCTC CCCATGTCTAATTTCAGATAACCTAGAACAA---CCTGTGGGAACTAGTCGTGCTTCTCAGGTTTGTTCTGAGACTCCCG ACGACCTGTTAGATGATGATGAAATAAAGGAAAATATCAGCTTTGCTGAAAACGGCATTAAGGAAAGATCT---GTTTTT ATTAAAGATGACCAGAGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTATCCCAT---TCAGGTTTGGCTCAGGGTTGCCT AAGAGGGGCCAGGGAATTGGAGCCCTCACAAGAGAACATATCTAG---- >AsianElep TGTGGCACAAATACTCATGCCAGCTCATTACAGCATAAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAACAAAAGCAAACAGCCTGGCTTAGCAAGAAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCTAGCACAGAGAAAAAGGTAGATGTGAATGCTAATCCCTTGTATGAGAGAAAAGAA GTGAATAAGCAGAAACCTCCACGCTCTGAGAATCTTAGAGAC---ACCCAAGAT---ATTCCTTGGATAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTTCAGAAGTGACGGCCTG---------GATGTCTTAAATGATGAGGGGCCTG AATCCAGTGCAGAAGTAGCTGGTGCATTAGAAGTTCCAAAT------GAAGTACAT------TCTAATTCTTCAGAGAAA ATAGACCTAATGGCCAGTGATCTGCGTGGTGCTTTAATATGTGAAAGTGAAAGAGTCCCCTCCAAACCAGCAGAAAGTAA C---ATCGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAGGTATCTCTCATTTGAGCCACATAACTGAAGATC TGATTATGGGAGCTATTGCTTCAGAACCTCAGATAGCACGA--------------------------------------- ---------------------GAACATCCTTTTACAAATAAATTAAAGCGTAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGTAGATTTGGCAGTTGTTCAAAAGATCCCTGAAAAGATAAATCAGGAAACTGACCATGTGG AGCAGAAC---------GGTCAAGTGATGAATATTGCTAATGGTGGTCGTGAGAATGAAACAAAAGGTGATTAT---GTT CAGAAAGAGAAGAATGCTATCCCAACAGAA------TCATTGGCAAAAGAATCTGCTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATATGGAGGTAGAATTAAATATGCATAATTCAAAAGCCCCAAAGAAGAATAGGCTGAGGAGGA AGTCGTCAACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGAAATCTAAGTCCACCTACTCATACTGAACTACAA ATTGACAGTTGGTCTAGCAGTGAAGAG---ACAAAGAAAAAA---AGTTCTGAGCAAAAGCCAATCAGACACAACAGAAA CCTTCAACTCATGAAAAATCAAGAAACCGCAACTGGAGCCAAGAAGAGTAACAAGCCAAAGGAACAAATAAGTAAAAGAC ATGGCGCTGACTCTTACCCAGAACTACATTTAACAACCACAGCTGGCTTTATTACTAAGTGTTCAAGTTCTGATAATCTT CAAGAATTTGTCAATCCTAGCCTTCAAGGAGAGAAAACAGAAGAAAAC---CTGGAAACAATTCAAGTGTCTAATATTAC CAAAGAGCCCAAGGATCTAGTGTTAAATGGAGGAAGA---GATTTGCAA---ACCAAAAAATCTATTGAGAGTACCAATA TCTCAGTGATACCTGATACTGTTTATGGCACTCAGGACAGCGTCTCATTGCTGGGAGCTGACACCCCAGGG---AAGGCA AAA---ACAGCACCAAATCAATGTGCAAGTCAGTGTACAGCAATTGAAAACCCAAGTGAACTTACCAACAGTTGT---CC TAAAGATACTAGAAATGACACAGAGGGTTTTAAGGATCTATTGAGATGTGAAGCTAGC---CACATTCAGGAGACATGCA TAGAAATAGAAGAGAGTGAACTTGATACTCAGTATTTACAGAGTACATTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGAAATGTGCAACAATTTGTGCCCACTCCAAGTCCTTAAGGAAGCAAAGCCCAAA AGTCACTCTTGTGTGTGGAGAAAAAGAAGAAAATCAAGGGAACAGAGAGCCTAAAATCAAGCATGAGCAGGCAGTTCATA TGCCTGCAGGCTATCCTGAGGCTTGTCAGAAAGAGAAG---CCAAGTGACTATACCAAATATAGCATTAAAGGAGTCTCT GGGCTTTGTCAGTCATCTCAGTTCAGA---GGCAGTGAATCTGAACTCATTACTGCAGATGGACATGGAATCTCACAAAA CCCAGATCAAATACCATCACTTTCTCCCACCAGGTCATCTGTTAAAACTAAATGTAAGACAAAC---CTGTTGGAAGAAA GGTTTGAGGAACATACAATATCACTTGAAAGAGCAATGAGAAATGAGAACGTCATTCAAAGTACAGTGAGCACAGTTAGC CAAAATAACATTAGGGAAAGTGCTTCTAAAGAAGCCAGCTCAAGCAGTATTAATGAAGTATGTTCCAGTATTAATGAAGT AGGTTCTAGT------------------------------------------GGTGAAAACATTCAAGCAGAAATAAGTA GGAAGAGAGGACCTAAATTAAATGCTGTGCTCAGATTAGGTCTTATGCAACCTGAAGTTTATAAACAAAGTCTT---CCT ATAAGTGACTGTAAACATCCTGGAATAAAAACGCAAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAATACAGATTTCTC CCCATGTCTAATTTCAGATAACCTAGAACAA---CCTGTGGGAACTAGTCGTGCTTCTCAGGTTTGTTCTGAGACTCCCG ACGACCTGTTAGATGATGATGAAATAAAGGAAAATATCAGCTTTGCTGAAAACAGCATTAAGGAAAGATCT---GTTTTT ATTAAAGATGACCAGAGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTATCCCAT---TCAGGTTTGGCTCAGGGTTGCCT AAGAGGGGCCAGGGAATTGGAGCCCTCACAAGAGAACA----------- >RockHyrax TGTGGCACAGATACTTGTGCCAGCTCGTTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGGATGTAGA AAAGGCTGAATTCTGTAGTAAGAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAGAGTACGGAAA CATGTAATGGT---AGGCAGATTCTTAGAACAGAGAAAAAGGTAGAAACGAATGCTGATCCTTTGTATGGGAAAAAAGAA GGGAATAAGCAGAAACCTCCATGCTCTGAGAGTCGCACAGAT---ACGCAAGAT---ATTCCTTGGGTAACACTGAATAA CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGACAACCTA---------AGTGATTCACCTAGTGAGGGGTCTG AATTAAATGGAAAAGTGGCTGGTCCAGTAAAACTTCCAGGT------GAAGTACATAGATATTCTAGTTTTCCAGAGAAC ATAGATTTAATGGCCAGTGGTCCTCCTGGT------------------GAAAGAATCCCCTGCAAACCACCAAAAAGTAA C---ATCAAAGATAAAATATTTGGGAAAACCTATCAGAGGAAGGGAAGTCTCCCTCACCTGAGCCACATAACCGAAGATC TAGTTAAAGGCGCCATTGCTACACAACCACAGATAGCACGA--------------------------------------- ---------------------GAACATCCTCTTACAAATAAGTTACAGCATAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTTATCAAAAAAGCAGATTTGACAGTTGTTCAGAAGGCATCTGAGAAGATAAATCAGGAAACTGACAAAGTGG AGCAGAGT---------GGTCAAGTGATAAATATTGGTAATGGTGGTCATGAGAATGAAACAAAAGATGATTAT---GTT CTGAAAGAGAAGAATGGTAACCCAGCAGAA------TCACTGAAAAAAGAATCTGTGTTCAAAACTAAAGCTGAACCTAT AAGCAACAGTATAAGCAATATGGAACTAGAATTAAATACACATAATTCAAAATCACTGAAGAAGAACAGGCTGAGGAAGA AGTCCTCCTCCAGACATGTTCATGCCCTTGAACTGGTAGTCAATAGAAATCCAAGTTCACCACCTCATACTGGGCTACAA ATTGATAGCTGGTCTGGCAGTGAAGAA---ATGAAGAAAACA---AGTTCTGAGCAAAAGCCAGTCAGACACAACATAAA CCTCCAACTCATGAAAAACCAAGAAACCACAACTGGAGCCAAGAAGAGTAACAAGCCAAAGGAACAAATAAATAAAAGAC ACACTAGTAATCCTTACCCAGAACTAAATTTAACAAGCACAGCTGGCTTTATTACTGTATGTTCAAGTTCTGATAATCTT CAAGAACCTGTCAACCCCAGCCTTCAAGGAGAGGAAATAGAAGAAAAC---TTGGCAACAGTCCAAATGTCTAATACTGC CAAAGAACCTGAGGATCTAGAGTTAAATGGAGGGAGA---GTTTTGCAA---ACCAAAAGATCTGTTGAAAGTACTAGTA CCTCAGTGATACCTGATGCTGACTGTGGTGCTCAGGACAGCATCTCGTTACTGGAAGCTGACACTCTAGGG---AAGGCA AAA---ACAGCACCAAATCAAGGGGCAGGTCAATGTTCGGCAATCGAAAACCCCAACGAACTTATTCATGGTAGT---CC TAAAGACACTAGAAATGATATAGAGGGTTTTAAGGATCCACTGAGATGCAAAGTTAAC---CCTATTCAGGAGATATGTG TAGAAATGGATGAAAATGAACTTGATACTCAGTATATACAGAGTACATTCAAGGTTTCAAAGCGTCATTCTTTTGCTCTG TTTTCAAATCCG---------GAAAAGGAGTGTGCGACAAGTTATACCCACTCCAAGTCGTTAAGGAAAGAAAATCCCAA AGTCACTCTTCAGCGTGGAGAAGAAGAAGAAAATCAAGGGAACAAAGAATCTAAAATCAAGCAT---------GTTCATA CAACTGCAAGCTGTCCTGAGGTTTGTCAGGAAGACACAAAGCCTAGTGATCGTACTAACTGTAGTGTTAAAGGACTCCCT AGGCTTTGTCACTCATCTCAATTCAAA---GGTAGTGAGTCTGAACTCATTACTGAAGGTGAACATGGAATTGCACAAAA CCCGGATCAGATGCCATCATCTTCTCCCATCAGATCATCTGTTAACTCTAAGTGTAACAAAAAC---CTGTCAGAAGAGC GATTTAAGGAACATAAAATATTACTTGAAAGAACAACAGGAAATGAAACCATTGTCCAAAGTACAGTGAGCACAGGTAGC CAAGATAACATTAGGGGAAGTGCTTCGAAAGAAACCAGCTCAAGCAGTATTAATGAAGTAGGTTCTAGT----------- ----------------------------------------------------AGCGAAAACATTCAAGCAGAAATAAGTA GGAACAGAGAACCTAGATTAAATGCTGTGCTCAGGTTAGGTGTTATGCAACCTGAAGTGTATAAACAAAGTCTT---TCT ATAAGTAACTGTAAACAGCCAGAAATAAAAAAGCGAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAGTACAGATTTCTC TGCATGTCCCATTTCAGAAAACCTAGAACAA---CCTGTGAGAAGTAGTCACACTTCTCAGGTTTGTTCTCAGACTCCTG ATAACCTGTTAGATGATGATGAAATAAAGGGAAAGACTGACTTTGCTGGAAGTAGCATTAAGGACAGACCT---GTCTTT AGTAAAGATGACCAGGGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTATCCCAT---TCAGGTTTGGCTCAGGGCCACCT GATAGGGGCCAGGGAATTAGAGGCCTCACAAGAGAACACATCTAGCGG- >TreeHyrax ------NCAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGGATGTAGA AAAGGCTGAATTCTGTAGTAAGAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAGAGTACGGAAA CATGTAATGAT---AGGCAGATTCTTAGAACAGAGAAAAAGGTAGATACAAATGCTGATCCTTTGTATGGTAAAAAAGAA GGGAATAAGCAGAAACCTCCATGCTCTGAGAGTCGCACAGAT---ACGCAAGAT---ATTCCTTGGGTAACACTGAATAA CAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGACAACCTA---------AGTGATTCACCTAGTGAGGGGTCTG AATTAAATGGAAAAGTGGCTGGTCCAGTAAAGCTTCCAGGT------GAAGTACATAGATATTCTAGTTTTCCAGAGAAC ATAGATTTAATGGCCAGTGGTCCTCCTGGTGATTTAATATGTGAAAGTGAAAGAATCCCCTGCAAACCACCAAAAAGTAA C---ATCAAAGATAAAATATTTGGGAAAACCTATCAGAGGAAGGGAAGTCTCCCTCACCTGAGCCACATAACTGAAGATC TAGTTAAAGGCGCCATTACTACACAACCACAGATAGCACGA--------------------------------------- ---------------------GAACATCCTCTTACAAATAAGTTACAGCATAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTTATCAAAAAAGCAGATTTGACAGTTGTTCAGAAGGCATCTGAGAAGATAAATCAGGAAACTGACAAAGTGG AGCAGAGT---------GGTCAAGTGATAAATATTGCTAATGGTGGTCATGAGAAGGAAACAAAAGATGATTAT---GTT CTGAAAGAGAAGAATGGTAACCCAGCAGAA------TCACTGAAAAAAGAATCTGTGTTCAAAACTAAAGCTGAACCTAT AAGCAACAGTATAAGCAATATGGAACTAGAATTAAATACACATAATTCAAAATCACTGAAGAAGAACAGACTGAGGAAGA AGTCCTCCTCCAGACATGTTCATGCCCTTGAACTGGTAGTCAATAGAAATCCAAGTTCACCACCTCATACTGGGCTACAA ATTGATAGTTGGTCTGGCAGTGAAGAA---ATGAAGAAAACA---AGTTCTGAGCAAAAGCCAGTCAGACACAACATAAA CCTCCAACTCATGAAAAACCAAGAAACCACAACTGGAGCCAAGAAGAGTAACAAGCCAAAGGAACAAATAAATAAAAGAC ACACTAGTAATCCTTACCCAGAACTAAATTTAACAAGCACAGCTGGCTTTATTACTGCGTGTTCAAGTTCTGATAATCTT CAAGAACCTGTCAACCCCAGCCTTCAAGGAGAGGAAATAGAAAAAAAC---TTGGCAACAGTCCAAATGTCTAATACTGC CAAAGAACCTGAGGATCTAGAGTTAAATGGAGGGGGA---GTTTTGCAA---ACCAAAAGATCTGTTGAAAGTACCAGTA CCTCAGTGATACCTGATGCTGACTGTGGTGCTCAGGACAGTATCTCGTTACTGGAAGCTGACACTCTAGGG---AAGGCA AAA---ACAGCACCAAATCAAGGGGCAGGTCAATGTTCAGCAATCGAAAACCCCAACGAACTTATTCATGGTAGT---CC TAAAGACACTAGAAATGATATAGAGGGTTTTAAGGATCCACTGAGATGCGAAGTTAAC---CCTATTCAGGAGATATGTG TAGAAATGGATGAAAATGAACTTGATACTCAGTATATACAGAGTACATTCAAGGTTTCAAAGCGTCATTCTTTTGCTCTG TTTTCAAATCCA---------GAAAAGGAGTGTGTGAAAAGTTATACCCACTCCAAGTCGTTAAGGAAAGAAAATCCCAA AGTCACTCTTCAGCGTGGAGAAGAAGAAGAAAATCAAGGGAACAAAGAATCTAAAATCAAGCAT---------GTTCATA CAACTGCAAGCTGTCCTAAGGTTTGTCAGGAAGACGCAAAGCCTAGTGATCGTACCAACTGTAGTGTTAAAGGACTCCCT AGGCTTTGTCACTCATCTCAATTCAAA---GGTAGTGAGTCTGAACTCATTACTGAAGGTGAACATGGAATTGCACAAAA CCCAGATCAGATGCCATCATCTTCTCCCATCAGATCATCTGTTAACTCTAAGTGTAACAAAAAC---CTGTCAGAAGAGC GATTTAAGGAACATAAAATATTATTTGAAAGAACAACAGGAAATGAAACCATTGTCCAAAGTACAGTGAGCACAGGTAGC CAAGATAACATTAGGGGAAGTGCTTCGAAAGAAACCAGCTCAAGCAGTATTAATGAAGTAGGTTCTAGT----------- ----------------------------------------------------AGCGAAAACATTCAAGCAGAAATAAGTA GGAACAGAGAACCTAGATTAAATGCTGTGCTCAGGTTAGGTGTTATGCAACCTGAAGTGTATAAGCAAAGTCTT---TCT ATAAGTAACTGTAAACAGCCAGAAATAAAAAAGCAAGGAGAAAATGAAGGAGTAGTTCAGGCTGTTAGTACGGATTTCTC TGCATGTCCCATTTCAGAAAACCTAGAACAA---CCTGTGAGAAGTAGTCACACTTCTCAGGTTTGTTCTCAGACTCCTG ATAACCTGTTAGATGATGATGAAATAAAGGGAAAGACTGACTTTGCTGGAAGTAGCATTAAGGACAGACCT---GTCTTT AGTAAAGATGACCAGGGAAGAGAGTTCAGAAGGAACCCAAGCCCTTTGTCCCAT---TCAGGTTTGGCTCAGGGCCACCT GATAGGGGCCAGGGAATTAGAGNCCTCACAAGAGAACACATCTAGCAA- >Aardvark TGTGGCACAAATACTCATGCCAGCTCGTTACAGCATGAGAACAGCAGTTTATCACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGTGAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCTTAGCACAGGGGAAAAGGTAGATATGAAGGCTGATCCCTTGTATGGGAGAAAAGAA GTGAATAAGCAGAAACCTCCATGCTCTGAGAATCCTAGAGAT---ACTGAAGAT---ATTCCTTGGATAACACTGAATAG TAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGGCCTG---------GATGGCTCACATGATGAAGGGTCTG AATCAAATGCAGAAATAGGTGGTGCATTAGAAGTTTCAAAT------GAAGTACATAGTTACTCTGGTTCTTCAGAGAAA ATAGACTTAATGGCCAGTGAACCTCATGTTGCTTTAATATGTGAAAGTGAAAGAGTCCTCTCCAAACCAGCAGAAAGTAA C---ATCGAAGATAAGATATTTGGGAAAACCTATCGGAGGAAGTCGAGTCTCCCTAACATAAGCCATATAACTGAAGATC TAATTTTAGGAGCTATTGCTACAGAACCTCAGGTTGCACGA--------------------------------------- ---------------------GAATGTCCTCTTGGAAATAAATTAAAGCGTAAAAGGAGA---ACATCAGGCCTTCATCC TGAGGATTTTATTAAGAAAGTAGATTTGGCAGTT---CAAAAGACTCCTGAAAAGATAAATCAGGAAAATGACCAAATGG AACAAAAT---------GGTCAGGAGGAGAATATTGCTGATGGCTGTCATGAGAATGCAACAAAAGGTGAATAC---ATG CAGAAAAAGAAGAGTGCAAATCCAACAGAA------TCATTGGCAACAGAATCTGTTTTCAGAACTAAAGCTGAACCTAT AAGCAGCAGTATAAGCAATTTGGAACTAGAATTAAATACACACAATTCAAAGGCATCCAAGAAGAATAAGCTGAGGAGGA AATCCTCTACCAGGCATATTCATGCACTTGAACTAGTAGTCAATAGGAATCCAAGTCCCCCTAGTCATACTGAGTTACAG ATTGATAGCTGGCCTAGCAGTGAAGAG---TTAAAGAAAAAA---AGTTCTGAGCACAAGCCAATCAGACAGAATACAAA CCTGCAACTCATGAAAGATCAAGAGGCCACAACTGGGGCCAAGAAGAGTAACAAGCCAAATGAACAAATAAGTACAGGAC ACGCCTCTGACATTTTCCCAAAATTGAATTTAACAAACATAACTGGTTTTATTACTAATTGTTCAAGTTCTGATAATCTT CAAGAATTTGTCAATCCTAGCCTTCAAGAAGAGGAAATAAAAGAGAAC---CTGGGAACAATTCAAGTGTCTGATAGTAC CAGAGATCCTACGGATGAGGTGTTAAAC---AGAAGA---GGTTTGCAA---ACTGAAAGATCTGTGGAGAATACCAGTA TTTCAGTGAAACCTGATACTGATTATAGCACTCAGGACAGCATCTTATTACTGAAAGCTAACTCCCTAAGG---AAGGCA AAA---ACAGCACCA------------AGTCAGTGTGCAGCAATTGAAAATCCTAACAAACTTAGCCATGGTTTT---CC TAAAGATACCAGAAATGACATAGAGGGTTTTAAGGATCTATTTAGAGGTGAAGATAAC---CACGTTCAGGAGACATACA TAGAAATGGAAGAGAGTGAACTTGATACTCAGTATTTACAGAATACATTCAAGGTTTCAAAGCGTCAGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATTTGCAACAGTCCATGCCCACTCCAGGTCCTTGAGGAAACAAAGTCCAAA CATCACTCTTGAGTGTGGAGAAAAAGAAGAAAATCAGGGGAACGAGGAATCTAAAATCAAGTGCGTACAGCTAGTTCTTT CAACTACAGGCTATGCTGGAGCTTGTCAGAAAGAGAAG---CCAAGTGATTATGCCAAATGTAGCATTAAAGGAGTCTCT AGACTTTGTCAGTCATCTCAATTCAGA---GGCAATGAATCTGAAATCATTACTGCAAATGAACATGGAGTCTCACAAAA CCTGGATCAGACACCATCACTTTCTCCCACTAGGTCATCTGTTAAAGCTAAATGTAAGACAAAT---CTGTCCAAAGAAA GATTTGAGCAACAGAAAATATCACATGAAAGAGTAATGGGAAATGAGAGCACCATTCAGAGTACAGTGGGCACAGTTAGC CAAAGTAACATTAGGGAAAGTGCTTTTAAAGAAGCTAGCTCAAGCAGTATTAATGAGGTAGGTTCCAGTGTTAATGAAAT AGGTTCTAGT------------------------------------------GGTGAAAACATTCAAGCAGAACTAGGTA AGAACAGAGGACCTAAATTGAATGCTGTGCTCAAATTATGTCTTATGCAACCTGAAGAGTATAAACAAAGTCCT---CCT ATAAGTAATTGTAAACATCCTTCAATAAAAACCCAAGGACAAAATGAAGGAGTAGTTCAGGCTGTTAATACAGGTTTTTC TTCATGTCTGATTTCAGATAACCTAGGACAA---CCTATGGGAAGCAGTCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGACAAAATAAAGGAAAATACCAGCTTTGCTGTAAGTGGCATTAAGGAAAGATCTGCTGTTTTT AGTAAAGATGACCAGGAAAGAGAGTTCAGAAGGAGCCTGAGCCGTTTCTCCCAT---TCAAGTTTGGCTCAGGTTCACGT AAGAGGTGCCAGGGAATTAGAGTCCTCAGAAGAGAACATATCTAGTGAG >GoldenMol TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTGTAATAAAAACAAACAGTCTGGCTTAGCGAGGAGCCAGCAGAGCAGATGGGCTGGAAGTAAGGCAG CGTGCAATGAC---AAGCAGACTCCTAGCACACAGACAGAGCTATATAGGAGTGCTGGTCCCATGCACAGGAGAAAAGAA GTAAATAAGCTGAAATCTCCATGGTCTGAGAGTCCTGGAGCT---ACCCAAGAG---ATTCCTTGGATAACACTGAATAG TAGCATTCGGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGGCCTG---------GATGAGTCACATGATGAGGGGTCTG AATCAAATGCAGAGGTTGCTTGTAAATTCGAATTTCCCAAG------GAAGTACAAGGATATTCTAGTTCTTCAGAGAAA ATGGACTTAATGGCCAATGATCCTCATGATGCTTTAATATGTGAAAATGAGAGAATCCTCTCCAAACCAGCTGTAAGTAA C---ATTGAAGATAAAATTTTTGGGAAAACCTATCGGAGGAAGACAAGTCTCCCTAACTTGAGCCATGTAACTAAAGACC TAATGATAGGAGCTGTTGCTACAGAAGCTCAGATGGCACAA--------------------------------------- ---------------------GAACGTCCTCTTACAAATAAATTAAAGCGCAAAAGGAGA---ACCTCAGGCCTTCATCC TGAGGATTTTATCAAGAAAGCAGGCATGAAAGTTGTTCAAAAGACTCCTGAAAAGATAAATCAAGAAACTAACCAAATGG AGCAGAAT---------GATCAAGTGAGGAATATTGTTGATGGTGGTCATGAAAATGATACAAAAAGTGATTAT---GTT CAGAAAGAGAAGAGTGCTAACCCAGCCAAA------TCATTGGCAAAAGAGTCCGCTTTCACAACTAAAGCGGAACCTAT AAGCAGTAGTATAAGCAATATGGAACTAGAATTAAATATGTACAATTCTAAAGCACTGAGGAAGAATAGGTTGAGGAGGA AGTCCTCTTCCAGGCATATTCACACACTTGACTTGGTAGTCAATAGAAATCCAAGTCCACCTCCTTACACTGAACTACAG ATTGATAGTTGGCCTAGCAGTGAAGAA---ATAAACGAAACA---AGTTCTGAACAAAAGCCAACCAGACACAGCAGAAA CCTTCACCTCATGAAAGAACAGGAAACTGCAACTGGAACCAAGAACAGTAACAAGCCAAATGAACAAATAAGTAAAAGAC ATGCCACTTACACTTTTTCAGAACTAAACATAACAAATAGAACTGACTTTATTACTAACTGCCCACGTTCTGATAATCTT CAAGAACTTGTCAATCCTAGCCTTCAAGGAGAGGAAAGGGAAGAGAAA---TCGGAAACAATGCAAGTATATGATAACAC CAAAGAACCTGAGGATCAGGTGTTAAGTGGAAGAAGG---GATTTGCAA---ATGGAAAGATCTGTTGAGAGTACCAGTG TTTCAGTGATACCCGATACTGATCATGGCACTCAGAACCACACCTCATTACCGGAAGCTGGCACCCTCGGG---AAGGCA GAA---ACAGCACCAAATCAATGTACAAGTCAGTGTAAAGCAATTGAAAATCCCAACCAACTTATCCATGGTTGT---CC T---------AGAAATGACACAGAGGGCTTTAAGGATCTATTGAAACATGAAGTTAAA---CACAATCAGGAGACATGCA TAGAAATGGAAGAGGGTGAGCTTGATATTCAGTATTTACAGAATACATTCAAGGTTTCAAAGCGTCGGTCATTTGCTCTG TTTTCAAATCCA---------GAAAAGGAATGTGCAACAGTCAGTGCCCACTCTAGGTCCTTCAGGAAACAAAGTCCAAA AGCCACTCTTGAATGTGGCGAAAAAGAAGAAAATCAGGGGCACAAAGAGTCTAAAGTCAAGCATGTACAGGCAGTTCATA CAAGTGTGGGCTATCCTGGACTCTGTCAGAAAGAGAAG---CCAAGTGATTATACCAAAGGTAGCATTCAGGGGGCCTCT AGGCTTCATCAGTCATCTCAGTTCAGT---GGCAATGAATCTGAACAAATTACTGCAAATGAAAATGGAATTTCACAAAG CCCAGATCAAACAGCATTGCTTTCT------------------------AAATGTAAGAAAAAC---TTGTCTGAAGAAA GATTTGAAGAACGTGCAGTATCACTCGAAAAAGCAGTGGGAAATGAGAGCATCATTCAAAGTACAGTGAGCACAGTTAGC CACAATAACATTAGGGAAAGGGCTTTTAAGGAAACCAGCTCAAGTAGTACTAATGAAGTAGGTTCCAGTATTAATGAAGT AGGTTCTAGT------------------------------------------AACGAAAACATCCAAGCAGAGGTAGGTA GGAACAGAGGACCTAAGTTAAATGCTATGCTCACATTAGGTTTTATGCAACCTGAAGTCTATAAACAAAATCTT---ACT CTAAGTAATTGTAAACATCCTGAAATAACAAAGCAGGGAGACAATGAAGAAAGAGTTCAAGCTGCTGACCCAGGTTTCTC TCCGTGTCTAATTTCAGATAACCTAGAACCA---CCTATGGGAAGTAATCATGCTTCTCAGATTTGTTCTGAGACACCTG ATGACCTGTTAGATGATGATGAAATCAAAGAAAATATCAGCTTTGCTGAAAGTGGCATTAACGAAAGATCTGCAGTTTTT AGTAAAGATGACCATAGAAGACAATTCAGAAGGAACCTAAGCCCTTTATCCCAT---TTAGGTTTGACTCAGGGTCACTT AAGAGGTACCAGAGAATTAGAGTCTTCAGAAGAGAACCTGTCTAGTGAG >Madagascar TGTGGAACAAATACGCTTGCCAACTCATTACAGCGTGAGAACTACAGTTTATTACTCACTAAAGACAGACTGAATGTAGA AAAGGCTGGATTCTGTAATGAAAGCAAACAGCCCGGCTTAGCAAGGAGCCAACATAACAGATGGGCTGAAAGTAAGGAGA CATGTAATGAC---AAGCCGACTCCTAGCACAGAGAAAAAGGTAGATAAGAATGCTGACCCCGTGCATGGGAGAAAAGGA GTGCCTAAGCAGAAACCTCCGTGCCATGGAAGTCCTAGAAAG---AGCCACGAT---GTGCCTTGGAAAACACGGAAGAG TAGCATTTGGAAAGTCAATGAGTGGTTTTCCAAAAGCGATGGCCTG---------GGTGACTCGCATGATGAGCGGCCTG AATCAGATGCAGACGTCGCTGGGGCCTTCGAAGTTCCAGAT------GAAGCACGCGAATCTTCTAGTTCACCAGAGAAA ACAGACTTGATGGTCAGTGATCCTCATGTTCCTTTACTGCCTAAAAGTGAAATAGTCCTTTCCAAACCAGTAGAAAGTAA C---ATCGAAGACAAAATATTTGGGAAAACGTATCGGAGGAAGTCAAGTCTCCTTAACTCGAGCCATGTAACCGAAGATC TAATGGTAGGAGCTGCTGCTGCTGTAGCACCTCCGATAGCG--------------------------------------- ------------------CAAGAGCACCTTCTTACCAGTAAATTAAAACGC---AGAAGA---TCATCCGTCCTCCACCC TGAGGACTTTATCAAGAAAGCAGATTTGGCAGTTGTTGAAAAG------------ATCAACCAGGGAACTGACCAAACAC GGCCGAAG---------GGTCAAGTGGAGCATACTCCCGATGGTGGTCACGGGCATGGAACAAAAGGCGGGAAT---TGT GTTCAGAAGGAGAGTGCTGACCTGGCAGAA------TCATCGGCCAAAGAATCTGCCTTTCGAACTGTAGCTGAGCCTTT GAGCAGTAGGAAAAGCGGTATGGAACTGGAACCGACTGTGTTCAATGCAAAAGCACAN---AAGAACAGGCTGAGGAGGA AGCCCTCTGCCAGGCACATCCACCCCCTGGAACTA---GTCAACAGACAGCCAAGCCCGACGAGGCACACAGAACTGCAA ATCAGTAGCTGGCCTAGCAGTGAAGAG---CTAAAGAAAGAA---GAATCTGAGCCAAAGCCAGTCAGACGGAGCCGGAA CTTACAGCTCACGAAAGACCAAGAAATGGCCATCGGAGCCAAGAAGAATGACAAACCAAATGAAGAAAGGGGCAAAAAAC ACGCCCCTGACACTTTCCCAGAACGAAATTTAACAGACACAACTGACCTAATTACTACCTGTTCGTTTTCTGGTAATCTT CAAGCATCTGTCTGTCCTAGCCTTCCAGGGGCAAACCTGAAAGAGAAA---CTGGGAACAGTGCAGGTGTCTAATGGTAC CAGAGACCCCAAAGGTGAGGTGTTGGGTAGAGCAAGG---GGCCTGCAA---ACCGCAAGATCTGTTGAGAGTACCAGTG TTTCACTGATACCTGATGCTGATGATGGCCCTCAGGGTACCATCTCAATCATGGAAGCAGGCAACCGAGGG---GAGGCA CAA---ACAGCACCAGGTGAATGTGCAAGGCAGTGTGCAGCAACTGAAAACCCCAACAGGAGTGTCCAGGGTCTT---TC CAAAGACACTAGAAATAACAGGGAGGGCTCTGAGGATCTCTTGACACACGACGTTAAC---CACCTTCAAGAGACATGCA CAGAAATGGAAGAGAGTGAACTTGATATTCAATCCTTACAGAATACATTCAAAGTCTCAAAGCGTCAGTCATTTGCTCTG TCTTCAAATCCA---------GAAAAGGAGTGTGCCTCGCTCGGTGCCCGCCCT---------------------CCAAA CATTGCTCTTGAGTGTGGGGAA---GAAGGGACTGAGGAGAACAAAGAGTCTGCAGTTCGACCTGTGCAGGCAGTTCCTG CAACTGTGGGCGGTGCTGGAGGTAGTCCAACAGAGCAACAGCCACGTGATTATACCAAAAGTCACCTTCAAGGAGTGTCC AGGCTTGGTCAGGCAGCTCCATTCAGA---GGCAGTGAATCTGACCCCAGTACCTCAGTTGAACATGGGATTGTGCAGAC CTCGGAGCNTGCCCCACCACTCGCTCCCATCAGGTCATCTGTTAACAGTGAACGTAAG------------------GAAA GACTGGAGGATCATGCAGAATCACTTGAAAGAGCCTCAGAAAACGAGAGTATCATTCCAAGTACAGTGAGCACAATTAGC CAAAATACCCCGAGAGAAAATGCTTTCAAAGGAACCAGCTCAAGCAGTCTTAATGAAGTAGGTTCTAGC----------- ----------------------------------------------------AGCGAAAACATTCAAGCAGAGCTAGCTA GGAACAGAGAACCTAAATTGGATGCTGTGCTCAGGTTGGGTCTTGTGCAACCTGAAGTCTGTAAGGAAAGTCTT---CCT ATGAGGAAATGTAAGCATTCTGAAGTAAAAAGGCAAAAAGGCAATGGAGGACTAGTTCCAGCTGTTCATCCAGATTTCTC TCGCTATCTAATGTCACATAACCCAGAGCAG---CCTATGGAGGACAAT---GATCCTCAGATTTGTTCTGAGACACCTG AAGACCTGTTAGATGACAGTGAAATAAATAAAAACAGCCACTTGGTTCAAAGTGACATTAGGGAAAGATCTGCTGTTTTT AGCAAAGATAACCAGAGAAGAGATTTCAGAAGGAGCCCTGGGCCTACATCCCAT---TTAGGTTTGTCTTGGGGTCACCC NAGAGGTGCTGAAGAGTTAGAGTCCTTAG-------------------- >Tenrec TGTGGCACACGTACGCTTGCCAGCTCGGCACAGCGCGAGGACTGCAGCTTATTACTCACCGAAGACAGACTGGATGGAGA AAAGGCTGGATTCTGCAAGGAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAGCATAGCAGATGGGCTGAAAGTAGGGGAA CGGGTAATGAC---AAGCCGACTCGTAACACAGAGGAAGAGGTAGTTGTGAGCGCTGACTCCGCGCATGGGAGGAAAGGA GTGTCTAAGCAGAAACCTCCGCGCCAGAAAAGGCCCAGAAAG---AGCCATGAT---GTGCCTTGGAAAACACGGAAGAG CAGCATTTCGAAGGTTAACGAGTGGTTTTCCAAAAGCCACGGCCTG---------GGTGACTCTCGCGATGGGCGGCCTG AGTCAGGCGCAGACGTAGCTGTAGCCTTCGAAGTTCCAGAC------GAAGCATGTGAATCTTATAGTTCTCCAGAGAAA ACAGACCTGATGGCCAGGGACCCTCCTATTCCCTTACTGCATAAAAGCGCAAGAGTCTTTTCCAAACCAGTAGAAAGTAA C---ATCGAAGACAAAATATTTGGGAAAACGTATCGGAGGAAGTCAAGCCTCCTTAACTCGAGCCATGTAACTGAAGATC TGGTGCTAGGAGCTGCGGCTGCCGTAGCGCCTCCGGTAGCA--------------------------------------- ------------------CAAGAGCAGCTTCTTACCAGTAAATTAAAGCGCAAGAGGAGA---TCCTTCGTCCTGCACCC TGAGGACTTCATCAAGAAAGCAGATTTGGCTGTCGTTCAAAAGACTCCTGAAACGATCCATCAGGGACCTGACCCAATGC AGCCGAAG---------GGGCCAGTGCAGAAGATTCCCGATGGTGGTCCCGGGCGAGGGACAAAAGGCGGGGATCGTGTT CCGAAGGAGAAGAGTGCTAACCTGGCAGAA------ACATCGGCCAAAGAATCTGCCTTTCGAACTGTAGCTGAACCTTT GAGCAGCCGGAAAAGCAGTATGGAACTGGAGTTGCCTGTGTTCAGTTCAAGAGCGCAG---AAGAACAGGCTGAGGAGGA AGCCCTCTGCCAGGCACATCCATCCGCTGGAACTA---GTCAACAGACAACCAAGCCCGACTACGCACACGGAGCTGCAA ATCAGCAGTTGGCCTAGCAGTGAGGAG---CTAAAGAAAGAA---GAATCCGAGCCAAAGCCAATCAGACGGAGCCGGCA CTTAAAGCGGGAAAGA---------------------GCCAAGAAGAATGACAAGCCAAACGGAGAAAGGGGCAAAAAAC ACGCCCCTGACACATTCCCAGACCGAAAGTCAACAAGCACAACGGACTTAATTGCTAACTGTTCCGTTTCTGGCAGTCTT CATGGAGCTGTCTGTCCTGGCCTTCCAGGGGCAAAGCAAACAGAGAAA---CGGGGAGCAGTGCAGGTGTCTAATAGCGC CAGAGACCCCAAAGATGAGGTGTTGGGTAGAGCAAGG---GGCCTGCAA---ACTGCAAGATCTGTTGAGAGCACCAGTT TTTCACTGATACCTGCTGCTGCTGACGGCACCCCGGGTGGCGTCTCAATAATGGAAGCAGGCAGCCGAGGA---CAGGCA CAA---ACAGCACCAGATCCATGTGCAAGGCAGTGTACAGCAACGGACAACCCCAGCGAAAGTGCCCACGGTCTT---TC CAGAGATACTAGAAACAGCACGGAGGGCTTTGCGGATCTATTGACACATGGCGTTCCC---CACATCCAAGAGACATGCA CAGAAATGGAAGAGAGTGAACTTGATATTCAGTCTCTACAGAATATGTTCAAAGTCTCCAAGCGTCAGTCATTTGCTCTC TCTTCAAATCCA---------GAAAAGGAGTGTGCCACACTCTGTGCCCATCCTAGCGCCTTCAGGAAACAGAATCCAAA CGATGCTCTTGAGGGTGGAGAA---GAAGGGACTCGGGAGAACAAAGAGTCTACAATTAAGCCTGTGCGGGCAGTTCATA CCACCGTGGGC------------------ACGGAGAAGCAGCCAAGTGATTACACCAACAGTAGCTTTCAAGGAGTTTCT AGGCGTGGTCAGGCATCTCAATTCAGA---GCCAGTGAATCTGACCCCGGTACCTCAGTCGAACACGGAAGTTTGCAAAC CACAGAGCATACCCCACCACTCTCTCCCATCAGATCCTCCCTTAAAAGTAAATGTAAGGAAAAG---CTGTCAGAAGAAA GATTGGAGGATCAGGCAGAATCCCTTGAAAGAGCCTCGGGGAATGAGAGCATCATTCAAAGTACAGTGAGCACAATTAGC CAAAATACGCTTAGAGAAAATGCTTTTAAAGGAACCAGCTCAAGCAGTCTTAATGAAGTAGGTTCTAGC----------- ----------------------------------------------------AGCGAAAACATTCAGGCAGAACTAGCTA GGAACAGAGAACCTAAATTGGATGCTGTGCTCAGACTAGGACTTGTGCAGCCTGAAGGCTGTAAGGAGGAAAGTCTTCCC CTAAGGAAATGCAAGCATCCTGAAGTAAGAAGGCAAAAAGGCAATGGAGGACTAGTTCCAGCTGTCAATCCGGATCTCTC CCGCTATCTAATGTCACGTAACCCGGAGCAA---CCGATGGAGAGCAAC---GGTTCTCAGATTTGCTCGGAGACACCTG AAGACCTGTTAGATGACAGTGAAATAAAAAATAACAGCTACTTTGTTCAAAGTGACGTTAAGGAAAGGTCTGCAGTTTTT GGCAAAGATAACCAGAGAAGAGATTTCAGAAGGAGCCCTGGGCCTACATCCCAT---TTAGGTTTGACTTGGGGTCACCC AAGGGGTGCGGAAGAATTAGAGTCCTTAGAAGAAAGCGAAGCCAGTGAG >LesserEle TGTGGCACAGATCCTCATGCCAGCTCATTACAGCATGAGAGCAGCAGTTTATTACTCACTAAAGACAGAATGAATGTAGA AAAGGCTGAATTCTATAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAA CATGTAATGAT---AGGCAGACTCCTAGCACAGAAAAAAAGGTAGCTGTGAGTGCTGATCTTTATTATGGGATAAAAGAA GTGAATAAGCAGAAATCTCCGCACTCTGAGAGTCCAAACAAT---AACCAGAAT---ACTCCTTGGATGACATTGAACAG TAGCATTCGGAAAGTTAATGACTGGTTTTCCAGAAGCGGTGGCCTG---------GATGGCTGCCATGAT---AGATCTG AATCGAATGTAGTAATAGCTGGTGAAGTGGAAGTTCCAAAT------GAAGTACACGGATATTCTATTTATTCAGAGAAA TTAGACTTAATGCCCAACAATCCTCATGATACTTCACTGTATGAAAGTGAAAGAACCCTCTCCAAACCAGCTGAAAACAA C---ATTGAAGATAAAATATTTGGGAAAACCTATCGGAAGAAGGCAAGTCTCTCTAACTTGAGCCATGTGACTCAAGATC TCATTACAGGAGCTGTTGCTACAGAATGTCAAACAGCACAA--------------------------------------- ---------------------GAACGCCCTCTTACAAATAAATTAAAGCGTAAAAGAAGA---ACATCAGGCCTTCAGCC TGAGGATTTTATCAAGAAGGTAGATTTGACAGTTGTTCAAAAGATTCCTGAAAAGACAAATCAGGAAACTGAACAAATGG TGCAGAAT------GGTCAAGTGAAGAATAAGATTGCTAGTGGTGGTCATGAGAGTGATACAAAAGGTGATTAT---GTT CAGGAAGAGAAGAATACTAACCCAACAGAG------TCACTGGTAAGAGAATCTGATTTCAGAAATAAAGCTGAACCTAT AAGCAGCAGTATAAGCAAAATGGAGCTAGATTTAAATACACACAGTTCGGTAGCACCAAAGGAGAACAGGCTGAGGAGGA TGTCCTCAACCGGGCATGTTCACGCACTTGAACTAGTAGTCAATATAAGTCCAAGGTCACCTATCCATACTGAAGTACAG ATTGATGGTTGGTCTAGCAGTGAAGAG---AAAACGAAAAAA---CATTCCGAGCATAAGCCAGTCAGACCTAGCAGAAA CGTTTCAGTTACAACAAATCAAGAAACTGAAACTGGGTCCAAGAAGAATAACAAGCCAAATGAACAAATGAGTAAAAGAC ATGCCACTGACACTTCCCCAGGGCAC---------AACATAACTGGCTTCATTACTGACTGTTCAAATACCAGCAGTCTT CAAGAATGTTTTAATCCTAGACTTCAGGGAGAGGCAGTAGAAGACAAC---TTTGGACCAGTTCAAGTGTCTAATAGTAT CAAAGATCCCAAGGATCAAGTATTAAATAGAGGAAGG---AGTTTGCAGCACACTGGAAAATGTGTTGAGAGTACCAGTA TTTCAGTGTTATCTGATACTGATTATGGCACTCAGAATAGCATCTCATTACTGGAGGCTGGCACCCTAGGG---AAGGCA GAA---ACAGCACCAGATCAATGTGCAAGTCACTGTGCAGCAATGGAAAACTCCAAGAAACATGTTCATGACTTT---CC TAAAGATACTGGAAATGATACAAAGGGTTTAAAGGACTCATTGAAATGTGATGCTAGC---CACAGTAAGGATACGTACA TGGAAATGGAAGATAGTGAACTTGATACTCAGTATTTACAGCATACATTCAAGGTTTCAAAGCGTCAGTCATTTTCTCTG TTTCCAAATCCA---------GAAAGGGACTATACA-------------------------------------------- ----------------------------------------------------------AAGCTAGTGCAGCTAGTTCATA CCACTGCGAGCTATCCTGGAGCCTGTGAGGAAAAGAAA---CCAAATGATTTTACCAAATGTAGCATTAAAGGAGTCACT AGACTCTCTCATTCATCTCCATTGAGA---GGCAATGAATCTGAACTCATTACCGCAAATGAACATGAAATGTCACATAG CCCAGATCAGACACCGTCACTTTCTCCCACCAAGGTTCCTGTTAAAATGACATGGAAGAAAAACCACCTGTCAGAAGAAA ACTTTGAGGAACATACAATA------------ACAATGGGAAATGAGAGCATCGTTTATAGTCCAGTGAGCACAGTTAGC CAAAGTAATGTTAAGGGA------TATGCTCAGGCCAGTTCAAGCAGTATTAACGAAGTAGGTTCCAGTACTAATGAAGG AGGTTCCAGTATTAATGAAGTAGGTTCTAGT---------------------GGTGAAAACATCCAAGCAGAGCTCGATA GGAACAGAGGACCTGCATTAAATGCTGTACTCAGATTAGGTCTTGTGCAACCTGAAGTCTATGAACAAAGTCTT---CCT GCAGGAAATTGTAAACATCCTGAAATGACAAGGCCAGGAGACAGTGAAGGAGAAAAAAAGACTGTTAATCCAGATTTCTC TCCATGTCTACTCTCAGATAATTTAGAACAA---CCTAGGGGAAGTTGTCATGCTTCTCAGATTTGTTCTGAGACACCAG ATGACCTGTTTGGTGATGATGAAATAAAGGAAAATAGCAGCTTTGGTGAAATCAGCTTTAAAGAGAGATCTGCTGTTTTT AGTAAAGATGAGCAGGCCAGAGAGTTCAATAGGAACTCAAGCCGTTCATCTCGT---TCAGATTTGGCTCCCAGTCATCT AAGACGCGATAGGGAATTAGAGTCCTCAGAAGAGAATGTATGTA----- >GiantElep TGTGCCACAAGTATTCATGCCAGCTCATTACTGCATGAGAACAGCAGGTTATTGCTCAGTAAAGACAGAATGGATGTAGA AAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGGAGCCAACAGAGCAGATGGACTGAAAATAAGGAAA CATGTAATCAT---AGGCAGACTCCTAGTATAGAGAAAAAATTAGATATGAATGCTGATCCCTCTTATGGGCGAAAAGAA GTGAATAAGCAGAAATATGAATGCTCTGAAAGCCCTCAAGAT---ACCCAAGAT---ATTCCTTGGATGACACTGAATAG TAGCGTTCAGAAAGTTAATGCATGGTTTTCCAGAAGTGATGGCCTC---------GATGATGCCCCTGATGTAGGGTCTG AATCAAATACAGCAATAACTGGTGCACTGGAAGTTTCAAAG------GAAGTACATGGATATTCTAGTTCTTCAGAGAAA GTACACACAATGACTGGTGATCCTCAGGGTGCTTCAGTATGTGAAAGTGAAAGAGTCTTCTCTAAACCAGCAGAAAATAA C---GTTGAAGATAAAATATTTGGGAAAACCTATCGGAGAAAGTCAAGTCTCCCTAACTTGAACCATATAACGCAAGATC TCATTGTAGGAGCTGTTGCTACAGGACCTCAGACAGCACAA--------------------------------------- ---------------------GAACACCCTCTTATAAATAAACTAAAGCGTAAAAGGAGA---ATATCTGGCCTTCGTCC TGAAGATTTTATCAAGAAGGTAGATTTGACT---AGTCAGAAGACTCCTGAAAAGAGAAATCAGACCATTTACCAAGTGG AC------------------------AGTAGTATTGCTGGCGGTGGTCATGAGCATGAAACGAAAGGTGACTGT---GTT CAGAAAGAGAAGAATGTTAGCCCAGCAGAA------TCATTGACAAAAGCTTCTGCTTTCAGAACTAAGGATGAACCTAT AAGCAGCAGTGTGAGCAGTATGGAACTAGAATTAAATATGTACAGTTCGATAGCACCAAAGAAGAACAGGCTGAGAAGGA AGTCCTCTACTAAGCGTATTTGTGCACTTGAACTAGTAGTCGATAGAAATCCAACTTCACCTACCCATGCTGAGCTAGAG ATTGATAGTTGGCCTAGCAGTGAAGAG---AGAAGGGAAAAA---TGTTCAGAGTTTAACTCCGTCAGACACAGCAGAAC CTTTCAGCTTATGAAAGGTCAAGAAACTGAAATTGGAGACCAAAAGAATAATGACAAGCAAAATGAACAAATAAGTAAAA AACATGCCACTGACACTTCTCCAAAACGA------AACATAACTGGCTTTATCACAAACTGTTCAAATACTGATAATCTT CAAGAATTTGTTAATCCTAGATTTGAAGGAGAGGAAATAGCC---------GTGGGACCAATTCAAGTGTTGAATAGTAC CAAGGACCCTGGAACTCAAGTGTTAAGTAAGGAAAGG---GGTTTGCAC---ACTGGGAATTATGTTGAGAGTACCAGTA TTTCAGTAATACCTGATACTGATGATGGTATTCAAAATAGCACCTCATTACTGGAAGCTGGCACATTAGAA---AAGGCA GAAGAAACAGCATTGACTCAATGTGCAAGTCACTGTGCAACAATTTTAAACCCCAACACACATGTTCATGGCTTT---CC TAAAGATCCTGCAAATGACACAGAGGGTGTTAAGGATCTGTTGAAATGTAATCCTAAC---CACCTTCAGGATACGTGCA TAGAAATGGAAGATAGTGAACTAGATACTCAGTATTTACAGAATACATTCAAAGTTTCAAAACGTCAGTCATTTACCCTG TTTCCAAATCCA---------GAAAAAGAATGTACAACAGTCTGTGCCCACTCCAGGCTCTCAAAGAAACAAAATCCAAG TGTCACTCTTGAGTGTAGAGAAAAAAAAGAAAATCAGGGGAATGAAGAGTCTAAAATGAAACATGTGCAGGTCGTTCATA CATCTGTGCACTATCCGGGAGTTTGTGAAGAAGAGAAGATACCAGATGATCACACCAAAGTTAGCATTAAAGGGATCTCT AGGCTTTGTCAGTCATCTCCTTTCAGG---AGCAATGAATCTGAACGCATTACTGAAAATGAATGTAAAATGTCACATAG CCCAAACCAGACACCATCATTTTCTCCCACCAAGCCATCAGTTAGAACCAAATATAAGCAATTCTACTTGTCAGAAGAAG AGTTGGAGGAACATGCACTATCACCTGAAAGAGCAATGGAAAATGAGAGTTTCATTCACAGTACAGTGAACGCAGTTAGC CAAAATAACCTTAGGGGAAGTGTCTTTAAAGAAGGCAGCTCAAGCAGTATTAATGAAGCAGGATCCAGTACTAATGAAGT AAGTTCCAGTATTAATGAAGTATGTTCTAGT---------------------GATGAAAACATCCAAGCCGAACTAGATA GGAACAGAAGACCTGAATTAAATGCTGTACTCAGGTTAGGTCTTATGCAACCTGAAGTGTATCAACCAAGTCTT---CCA ACAAATAACTGTGAATATCTTGAAATTAGTGGGCCAGGAAAAAATGAAGGCATAATTCAGGCTGTTAATCCAGATTTTAC TCCGTGTCTGATTTCAGATAACTTAGAACAACAGCCTATGGGAAGTAATCATGCTTCTCAGATTTGTTCTGAAACACCTG ATGACCTGTTAAATGATGAGAAAAGAAATGAGAATATCAGCTTTGCTCAAATCAGCATTAAGGAACGATCCACTGTTTTT AGTAAA---GACCAGGCAAGAGAATTCAGAGGAAACCCAAACCCTTTATCCCAT---TCAGATTTGACTCAGAGTCATCT AAGAAGTGGTAGGGAATTAGAGTCATCAGAAGANAA------------- >Caenolest ---------------------NGCTCATTACTGCCTGAGATCACCAGTTTATTGCCCAACACAGACAGAATGAATGTAGA AAAGGCTGAACTCTGTAATAAAAGCAAACAGCCTGGCTTAGCAAGCAACCAGCAGAGCAATCTGGATGAAAGTAAGGAAA TATGTAGTGCTAGCAAGACCCCAGGTGCAGAGGAA---CAGCCTGAGCTGAATGCCAATCATCTGTGTGAGAGGAAAGAA CTAGAGGAG---AAGCTGCAGTGCCCTGAGAGCCCCAGAGGTGATTCTGGGGACTGCCTGTCTGGAACCAAAGTGAAAAA CAGTATTCAGAAAGTTAATGAGTGGTTATTCAGAAGTAATGACGTTTTAGCCCCAGATTACTCAAGTGTTAGGAGCCATG AACAGAATGCAGAGGCAACCAATGCTTTAGAATATGGGCATGTAGAG---ACAGATGGAAATTCTAGCATTTCTGAAAAG ACTGATATGGTGGCTGACAGTACTGATGGTTCCTGGCTACATGCACCTGAAAAAAACTGCCCCAGACCGGCAGAGAGCAA CAATATTGAAGATAAAATATTTGGAAAAACCTATCGAAGAAAGTCGGTTCACCCTGTTTTGAATTACGTAACTGAAAACT TGATTGTTGAAACCATTGCTCCTGATTCTGTGATCCCTCCA--------------------------------------- ---------------------GAGCCTCTCAAAAAAACCAAATTAAAGCGTAAAAGAAAGACCATATGTRACCTGCAGCC TGAGGATTTCATCAAAAAGACAAACATTCCAGTTATTCCCAAAACCCCTGAAAAGAAAAATCACCCTGTTGACCAGATCC TTGAAAAAGAACAAAATGGCCAGGTGATGAGCACAGCTGATGGTCAGTCCAAGCAGAAAGCACCCGGTTGTCAT---GTG GGGGAACTGAAAGAGGCTCAGGCATCAGAACCATCCTCTGTAGAGAAAGGATCCACTTTCAAAACTGGATCCGAGCCTGC AGCTGGTAGCACAAACCAGAGGGAACTTGAATTAAATGGTAGAGATGCAAAAATGACAAAAAAAGACAGACCAAGGAAGA GGCCTTCAGTTAGGACTGTCTGTGCTCTGGAGCTCGTGACTGATAGACACCCAGGCTCTTCTAATGAGACAGAACTACAG ATCGATAGCTATCCCAGCAGTGAAGAG---ATAAGGAAAGGAAATAATTCTGAGCAAAAGCAAGTCAGACGCAGCAGAAG ACTTCAGATGCTGTCAGAA---GAAATTGCAGCAGGAGCCAAGAAGGCCCATAAGCCAGATGAAGAAGTGGAAGAGAATT GTGTCAATGAAGTTTCCCCAGAACTAAAAATAGGAAAAGTATCTGCCTGTTCTACTGACAGTCTAGCTACTGATAGTGAT CTGGTATTAGCTAGCTGCAGTTTCACAGAA---GGAGATGAAAAGAGC---CTGGAAGCCTTCCAG---------CCCAG CAGAGACCAAGAC---AGCCTGGCAGTAAGCAGAGGAGAGAAGTTGCAA---GGGAAAAGAACCAAAGGAAACATGGAGG TTTTGGAGGTTGCCAGTACTGATTGGGACACTCAGGACAGCACCTCATTGTTTCCAACCGATGTTCCCCCA---AATCCA ACA---GCAGACTCTGGCCCACACAGAAATCAACATGAGGTCATAGAAACCCCCAAGGAACTCTTTGATGATTGCTCATC CAAAAACACCGGAAGTGGTAAAGAAGATTTG---------ACAAGACAAGAAGTCAAA---AATACCTCAGAGACAACCA CAGAGATAGAGGACAGTGAACTTGATACCCAATATTTACAGAATACCTTCAAACGTTCAAAACGCCAATCATTTGCTCTG TGTCCCAGTCCAAGG------CAGACAAGTATGAAACTCCGGTCCATCCCTAGAGCTCTAAGTCATCAGAGCCCAGATAA AGCCACGAATCATGGGGGGCAGGAAAAAAAAAAGCAGGGAAGCAGAGAATCAGACAAGGCTGTGCAGCCAGAACTTGCAG TCATGAGTTCAGCTGCGGTTTGTCAGACAAAGGAGAAAAAGCCAGGTGATTATGCCAAATGTAGCACA------GTCTCT AGGCTTTGTCACATAGCTCCATTACATGATGACAATGACTGTGACCACAGTGCTGAAAACAACAAGGGAATTTCACAAGT TCCTGATCAAAAGCAATCTGTCTCTCCAGCAAAGTCATCAGCTGGTAGATCTATATACACAAAAAGCCTCCTGGAGGAAA GACTTGATGAACAGACCACATGTCCCGAAACAGTGATGGGAAATGAAAGCTTAGTCCACAGTAGCTTAAGCCTGGTTAGC CAAAGTAACAGCGGAGAATATATTTCTAAAGCAACTGACTCAAACAGATTTGTTGGTGTAGACTCTAGC----------- ----------------------------------------------------GGAGAAGGTAGTCAGGAAGAAAAAGGTG AAAACAAATTAAATCAGTCCCAAGTCTGTCAACAAAGCTT---------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ------------------------------------------------- >Phascogale ---------------------NGCTCATTACTGCCTGAAACCTCCAGTTTATTGCCCAACACAGACAGAATGTCT---GA AAAAGCTGAGCTCAGTAATAAAAGCAAACAGTCTGGCTTAGCAAGGAACCAACAGAGCAGACTGGATGAAAGTAAGGGAA TATGTAGTACTGGGAAGACCCCCAGTGTAGGAGAG---CAGCCTGAGTTGAATGCCCATCATCTGTGTGAGAAGCAAGAA CTAAAGGAG---AAGCCACAAGGTCCTGAAGGCCCCAGAGGAAATCCTCAGAATTGCCTATCTGGAACCAAACTGAAAAG CAGTATTCAGAAAGTTAATGAGTGGTTATCCAGGAGTAATGACATTTTAGCCTCCGATTGCTCCCTTGACAAGAATCATA AGCAGAATGCAGAGATAGCTAGTGCCTTAGAAGATGGGCATCCAGATAACTCTGATGGAAATTCTAGCATTTCTGAGAAG ACTGACGTTGTGGCTGACACTGCCGATGGAGCCTGGCTACATGTGCCTAAAAGAACTTACACCAGGCCAGCAGAAAACAA CAATATTAAAGATAAAATATTTGGAAAAACCTATCGGAGAAAGTCGGGTCACCCTAATTTGAATCACATAACTGAAAATT TGATTGTTGAGACAGTTGTTCCTGATTCTTTGGTTCCTCCA--------------------------------------- ---------------------GAACCTCTGAAAAATACCAAGTTAAAGCGTAAAAGAAAGACTATATGTGACTTGCAGCC TGAGGATTTCATTAAGAAGACG---GTTCCAGTTACTCACAAAACCCCTGAAAAGAAACATCACGCTGCTGACCAAACCC TTGAAAGAGAACAAAATGACCAAGTGATGAACATGGATAATGGTCATCCTGAACAGAAAGCACTAAATGGTCAT---GTA GGGGAAATTAAAGAAGCTCAGACATCGGAACTGTTCTCTGCAGAGAAAGAATCCACTTTCAGAACTGGAACAGAGCCTGC AGCTAGC------------------------TTAAATGGTGAAGAAACCAACATAACAAAAAAAGACAAGCTGAGGAAGC AGCCTTCATTCAAGATTGTCTGTGCTCTTGAGCTTGGAGTTGATGGAAGCCCAAGCTCTTCTAATGAGACTGAACTACAG ATCGATAGCTATCCCAGCAGTGAAGAG---ATAAGGAGAAAAAATAATTCTGAACAGAAGCAACTTAGACGCAGCAGAAG ACTTCAGCAGGTGTCAGAA---GAGATTGCAATTGGAGCCAAGAAAGCCCATAAGCCAGATGATCAAGCAGAAGAAAGTT GTATCACTGAAGTTGTCTCAGAACTAAAATTAGGAAATCTACCTGCCTGTGCTCCTGACAATGTCAGTACTGATAAGGAT CAAGTATTAGCTAGTTGCAGTTTCACA------GGAGATGAAAGGAGT---CTGGAAGTAATCCCT---------AGCAG CCAAGACCAAGAT------TTGGCATTGAGTGGAAAGGAAGGGTTGCAA---GGTGAAATATCTCAAGGAAGCCTGGGGA CTGTGGAGGTTCCTGATACTGCTTGGGACACTCAGGACAGTACCTCGTTGTTTCCAGCTGATACTCCTCAA---AATTCA AAA---CCAGGTCCCAGTCCTCACAGAAATCACTGTGAAATAATGGAAACCCCCAAGGAACTCTTAGAAAGTAGTTCATC CAAAAACACTGAAAGTAGTGTAGAAGATTTAAGGAGCCTGATGAAACAAGAAGTTAAA---AATGCCTCCAAGACAATCA CAGAAATGGAGGATAGTGAACTTGACACCCAGTATTTACAGAATACTTTCAAGCGTTCAAAGCGCCAGTCATTTGCACTG TGTTCTAGCCCAAGG------CAAGAATATGTGAAAACCTGTGCTGTCCCTGGGGCTATAAGTCAGCAAAGT-------- ----ACAGTTCGTAGGTACCAGGAAAAAGAAAAGCAAGAAAACAGAGAATCAAACAAGCCTGTGCAGCCAAAGCCTGCTG TTGTG---------------------ATAGAGAAGGGGAAGCCAGGTGATCATGCCAAATGGAGCACCAGAGAAGTCTCC AGGCTTTGTCACATAGCTTCATTAAATAGTGGCAATGACTGTGAACCCATTGCTGAAATCAACCAGGGAATTTCACAAGT TCCTGATCAAAACCAATCAGTTTCTCCAGCAAGGTCATCTGATAGTAAAACCATATATGCAAAAAACTTCCTGGAGGAAA GGCTTGATGAGCAAACCACGTGCCCTGAAACAACTATGGGAAATGAAAGCTTAGCCCAAAGCAGCATTAGTTTAGTTAGT CAAAGTAATAGCAGAGAATATGTTTCTAACCCAATTGACTTAATT------ATCAGCGTAGGCTCCAGT----------- ----------------------------------------------------GGAGAAGGCACTCAGGCAGAAAAAAGTG AAAACAAAGAATCTGAATTAAATACACCACCCAAATTAAAGCTTATGCAACCCCAAGTCTCTCAACAGACCTTT---CCT CAGAATAATTGCAAA----------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ------------------------------------------------- >Wombat ---------------NNTGCCAGTTCATTACTTCCTAAAACCAGCAGTTTATTGCCCAACACAGACAGAATGAATGTAGA AAAGGCTGAACTCTGCCATAAAAACAAACAGCCTGCCTTAGGAAGGAACCAAGAGAGCAGTCTCGATGAAAGTAAGGAAA TATGTAGCCCTGGGAAGACCCCAGGTGCCTGTGAG---CGGCATGAGCTGAATGACCATCATCTGTGTGAGAGGCAAGGC CTAGAGGAG---AAGCCGCAGCACCCTGAGAGCCCTAGAGGTAATCCTCAGAACTGCCTGTCTGGAACCAAACTGAAAAG CAGCATTCAGAAAGTTAATGAGTGGTTATCCAGAAGTAGTGACATTTTAGCCTCTGATAACTCCAACGGTAGGAGCCATG AGCAGAGCGCAGAGGTGCCTAGTGCCTTAGAAGATGGGCATCCAGATACCGCAGAGGGAAATTCTAGCGTTTCTGAGAAG ACTGACCCAGTGGCTGACAGCACTGATGATGCCTGGCTACATGTGCCTAAAAGAAGCTGTCTCAGGCCTGCAGAAAAC-- -AATATTGAAGATAAAATATTTGGAAAAACCTATCGGAGAAAGTCAGGTCACCCTAATTTGAATTATGTAACTGAAAACT TGTTTGCTGAAGCTGTGGCTCCTGATTCCTTGATCCCTCTA--------------------------------------- ---------------------GAGGCTCCCAAAAACACCAGGTTAAAGCGTAAAGGAAGGAGCATAGCTGACCTGCAGCC TGAGGATTTCATCAGGAAGACGGACGTTCCAGTTATTCACAAAACCCCTGAAAAGAAAAATCACTCTGTTGACCAAATTC TCAAAAGAGAACAAAGTGACCAAGTGATGAACACGGCTAACAGTCTTCCTGAGCAGAAAGCCCTAGGTGGTCAT---GTA GGGGAAGTGAAAGATGTTCAGGTATTAGAGCTGTTCTCTGCGGAGAAAGAATCCACTTTCAGAACTGGAACAGAGCCTGT AGCTGGCAGCACAAACCATGGGGAGCTTGAATTAAATAGTAGAAATGCCAAAATGACAAGAAAAGACAGGCTGAGGAAGA AGCCTTCAGCCAGGATCGTCCGTGCTCTTGAGCTCGTAGTTGATAGAAACCCAAGCTCTTCTAATGAGAGTGAACTGCAG ATCGATAGCTATCCCAGCAGTGAAGAG---ATAAGGAAAGGAACTAATTCTGAACAGAAGCAAATCAGACGCAGCAGAAG GCTTCGGCTGCTATCAGAA---GAAATTGTGGTTGGAGCCAAGAAGGCCCATAAGCCAGATGACCAAGCAGAAGAAAGTT GTATCAGTGAAGTTTTCCCAGAACTAAAAATAGGAAACGTGCCTGCCTGTGCTACTGACAGTCTAACTACTGATAGGGAT CAAGTGTTAGCTAGCTGCAGTTTCACAGAAGAAAGAGATGAAAGGAGC---CTGGAAGCAATCCCA---------AGCAG CAAAGACCAAGAT------CTGCCCTTGAATGGAGGGGAGGGGTTGCAA---GGTGAAAGAGCCCCAGGAAGCCTGGAGG CTTTGGAGGTTCCTGATACTGATTGGGACACTCAGGACAGTACCTCATTGTTTCCAGCTGATACTCCCCAA---AATTCA AAA---GCAGGACCCAGTCCTCACAGAAGCCACAGTGAAATAATGGAAACCCCCAAGGAACTCTTAGATGGTTGTTCATC CAAAAACACTGAAAGTGACGAAGAAGATTTGAGGAGCCTGATGAGACAGGAAGTTAAA---AATGCCTCCAAGACAACCA CAGAAATGGAGGATAGTGAACTCGACACCCAGTATTTACAAAATACCTTCAAACGTTCAAAGCGCCAGTCATTTGCTCTG CGTTCTAGCCCAAGG------CAGGAATGTAGGAAACCCTCTGCTGTCCCTGGGACTGTAAATCAGCAGAGTCCAGATAA CACCACAGATTGTGGGGGCCAGGAAAAAGAAAAGCAGGGAAACAGAGAATCAAACAAGCCTGTGTGGCCAAAGTCTGCAG TCATGAGCTTAGCTGCGGCTTGTCAGACAGAGGAGAGGAAGCCAGGTGTTTATGCCAAATGTAGCACCACAGAAGTGTCC AGGCTTTGTCACATAGCTCCATTACATGGTGTCATTGACTGTGAACACATTGCTGGAAACAACCAGGGAATTTCGCAAGT TCCTGATCAAAAACCATCAGTTTCTCCAGCAAGGTCATCTGCTAGTAAAACTATAAATACAAAAAACCTCCTGGAGGAAA GGCTTGATGAACAGACCACATGTCCTGAAACAGCTATGGGAAATGAAAGCTTAGCCCAAAGCAGCTTAAGTCCAGTAAGC CAAAGTAATAGCAGAGAATATATTTCTAAAGCAACTGACTTAAATAGATTTATCAGCATAGGCTCTAGT----------- ----------------------------------------------------GGAGAAGGCAGTCAGGCACAAAAAGGTG AAAACAAAGAATCTGAATTAAATACACAACCCAAATTAAAGCTTGTGCAACCCCAAATCTGTCAACAAAGCTTT---CCT CAGGATAATTGCAAAGAGTCTAAAAGAAAAGGGAAGGGAGGAAATGGAAAATTAGCTCAGGCCATCAGTACAGATTCATC TCCATGTTTAGAACAA------------------ACTAAAGAGAGTACACATTCTTCTCAGGTTTGTTCTGAGACACCTG ATGACCT------------------------------------------------------------------------- -------------------------------------------------------------------------------- ------------------------------------------------- >Bandicoot ---------------------NACTCATTAATGCTTGAAACCAGCAGTTTATTGTCCAACATAGACAGAATGACTACAAA AAAGGCTGAACTCTGTAATAAAAGTAAAGATCCTGGCTTAGCAAGGAACCAACAGGACTGTTTGGGTGAAAGTAAAGAAA TAAGTAGCACTGTGAAGACCCCAGCTGCAGGGGAG---CAGCATAAGCTGAATGCCCACCATCTGTGTGAGAAGCAAGAA CTAGAGGTA---AAGTCACAGCACCCTGAGAGCCCCAAAGGTAGTCCTCGGAGGTGCCGGTCTGGAACCAAACTGAAAAG CAGTATTCAGAAAGTCAGTGAGTGGTTGTCTAGAAGTAATGACATTTTAAACTCTGATTATTCCCATGAGAGAAGCCAGG AGCAGAATGCAGAGATTGCTATTGCCTTAGAAGATGGGCATCTGAATACTGCTGATGGAAATTCTAGCATTTCTGAGAAG ACTGNCCTGGTGGCTGNCACCACTGAT---------------GTGCTGGAAACTAGCTTNTCCAGGCCAGCTAAAAGC-- -AATATTGAAGATAAAATATTTGGGAAGACCTATCGGAGAAAGTCANGTCACTCTAATTTGAATTATCTAACTGAAAACC TCCTTGTTGAAACTATTGCTCCTGATTCTTTGATCCCTCCA--------------------------------------- ---------------------GAATCTCTCAAGAACATCAAGTTAAAGCGAAAAAGAAAAACTATATCTGACCTGCAGCC TGAAGATTTCATCAGGAAGACAGATGTTCCAGTTAGTCACAAAACCCCTGAAAAGAAAAATCCTGCTGTTGACCAGATTC TTGAAAGAGAACAAAATGACCAAGTGATGAACACAGCTAATGGTCATCTTAAACAGAAAGCACTGGGTGATCAT---GTC AAGGAAGTGAAAGATGCTCAGGCAGCAGAACTGTTCTCTACAGAGAAAGAATCAACATTCAGGACTAGAACAGAGCCTAA AGCTGGCAGCATAGTCCACGGGGAGCTCGAATTAAATGGGAGAGGTGCCAAAATGACAAAAAAGGACAGGTTGAGGAAAA AGCCTTCAGCCAGGATTGTCCGTGCTCTTGAGCTTGTGGCTGATAAAAACTCAGGCTCTTCTAATGAGGCTGAATTACAG ATCGATAGTTATCCCAGCAGTGATGAG---ATAAGGAAAGGAAATAATTCTGAACAGAAGCAAATCAGACGCAGCAGAAG ACTTCAGCTACTGTCAGAA---GAAATTGCAGTTGGAACCAAGAAGGCCCATGAGCCATATGACCAAGCAGAAGAAAGAT GTGTCAAGAAGGTTTTCCCAGAATTGGAAATGGGAACTGTGTCTGCTGGTGCTACTGACAGCCTATCTACTGATAGGGAT GAAGTGTTAGCTAGTTGCAGTTTCACAGAT---GGAGAGGAAAGGAAC---CTGGAAGTAATCCCA---------AGCAG CAAAGACCAAGACCAAGATCTGGCATTAAGTGAAAGGGAAAGGTTGCAA---GAAAAAAGAACCCAAGGAAACCTGGAGC TTCTGGAGGTTCCAGATACTTATTGGGAAACTCAAGACAGTACTTCACTGTTTCCAGCTGAAACTCCCCAG---AATTCA AAA---GCAGGACCCAGTCCTCACAGAAGTAACTGTGAAATAATGGAATCCTCCAAAGAACTCTTCGATGCTTATTCATC CAAAAACACTGACAGTGGCACTGAAGGTTTG---------ATAAGACAGGAAATTAAA---TATGGCTCTGAGACATCTT CAGTAATGGAGGATAGTGAACTTGACACCCAGTATTTGCAGAATACCTTCAAACGTTCTAAGCGCCAAACATTTGCTCTG TGTTCCAACCCAAAG------CAGGAACAGATAAAACCCTGCTCTGTTCCTAGGGCTGTCAGCCACCAGAGTTCAGATAA TGCCTCAGACTGTGGGGGCCAAGAAAAAGAAAAGCAAGGAAACAGAGAATCAAATAAACCTGGCCAACTAGCATCTGCAG TCAGGAGCTCAGCTGCCACTTGTCAGACA---GAGAGGAAGCCAGGTGATCCTGACAAATGTAGCGCCACAGGAGTTTCC AGACTTTGTCACATAGCTCCATTACAAGAAGGCAATGACTGTGAATACATTGCTGGAAAAAAACAGGGAATTTCACAAGT TCCTAATCAAAAACAATCAGTTTCTCCGACAAGGTCATCAGTTAGTAAA---ATATATACAAAAAACCTCTTGAAAGATA GACTT---GAACAGACCACATGCCCTGAAACAGTTATGGGAAATGAAAGCTTAGGCCAAAGCAGCTTAAGTCCAGTTAGC CAAAATAACAGCAGAGAATATATTTCTAAAGCAACTGACTTAAATAGATTTATTAGCAGGGACTCTAGT----------- ----------------------------------------------------GGAGAAGACAGTCGGGCAGAAAAAGGTA AAATCAAAGAATCTGAATTAAATACACCAACCAAATTCAAACTTGTGCAACCACAAGTATGTCAACTAAGCTTT---TCT CAGGATAATTGCAAAGAGCCAAAAAGAAAAGGGAAAGGAGGAAATGGAATATTAGCTCTGGCCACCAGTACAGATTCATC TCCATGTTTAAAAGAA------------------ACTAAAGAGAGTACACATTCTTCTCAGG------------------ -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ------------------------------------------------- cogent3-scinexus-b16bc06/tests/data/c_elegans_WS199_dna_shortened.fasta000066400000000000000000000001501525761402500261070ustar00rootroot00000000000000>I gcctaagcctaagcctaagcctaagcctaagcctaagcctaagcctaagcctaagcctaa gcctaagcctaagcctaagcctaagcctaagcctaagcctcogent3-scinexus-b16bc06/tests/data/formattest.fasta000066400000000000000000000060621525761402500226160ustar00rootroot00000000000000>Rhesus with extra words tgtggcacaaatactcatgccagctcattacagcatgagaac---agtttgttactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggcttg gcaaggagccaacataacagatggactggaagtaaggaaacatgtaatgataggcagact cccagcacagagaaaaaggtagatctgaatgctaatgccctgtatgagagaaaagaatgg aataagcaaaaactgccatgctctgagaatcctagagacactgaagatgttccttgg >Manatee tgtggcacaaatactcatgccagctcattacagcatgagaatagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtcataaaagcaaacagcctggctta acaaggagccagcagagcagatgggctgaaagtaaggaaacatgtaatgataggcagact cctagcacagagaaaaaggtagatatgaatgctaatccattgtatgagagaaaagaagtg aataagcagaaacctccatgctccgagagtgttagagatacacaagatattccttgg >Pig tgtggcacagatactcatgccagctcgttacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattttgtaataaaagcaagcagcctgtctta gcaaagagccaacagagcagatgggctgaaagtaagggcacatgtaatgataggcagact cctaacacagagaaaaaggtagttctgaatactgatctcctgtatgggagaaacgaactg aataagcagaaacctgcgtgctctgacagtcctagagattcccaagatgttccttgg >GoldenMol tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaaacaaacagtctggctta gcgaggagccagcagagcagatgggctggaagtaaggcagcgtgcaatgacaagcagact cctagcacacagacagagctatataggagtgctggtcccatgcacaggagaaaagaagta aataagctgaaatctccatggtctgagagtcctggagctacccaagagattccttgg >Rat tgtggcacagatgctcgtgccagctcattacagcgtgggacccgcagtttattgttcact gaggacagactggatgcagaaaaggctgaattctgtgatagaagcaaacagtctggcgca gcagtgagccagcagagcagatgggctgacagtaaagaaacatgtaatggcaggccggtt ccccgcactgagggaaaggcagatccaaatgtggattccctctgtggtagaaagcagtgg aatcatccgaaaagcctgtgccctgagaattctggagctaccactgacgttccttgg >Human tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccaacataacagatgggctggaagtaaggaaacatgtaatgataggcggact cccagcacagaaaaaaaggtagatctgaatgctgatcccctgtgtgagagaaaagaatgg aataagcagaaactgccatgctcagagaatcctagagatactgaagatgttccttgg >Jackrabbit ------------------------------------------------------------ ---------------------aaggctgaattctgtaataagagcaaacagcctggctta gcaagaagccaacagagcagatgggctgaaagtaaggaaacatgtaatgataggcagact cccagcacagagaaaaaggtagttctgaatgttgactgcctgtatgggagaaaacaacag gataagcagaaacctccatgccctgagacctctggagataaccaagatgtttcttgg >FlyingSqu tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccagcagagcagatgggctaaaagtaaggaaacctgtaatgataggcaaatt cccagctcagagaaaaaggtagatttgaatgctgatccccaatatgagaaaaaagaacca agtaagcagaaacatccatgctctgagaattccagagatacccaagatgttccttgg >FreeTaile tgtggcacagatactcatgccagctcattacagcatgagaacagcagtttactactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaagcagcctggctta gcaaagagccagcagagcagatgggctgaaagtaaagaaacatgtaatgataggcagact ctcagcacagagaaaagggtagttctgaatgctgatcctctgaat--------------- aggagaaaagaacctccaggctctaactatcctagagattcccaagatgttccttgg >Mole tgtggcataaatactcatgccagcttattacagcatgaaaacagcagtttattactcact gaaaacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctgactta gcaaagagccagcagaacagatgggctgaaagtaaagaaacatgtaatgataggcagact tccagcccagagaaaagggtagacccgaatgctgatcccatgtatgggagaaaagaactg aataagcagaaacctccatgctctgacagccccagaaattcccaaggtgttgcctgg cogent3-scinexus-b16bc06/tests/data/formattest.fasta.gz000066400000000000000000000013401525761402500232270ustar00rootroot00000000000000‹ùÁUformattest.fastaÍ–onÛ0 Å¿û¹@®Ð�Ý0 @Ñí¯¬ÁÍ,aÐíö#eÇòŸtÉ€aÚÚ’HŠ?=о{zm�§ãê}c¯«ö§°zß^Ž�©© €AL`*哸“�@Y¯×P37È…b�i¬A¬ñ_‹Gsóx7‰!÷c!ÜIÄs/MŒ¨ÆHLg݇;Œ?Ëøsôâ“á9"Z#Ü$MÒg"¸e`À¿Â pÿ¹±Õ†[â†Á„IÓ4¸×G†±�ƒay÷€¬mÿ›Åc`ÃuØdœ±†liü!6›Å†¶À1ÁæéöØè]¬Ã&\GY(} �öØ7zF¦cd:RÚ È¬VšdFd„Ø+m·¶€ dV”F�XPHdš«D©¬12õT‹Ò"ÃNiÁIÆJû´ß¾´»‡ýöнQkŠÖ¬×šÆÙ.h­/ј‹´¡°¯´&e!/#Zƒ*ó¶ÅPkèÀñ|¬hMK‰œ¦¿NZ{‚UZK…�‘)ÏÊÍÏÈ~lœ9gJûd”ȹ<™¿5t·TžBd¨Ê3øjÏŽ·Z8aš,âTa¤Ï³Ó¢•ÜB9ËŒH­ XŽäÞ€èvÝ!chíóé;vÿLgÓ;m©T:›Þi:n˜m˜<[�Ö­u+°¹VðòvÀóóÆšõ_üÌÏ‘Ó%røàj»®‰vW›?h2©®6d¤FçºA‘[*4Éá|µY!w¿ýµÙé×§ÿCrã:ENjpḀ³¸hCµä€ÒFQ$çCMú–î}}Ä{‘Ü´'ÜÚö6ÛöB3]'·‚Ó 8\þþÀ²â¬VœÖŠ+àÊë¸P†="§•‹ËÍÔÛhÏÌ&b³!3Ì‹­›¸Rl˜e†k˜1Q‘3¶®¡ØPWéüÈ@lé4g:füŠË³n~vxô2 cogent3-scinexus-b16bc06/tests/data/long_testseqs.fasta000066400000000000000000000311711525761402500233170ustar00rootroot00000000000000>Human TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT AAAGACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTA GCAAGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAACATGTAATGATAGGCGGACT CCCAGCGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAATGGAAT AAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGATACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGT TCTGATGACTCACATGATGGGGAGTCTGAATCAAATGCCTTGGACGTTCTAAATGAGGTA GATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGTGATCCTCATGAGGCT TTAATATGTGAAAGAGTTCACTCCAAATCAGTAGAGAGTAATATTGAAGACAAAATATTT GGGAAAACCTATCGGAAGAAGGCAAGCCTCCCCAACTTAAGCCATGTAACTGAAATTATA GGAGCATTTGTTACTGAGCCACAGATAATACAAGAGCGTCCCCTCACAAATAAATTAAAG CGTAAAAGGACATCAGGCCTTCATCCTGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT CAAAAGACTCCTGAAATGATAAATCAGGGAACTAACCAAACGGAGCAGAATGGTCAAGTG ATGAATATTACTAATAGTGGTCATGAGAATAAAACAAAAGGTGATTCTATTCAGAATGAG AAAAATCCTAACCCAATAGAATCACTCGAAAAAGAATCTTTCAAAACGAAAGCTGAACCT ATAAGCAGCAGTATAAGCAATATGGAACTCGAATTAAATATCCACAATTCAAAAGCACCT AAAAAGAATCTGAGGAGGAAGTCTACCAGGCATATTCATGCGCTTGAACTAGTCAGTAGA AATCTAAGCCCACCTAATTGTACTGAATTGCAAATTGATAGTTGTTCTAGCAGTGAAGAG ATAAAGAAAAAAAAGTACAACCAAATGCCAGTCAGGCACAGCAGAAACCTACAACTCATG GAAGGTAAAGAACCTGCAACTGGAGCCAAGAAGAACAAGCCAAATGAACAGACAAGTAAA AGACATGACAGCGATACTTTCCCAGAGCTGAAGAATGCACCTGGTTCTTTTACTAAGTGT TCAAATACCAGTGAACTTAAAGAATTTAATCCTAGCCTTCCAAGAGAAGAAAAAGAGAAA CTAGAAACAGTTAAAGTGTCTAATAATGCTGAAGACCCCAAAGATCTCATGTTAAGTGGA GAAAGGGTTTTGCAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGT ACTGATTATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGGAAGGCA AAAACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTA ATTCATGGTTCCAAAGATAATAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACAT GAAGTTAACCACTCAAATCCAGAAGAGGAATGTGCACACTCTGGGTCCTTAAAGAAACAA AGTCCAAAAGTCACTTTTGAATGTGAACAAAAGGAAAATCAAGGAAAGAATGAGTCTAAT AAGCCTGTACAGACAGTTAATATCACTGCAGGCTTTCCTGTGGTTGGTCAGAAAGATAAG CCAGTTGATAATGCCAAATGTAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGA GGCAACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAACCCATATCGT ATACCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAATCTGCTA GAGGAAAACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAACATT CCAAGTACAGTGAGCACAATTAGCCGTAATAACAGAGAAAATGTTTTTAAAGAAGCCAGC TCAAGCAATATTAATGAAGTAGGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGA AACAGAGGGCCAAAATTGAATGCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTAT AAACAAAGTCTTCCTGGAAGTAATAAGCATCCTGAAATAAAAAAGCAAGAAGTTCAGACT GTTAATACAGATTTCTCTCCACTGATTTCAGATAACTTAGAACAGCCTATGAGTAGTCAT GCATCTCAGGTTTGTTCTGAGACACCTGATGACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTTAGCAAAAGCGTC CAGAAAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCATACACATTTGGCTCAGGGT TACCGAAGAGGG >HowlerMon TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTGTTACTCACT AAAGACACACTGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTA GCAAGGAGCCAACATAACAGATGGGCTGAAAGTGAGGAAACATGTAATGATAGGCAGACT CCCAGCGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGCATGGGAGAAAAGAATGGAAT AAGCAGAAACCTCCGTGCTCTGAGAATCCTAGAGATACTGAAGATGTTGCTTGGATAATG CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAACT TCTGATGACTCACATGATGGGGGGTCTGAATCAAATGCCTTGGAAGTTCTAAATGAGGTA GATGGATATTCTAGTTCTTCAGAGAAAATAGACTTACTGGCCAGTGATCCTCATGATCAT TTGATATGTGAAAGAGTTCACTGCAAATCAGTAGAGAGTAGTATTGAAGATAAAATATTT GGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTGAGCCACGTAACTGAAATTATA GGAGCATTTGTTACTGAGCCACAGATAATACAAGAGCATCCTCTCACAAATAAATTAAAG CGTAAAAGGACATCAGGACTTCATCCTGAGGATTTTATCAAGAAAGCAGATTTGGCAGTT CAAAAGACTCCTGAAAAGATAAATCAGGGAACTAACCAAACAGAGCGGAATGATCAAGTG ATGAATATTACTAACAGTGGTCATGAGAATAAAACAAAAGGTGATTCTATTCAGAATGAG AACAATCCTAACCCAGTAGAATCACTGGAAAAAGAATCATTCAAAAGTAAAGCTGAACCT ATAAGCAGTAGTATAAGCAATATGGAATTAGAATTGAATGTCCACAATTCCAAAGCATCT AAAAAGAATCTGAGAAGGAAGTCTACCAGGCATATTCATGAGCTTGAACTAGTCAGTAGA AATCTAAGCCCACCTAATTATACTGAAGTACAAATTGATAGTTGTTCTAGCAGTGAAGAG ATAAAGAAAAAAAATTACAACCAAATGCCAGTCAGGCACAGCAGAAAGCTACAACTCATG GAAGATAAAGAACGTGCAGCTAGAGCCAAAAAGAGCAAGCCAAATGAACAAACAAGTAAA AGACATGCCAGTGATACTTTCCCAGAACTGAGGAACATACCTGGTTCTTTTACTAACTGT TCAAATACTAATGAATTTAAAGAATTTAATCCTAGCCTTCCAAGAGAACAAACAGAGAAA CTAGAAACAGTTAAACTGTCTAATAATGCCAAAGACCCCAAAGATCTCATGTTAAGTGGA GAAAGTGTTTTGCAAATTGAAAGATCTGTAGAGAGTAGCAGTATTTTGTTGATACCTGGT ACTGATTATGGCACTCAGGAAAGTATCTCATTACTGGAAGTTAGCACTCTGGGGAAGGCA AAAACAGAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTA ATTCATGGTTCTAAAGATACTAGAAATGGCACAGAAGGCTTGAAGTATCCATTGGGACCT GAAGTTAACTACTCAAATCCAGAAAAGGAATGTGCATGCTCTAGGTCCTTAAAGAAACAA AGTCCAAAGGTCACTCCTGAATGTGAACAAAAGGAAAATCAAGGAGAGAAAGAGTCTAAT GAGCTTGTAGAGACAGTTAATACCACTGCAGGCTTTCCTATGGTTTGTCAGAAAGATAAG CCAGTTGATTATGCCAGATGTGAAGGAGGCTCTAGGCTTTGTCTATCATCTCAGTTCAGA GGCAACGAAACTGGACTCATTATTCCAAATAAACATGGACTTTTACAGAACCCATATCAT ATGTCACCGCTTATTCCCACCAGGTCATTTGTTAAAACTAAATGTAAGAAAAACCTGCTA GAAGAAAACTCTGAGGAACATTCAATGTCACCTGAAAGAGCAATGGGAAACAAGAACATT CCAAGTACAGTGAGCACAATTAGCCATAATAACAGAGAAAATGCTTTTAAAGAAACCAGC TCAAGCAGTATTTATGAAGTAGGTTCCAGTGATGAAAACATTCAAGCAGAGCTAGGTAGA AACAGAAGGCCAAAATTGAATGCTATGCTTAGATTAGGGCTTCTGCAACCTGAGATTTGT AAGCAAAGTCTTCCTATAAGTGATAAACATCCTGAAATTAAAAAGCAAGAAGTTCAGACT GTTAATACAGACGTCTCTCTACTGATTTCATATAACCTAGAACAGCATATGAGCAGTCAT ACATCTCAGGTTTGTTCTGAGACACCTGACAACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAATATGGCATTAAGGAGACTTCTACTGTTTTTAGCAAAAGTGTC CAGAGAGGAGAGCTCAGCAGGAGCCCTAGCCCTTTCACCCATACACATTTGGCTCAGGTT TACCAAAGAGGG >Mouse TGTGGCACAGATGCTCATGCCAGCTCATTACAGCCTGAGACCAGCAGTTTATTGCTCATT GAAGACAGAATGAATGCAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCATA GCAGTGAGCCAGCAGAGCAGATGGGCTGCAAGTAAAGGAACATGTAACGACAGGCAGGTT CCCAGCGGGGAAAAGGTAGGTCCAAACGCTGACTCCCTTAGTGATAGAGAGAAGTGGACT CACCCGCAAAGTCTGTGCCCTGAGAATTCTGGAGCTACCACCGATGTTCCTTGGATAACA CTAAATAGCAGCGTTCAGAAAGTTAATGAGTGGTTTTCCAGAACTGGTGAAATGTTAACT TCTGACAGCGCATCTGCCAGGAGGCACGAGTCAAATGCTTTGGAAGTTTCAAACGAAGTG GATGGGGGTTTTAGTTCTTCAAGGAAAACAGACTTAGTAACCCCCGACCCCCATCATACT TTAATGTGTGGAAGAGACTTCTCCAAACCAGTAGAGGATAATATCAGTGATAAAATATTT GGGAAATCCTATCAGAGAAAGGGAAGCCGCCCTCACCTGAACCATGTGACTGAAATTATA GGCACATTTATTACAGAACCACAGATAACACAAGAGCAGCCCTTCACAAATAAATTAAAA CGTAAGAGAAGTACATCCCTTCAACCTGAGGACTTCATCAAGAAAGCAGATTCAGCAGGT CAAAGGACTCCTGACAACATAAATCAGGGAACTGACCTAATGGAGCCAAATGAGCAAGCA GTGAGTACTACCAGTAACTGTCAGGAGAACAAAATAGCAGGTAGTAATCTCCAGAAAGAG AAAAGCGCTCATCCAACTGAATCATTGAGAAAGGAACCTTCCACAGCAGGAGCCAAATCT ATAAGCAACAGTGTAAGTGATTTGGAGGTAGAATTAAACGTCCACAGTTCAAAAGCACCT AAGAAAAATCTGAGGAGGAAGTCTATCAGGTGTGCTCTTCCACTTGAACCAATCAGTAGA AATCCAAGCCCACCTACTTGTGCTGAGCTTCAAATCGATAGTTGTGGTAGCAGTGAAGAA ACAAAGAAAAACCATTCCAACCAACAGCCAGCCGGGCACCTTAGAGAGCCTCAACTCATC GAAGACACTGAACCTGCAGCGGATGCCAAGAAGAACGAGCCAAATGAACACATAAGGAAG AGACGTGCCAGCGATGCTTTCCCAGAAGAGAAAAACAAAGCTGGTTTATTAACTAGCTGT TCAAGTCCTAGAAAATCTCAAGGGCCTAATCCCAGCCCTCAGAGAACAGGAACAGAGCAA CTTGAAACACGCCAAATGTCTGACAGTGCCAAAGAACTCGGGGATCGGGTCCTAGGAGGA GAGCCCAGTGGCAAAACTGACCGATCTGAGGAGAGCACCAGCGTATCCTTGGTACCTGAC ACTGACTACGACACTCAGAACAGTGTCTCAGTCCTGGACGCTCACACTGTCAGATATGCA AGAACAGGATCCGCTCAGTGTATGACTCAGTTTGTAGCAAGCGAAAACCCCAAGGAACTC GTCCATGGCTCTAACAATGCTGGGAGTGGCACAGAGGGTCTCAAGCCCCCCTTGAGACAC GCGCTTAACCTCTCAAAACCTCAAAAGGACTGTGCTCACTCTGTGCCCTCAAAGGAACTG AGTCCAAAGGTGACAGCTAAAGGTAAACAAAAAGAACGTCAGGGACAGGAAGAATTTGAA AGTCACGTACAAGCAGTTGCGGCCACAGTGGGCTTACCTGTGCCCTGTCAAGAAGGTAAG CTAGCTGCTGATACAATGTGTGATAGAGGTTGTAGGCTTTGTCCATCATCTCATTACAGA AGCGGGGAGAATGGACTCAGCGCCACAGGTAAATCAGGAATTTCACAAAACTCACATTTT AAACAATCAGTTTCTCCCATCAGGTCATCTATAAAAACTGACAATAGGAAACCTCTGACA GAGGGACGATTTGAGAGACATACATCATCAACTGAGATGGCGGTGGGAAATGAGAACCTT CAGAGTACAGTGCACACAGTTAGCCTGAATAACAGAGGAAATGCTTGTCAAGAAGCCGGC TCGGGCAGTATTCATGAAGTATGTTCCACTGGTGACTCCTTCCCAGGACAACTAGGTAGA AACAGAGGGCCTAAGGTGAACACTGTGCCTCCATTAGATAGTATGCAGCCTGGTGTCTGT CAGCAAAGTGTTCCTGTAAGTGATAAGTATCTTGAAATAAAAAAGCAGGAGGGTGAGGCT GTCTGTGCAGACTTCTCTCCACTATTCTCAGACCATCTTGAGCAATCTATGAGTGGTAAG GTTTTTCAGGTTTGCTCTGAGACACCTGATGACCTGCTGGATGATGTTGAAATACAGGGA CATACTAGCTTTGGTGAAGGTGACATAATGGAGAGATCTGCTGTCTTTAACGGAAGCATC CTGAGAAGGGAGTCCAGTAGGAGCCCTAGTCCTGTAACCCATGCATCGAAGTCTCAGAGT CTCCACAGAGCG >NineBande TGTGGCACAAATACTCATGCCAACTTATTACAGCATGAGAACAGCAGTTTATTACTCACT AAAGACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTA GCAAGGCGCCAACAGAGCAGATGGGCTGAAAGTAAGGAAACATGTAATGATAGGCAGACT CCCAGCGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGTATGGGCGAAAAGAACTGAAT AAGCAGAAACCTCCATGCTCTGAGAGTCATAGAGATACCCAAGATATTCCTTGGATAATG CTGAATAGTAGCATTCAGAAAGTTAACGAGTGGTTTTCCAGAGGTGATGACATATTAACT TCTGATGACTCACACGATAGGGGGTCTGAATTAAATGCATTGAAAGTTTCAAAAGAAGTA GATGAATATTCTAGTTTTTCAGAGAAGATAGACTTAATGGCCATTAATCCTCATGATACT TTACAATTTGAAAGAGTCCAATTGAAACCAGCAGAGAGTAACATCAAAGATAAAATATTT GGGAAAACCTATCATAGGAAGGCAAGCCTCCCTAACTTGAGCCACATAACCCGATTTATA GGAGCTATTGCTGCAGAGCCCAAGATAACACAAGAGCATTCCCTCCAAAATAAAATAAAG CGTAAAAGGGCATCAGGCCTTCGTCCTGAGGATTTATCCAAGAAAGTAGATTTGACAGTT CAAAAAACCCCTGAAAAGATAAATCAGGGAACTGACCAAATGGAGCAGAATGATCCAGTG ATGAATATTGCTAATAGTGGTCATGAGAATGAAACAAAAGGTGATTGTGTTCAGAAAGAG AAAAATGCTAATCCGACAGAATCATTGGGAAAAGAATCTTTCAGAACTAAAGGCGAACCT ATAAGCAGCAGTATAAGCAATATGGAACTAGAATTAAATATTTTAAATTCAAAAGCATCT AAGAAGAATCCGAAGAGGATGTCCACCAGGCATATTCATGCACTTGAACTAGGCAGTAGA AATCCAAGCCCACCTAATCATACTGAACTACAAATTGATAGTTGTTCTAGCATTGAAGAG ATAGAGAAAATAAATTCTAACCAAAAGCCAATCAGACACAACAGAATGCTTCAACTCACG AAAGAAAAAGAAACCACAACTGGAGCCAAAAAGAATAAGCCAAATGAACAAATAAGTGAA AGACATGCCAGTGATGCTTTCCTAGAACTTAAAAATGTAACTGATTTTCTTCCTAAATGT TCAAGTTCTGATAAACTTCAAAAATTTAATTCTAGCCTGCAAGGAGAAGTAGCAGAGAAC CTAGAAACAATTCAAGTGTCTGATAGTACCAGGGACCCTGAAGATCTGGTGGTAAGTGGA GAAAAGTGTTTGCAAACTGAAAGATCTGCAGAGAGTACCGGTATTTCAGTGGTACCTGAT ACTGATTATGGCACTCAAGACAGTATCTCATTACTGGAAGCTGACACCCTGGGGAAGGCA AAAACAGCACTAAATCAACATGTGAGTCAGTATGTAGCAATTAGAAATGCCACTGAACTT TCCCATGGTTCTAAAGACACTAGAAATGACACTGAAGATTTTAAGGATTCATTGAGACAT GAAGTTAACCACTCGAATCCAGAAAATGAATGTGCACACTCCAGGTTCTTAGGGAAACAA AGTCCAAAAGTCACCTTTGAATGTAGACATAAAGAAAATCAGGGGAAGAAAGAGTCTAAA AAACATGTGCAGGTAATTCACACAACTGCAGGCTTTCCTATAGTTTGTCAGAAAGATAAG CCAGGTGATTATGCCAAAGGTCAAGGAGTCTCTAGGCTTTGTCAGTCCTCTCAGGCCAGA GGCAATGAATCTGAACTCATTAATTCAAATGAACATGAAATTTCACAAAACCCAGATCAA ATGCCATCACTTTCTCACATGAAGTCATCTGTTAAAACTAAATGTAAGGAAAACCTGTCA GAGGAAAAGTTTGAGGAACTTACAGTGTCACTTGAAAGAACAATGGTAAATGAGAACATT CAAAGTACAGTAAGCACAATTAGCCACAGTAACAGAGAAAACACTTTTAAAGAAGCCAGC TCAAGCAGTATTAATGAAGTAGGGTCCAGTGATGAGAACATTCAAGCAGAAGTAGGTAGA AACAGAGCACCTAAATTAAATGCTATGCTCAGATTAGGTCTTATGCAACCTGAAGTCTAT AAGCAAAGTCTTCCTATAACCAATAAATATCCTGAAATAAAAAGTCAAGGAATTCGGGCT GTTGATATAGACTTCTCTCCACTAATTTCAGATAACCTACAACTACCTATGAATAGTTGT GCTTCCCAGATTTGTTCTGAGACACCTGATGACTTGTTAGATGATGATGAAATAAAGGAA AATAACTGCTTTGCTGAAAGTGACATTAAGGAAAGATCTGCTATTTTTAGCAAAACTGTC CAGAAAAGAGAGTTCAGAAGGAGCCCTAGCCCTTTAGTCCATACAAGTTTTGCTCAGGGT CACCAAAGAAAG >DogFaced TGTGGCACAAATACTCATGCCAACTCATTACAGCATGAGAACAGCAGTTTATTATACACT AAAGACAGAATGAATGTAGAAAAGACTGACTTCTGTAATAAAAGCAAACAGCCTGGCTTA GCAAGGAGCCAGCAGAACAGATGGGTTGAAACTAAGGAAACATGTAATGATAGGCAGACT TCCAGCGAGAAAAAGGTAGTTCTGAATGCTGATCCCCTGAATGGAAGAATAAAACTGAAT AAGCAGAAACCTCCATGCTCTGACAGTCCTAGAGATTCCAAAGATATTCCTTGGATAACA CGGAATAGTAGCATACAGAAAGTTAATGAGTGGTTTTCCAGACGTGATGAAACATTAACT TCTGATGTCTTACTTGATGAGAGGTCTGAATCAAATGTGGTAGAAGTTCCAAATGAAGTA GATGGATACTCTGGTGCTTCAGAGGAAATAGCCTTAAAGGCCAGTGATCCTCATGGTGCT TTAATATGTGAAAGAGTTCACTCCAAATTGATAGAAAGTAATATTGAAGATAAAATATTT GGGAAAACATATCGGAGGAAAGCAAGCCTCCCTAACTTAAGCCACATAACTGAAATTACA AGAGCATCTGCTACAGAACCTCAGATAACACAAGAGTGCCCCCTCACAAATAAACTAAAA CGTAAAAGAACATCAGGCCTTCATCCTGAGGATTTTATCAAGAAAATAGATTTGACAACT CAAAAAACTTCTGAAAATATAATTGAGGGAACTGACCAAATAGAGCAGAATGGTCATGTG ATGAATAGTTCTAATGATGGTCATGAGAATGAAACAAAAGGTGATTATGTTCAGAAGAAG AAAAATACAAACCCAACAGAATCATTGGAAAAAGAATCTTTCAGAACTAAAGTTGAGTCT GTACCCAACAACATAAGCAATGTGGAACTAGAATTAAATATTCACGGTTCAAAAGCACTC AAGAAGAATCTGAGGAGGAAGTCCACCAGGCATATTCATGCACTTGAACTAGTCAATAGA AATTCAAGCCCACCTAATCATACTGAACTACAAATTGATAGTTGTTCCAGCAGTGAAGAA CTGAAGGAAAAAAATTCTGACCGAATGCCAGACAGACACAGCAAAAAACTTCAGTTCGTA GAAGATAAAGAATCTGCAACTGGAGCCAAGAAGAACATGCCAAATGAGGCAATAAATAAA AGACTTTCCAGTGAAGCTTTTCCCGAATTAAATAACGTACCTGGTTTTTTTACTAATGGT TCAAGTTCTAATAAACGTCAAGAGTTTAATCCTAGCCTTCAAGGAGAAGAAATAGAGAAT CTACGAACAATTCAAGTGTCTAATAGCACCAAAGACCCCAAAATTCTAATCTTTGGTGAA GGAAGAGGTTCACAAACTGATCGATCTACAGAGAGTACCAGTATTTTATTGGGACCTGAA ACGGATTATGGCACTCAAGATAGTATCTCATTACTGGAATCTGACATCCCAGGGAGGGCA AAGACAGCACCAAACCAACATGCAGATCTGTGTGCAGCAATTGAAAACCCCAGAGAACTT ATTCATGATTTTAAAGAAACTAGAAATGACACAGAGAGCTTTAAAGATCCATTGAGACAT GAAGTTAACTCCTCAGACCCAGAAAAGGAATGTGCACACTCCAGGTCCTTGATAAAACAA AGTCCAAAAGTCACTCTTGAATGTGACCGAAAAGGAAATCAGGGAAAGAAAGAGTCTAAC GAGCATGTGCAGGCAGTTTATACAACTATAGGCTTTCCTGGGGTTTCTGAGAAAGACAAA CCAGGAGATTATGCCAGATATAAAGAAGTCTCTAGGCTTTGTCAGTCATTTCAGTCTAGA AGAAATGAAACTGAGCTCACTATTGCAAATAAACTTGGACTTTCACAAAACCCATATCAT ATGCCATCCATTTCTCCCATCAAGTCATCTGTTAAAACTATATGTAAGAAAAATCTGTCA GAGGAAAAGTTTGAAGAACATTCAATATTCCCTGAAAGAGCAATAGGAAATGAGACCATT CAAAGTACAGTGGGCACAATTAGCCAAAATAACAGAGAAAGCACTTTTAAAGAAGGCAGC TCAAGCGGTATTTATGAAGCAGGTTCCAGTGGTGAAAACATTCAAGCAGAACTAAGTAGA AACAGAGGACCAAAATTAAATGCTGTGCTTCAGTTGGGTCTCATGCAGCCTGAAGTCTAT GAGCAAAGCCTTCCTCTAAGTAATAAACATTCTGAAATAAAAAGGCAAGGAGTTCAGGCT GTTAATGCAGATGTCTCTCCACAAATTTCAGATAACTTAGAGCAACCTATGAACAGTAAT ATTTCTCAGGTTTGTTCTGAGACACCGGATGACCTGTTAAATGATGACAAAATAAAGGAC AATATCAGCTTTGATGAAAGTGGCATTCAGGAAAGATCTGCTGTTTTTAGCAAAAATGTC CAGAAAGGAGAATTCAGAAGGAGCCCTAGTCCCTTAGCCCATGCAAGTTTGTCTCAAGGT CGCCCAAGAAGG cogent3-scinexus-b16bc06/tests/data/primate_brca1.fasta000066400000000000000000000471751525761402500231510ustar00rootroot00000000000000>Galago TGTGGCAAAAATACTCATGCCAGCTCATTACAGCATGAGAGCAGTTTATTACTCACTAAA GACAAAATGAATGTAGAAAAGGCTGAATTTTGTAATAAAAGCAAACAGCCTGGCTTAGCA AGGAGCCAACAGAGCAGATCGGCTCAAAGTAAGGAAACATGCAATGATAGGCACACTTGC AGCCCTGAGCAAAAGGTAGATCTGAATACTGCTCCCCCATATGGGAGAAAAGAACAGAAT AAGGAGAAACTTCTATGCTCCAAGAATCCTAGAGATAGCCAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAATGGTTTTCTAGAAGTGATGAAATGTTAACT TCTGATGACTCACATGATGAGGGTTCTGAATCACATGCTGAAGTAGCTGGAGCCTTAGAA GTTCCAAGTGAAGTAGATGGATATTCCAGTTCCTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATTATCCTATAATATGTAAAAGTGAAAGAGTTCACTCCAAACCAATAAAGAGT AAAGTTGAAGATAAAATATTTGGGAAAACTTATCGGAGGAAGGCAAGCCTCCCTAACTTA AGCCATGTAACTGAAAATCTAATTATAAGAGCAGCTGCTACTGAGCCACAGATAACACAA GAGTGTTCCCTCACAAATAAATTAAAACGTAAAAGGAGAACTACATCAGGTCTTTGTCCT GAGGATTTTATCAAGAAGGCAGATTTGGCAGTTCAAAAGACACCTGAAAAGAGAATTCAG GGAACTAACCAAGTGGATCAGAATAGTCACGTGGTAAATATTACTAATAGTGGTTATGAG AATGAAACAAAAGGTGATTATGTTCAGAATGAAAAAAATGCTAACTCAACAGAATCATTG GAAAAAGAATCTCTCGGAACTAAAGCTGAACCTATAAGCAGCAGTATAAGTAATATGAAA TTAGAATTAAATATTCACAATTCAAAAGCAAGTAAAAAGAAAAGGCTGAGGAAGAAGTCT TCTAGCAGGCATATTCGTGCACTTGAACTAGTAGTCAATAAAAATCCAAGCCCTCCTAAT CATACCAACCTACAAATTGACAGTTGTTCTAGCAGTGAAGAAATAAAGGATAAAAGTTCT GACCAAATACCAGTCAGGCATAGCAGAAAGCCTGGACTCATGGAAGATAGAGAACCTGCA ACTGGAGCCAAGAAAAGTAACAAGCCAAATGAGCAAATAAGTAAAAGACATGTCAGTGAT ACTTTCCCAGAAGTGGCATTAACAAATATATCTAGTTTTTTTACTAACTGTTCAGGTTCT AATAGAAAAGAATTTGTCAATCCTAGCCTTCAAAGAAAAAAAACAGAAGAGAACGAAGAA ACAATTCAAGTGTCTAATAGTACCAAAGGTCCGGTGTTAAGTGGAGAAAGGGTTTTGCAA ATTGAAAGTGAAGAAAGATCTATAAAAAGCACCAGTATTTCATTGGTACCTGATACTGAT TATGGTACTCAGGACAGTAACTCGTTACTGAAAGTTAAAGTCTTACGGAAGGTGAAAACA GCACCAAATAAACATGCAAGTCAGGGTACAGCCACTGAAAACCCCAAGGAACTAATCCAT GGTTGCTCTAAAGATACTGGAAATGACACAGAGGGCTATAAGGATCCATTGAGACATGAA ATTAACCACATTCAGAAGATAAGCATGGAAATGGAAGACAGTGAACTTGATACTCAGTAT TTACAGAATACATTCAAGTTTTCAAAGCGTCAGTCGTTTGCTCTGTTTTCAAACCTAGGA AAGGAATGTGCAACAGTCTGTGCCCAGTCTCTCTCTGCGTCCTTAAGAAAAGGTTCAAAA GTCATTCTTGAATGTGAACAAATAGAAAATCCAGGAATGAAAGAGCCTAAAATCAAGCAT ATACAGGGAAATAATATCAATACAGGCTTCTCTGTAGTTTGTCAGAAAGATAAGACAGAT GATTATGCCAAATACATCAAAGAAGCATCTAGGTTTTGTTTGTCAAATCAGTTTCGAGAC AATGAAACTGAATCCATTACTGTAAATAAACTTGGAATTTTACAAAACCTCTATCATATA CCACCACTTTCTCCTATCAGGCTATTTGATAAAACTAAATGTAATACAAACCTGTTAGAG GAAAGGTTTGAAGAACATTCAGTGTTACCTGAAAAAGCAGTAGGAAACGAGAACGTTCCA AGTACAATGAATACAATTAACCAAAATAACAGAGAAAGTGCTTATAAAGAAGCCAGTTCA AGCAGTATCAATGAAGTAAGCTCGAGTACTAATGAAGTGGGCTCCAGTGTTAACGAAGTA GGCCCCAGTAGTGAAAACATTCAAGCAGAACTAGATAAAAACAGAGGACCTAAGTTGAAT GCTGTGCTTAGATTAGGTCTTATGCAACCTGAAGTCTATAAACAAAATCTTCCTATAAGT AATTGTGAACATCCTAAAATAAAAGGGCAAGAAGAAAATGGAGTAGTTCAACCTGTTAAT CCAGATTTTTCTTCATGTCTAATTTCAGATAACCTAGAACAACCTACGAGAAGTAGTCAT GCTTCTCAGCTTTGTTCTGAGACACCTGATGACTTATTAGTTGATGATGAACTAAAGGAA AATACCAGTTTTGCTGAAAATAACATTAAGGAAAGATCTGCTGTTTTTAGCAAAAATGTC ATGAGAAGAGAGATTAGCAGGAGCCCTAGCCCTTTAGCCCATATACATTTGACTCAGGCT CACCAAAGAGAGGTTAGGAAATTAGAGTCCTCAGAAGAGAACATGTCTAGTGAA >HowlerMon TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACTAAA GACACACTGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCA AGGAGCCAACATAACAGATGGGCTGAAAGTGAGGAAACATGTAATGATAGGCAGACTCCC AGCACAGAGAAAAAGGTAGATGTGGATGCTGATCCCCTGCATGGGAGAAAAGAATGGAAT AAGCAGAAACCTCCGTGCTCTGAGAATCCTAGAGATACTGAAGATGTTGCTTGGATAATG CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAACT TCTGATGACTCACATGATGGGGGGTCTGAATCAAATGCCAAAGTAGCTGAAGCATTGGAA GTTCTAAATGAGGTAGATGGATATTCTAGTTCTTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATGATCATTTGATATGTAAAAGTGAAAGAGTTCACTGCAAATCAGTAGAGAGT AGTATTGAAGATAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCTAACTTG AGCCACGTAACTGAAAATCTAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA GAGCATCCTCTCACAAATAAATTAAAGCGTAAAAGGAGAGTTACATCAGGACTTCATCCT GAGGATTTTATCAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAAAGATAAATCAG GGAACTAACCAAACAGAGCGGAATGATCAAGTGATGAATATTACTAACAGTGGTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAACAATCCTAACCCAGTAGAATCACTG GAAAAAGAATCATTCAAAAGTAAAGCTGAACCTATAAGCAGTAGTATAAGCAATATGGAA TTAGAATTGAATGTCCACAATTCCAAAGCATCTAAAAAGAATAGGCTGAGAAGGAAGTCT TCTACCAGGCATATTCATGAGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAT TATACTGAAGTACAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAATTAC AACCAAATGCCAGTCAGGCACAGCAGAAAGCTACAACTCATGGAAGATAAAGAACGTGCA GCTAGAGCCAAAAAGAGTAGCAAGCCAAATGAACAAACAAGTAAAAGACATGCCAGTGAT ACTTTCCCAGAACTGAGGTTAACAAACATACCTGGTTCTTTTACTAACTGTTCAAATACT AATGAAAAAGAATTTGTCAATCCTAGCCTTCCAAGAGAACAAACAGAAGAGAAACTAGAA ACAGTTAAACTGTCTAATAATGCCAAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGT GTTTTGCAAATTGAAAGATCTGTAGAGAGTAGCAGTATTTTGTTGATACCTGGTACTGAT TATGGCACTCAGGAAAGTATCTCATTACTGGAAGTTAGCACTCTGGGGAAGGCAAAAACA GAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCAT GGTTGTTCTAAAGATACTAGAAATGGCACAGAAGGCTTGAAGTATCCATTGGGACCTGAA GTTAACTACAGTCAGGAAACAAGCATAGATATGAGAGAAAGTGAACTTGATACTCAATAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTGTTTTCAAATCCAGGA AAGGAATGTGCAACATTCTCTGCCTGCTCTAGGTCCTTAAAGAAACAAAGTCCAAAGGTC ACTCCTGAATGTGAACAAAAGGAAGAAAATCAAGGAGAGAAAGAGTCTAATATCGAGCTT GTAGAGACAGTTAATACCACTGCAGGCTTTCCTATGGTTTGTCAGAAAGATAAGCCAGTT GATTATGCCAGATGTATCGAAGGAGGCTCTAGGCTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTATTCCAAATAAACATGGACTTTTACAGAACCCATATCATATG TCACCGCTTATTCCCACCAGGTCATTTGTTAAAACTAAATGTAAGAAAAACCTGCTAGAA GAAAACTCTGAGGAACATTCAATGTCACCTGAAAGAGCAATGGGAAACAAGAACATTCCA AGTACAGTGAGCACAATTAGCCATAATAACAGAGAAAATGCTTTTAAAGAAACCAGCTCA AGCAGTATTTATGAAGTAGGTTCCAGTACTAATGAAGCAGGTTCTAGTACTAATGAAGTA GGCTCCAGTGATGAAAACATTCAAGCAGAGCTAGGTAGAAACAGAAGGCCAAAATTGAAT GCTATGCTTAGATTAGGGCTTCTGCAACCTGAGATTTGTAAGCAAAGTCTTCCTATAAGT GATTGTAAACATCCTGAAATTAAAAAGCAAGAACATGAAGAAGTAGTTCAGACTGTTAAT ACAGACGTCTCTCTATGTCTGATTTCATATAACCTAGAACAGCATATGGGAAGCAGTCAT ACATCTCAGGTTTGTTCTGAGACACCTGACAACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAATATGGCATTAAGGAGACTTCTACTGTTTTTAGCAAAAGTGTC CAGAGAGGAGAGCTCAGCAGGAGCCCTAGCCCTTTCACCCATACACATTTGGCTCAGGTT TACCAAAGAGGGGCCAAGAAATTAGAGTCCTCGGAAGAGAATTTATCTAGTGAG >Rhesus TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTGTTACTCACTAAA GACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTGGCA AGGAGCCAACATAACAGATGGACTGGAAGTAAGGAAACATGTAATGATAGGCAGACTCCC AGCACAGAGAAAAAGGTAGATCTGAATGCTAATGCCCTGTATGAGAGAAAAGAATGGAAT AAGCAAAAACTGCCATGCTCTGAGAATCCTAGAGACACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAAGT TCTGATGACTCACATGATGGGGGGTCTGAATCAAATGCCAAAGTAGCTGATGTATTGGAC GTTCTAAATGAGGTAGATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATGAGCCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAGTTCAGTAGAGAGT AATATTAAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAACCTTCCCAATTTA AGCCATGTAACTGAAAATCTAATTATAGGAGCACTTGTTACTGAGTCACAGATAATGCAA GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAACTACATCAGGTCTTCATCCT GAGGATTTTATAAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAATAATAAATCAG GGAACTAACCAAATGGAGCAGAATGGTCAAGTGATGAATATTACTAATAGTGCTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAAAAATCCTAACCCAATAGAATCACTG GAAGAAGAATCTTTCAAAACTAAAGCTGAACCTATAAGCAGCAGTATAAACAATATGGAA CTAGAATTAAATATCCACAATTCAAAAGCACCTAAAAAAAATAGGCTGAGGAGGAAGTCT TCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAC TGTACTGAACTACAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAATTAC AACCAAATGCCAGTCAGGCACAGCAGAAACCTACAACTCATGGAAGATAAAGAATCTGCA ACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGACATGCCAGTGAT ACTTTCCCAGAACTGAAGTTAACAAAGGTACCTGGTTCTTTTACTAACTGTTCAAATACT AGTGAAAAAGAATTTGTCAATCCTAGCCTTTCAAGAGAAGAAAAAGAAGAGAAACTAGAA ACAGTTAAAGTGTCTAATAATGCCAAAGACCCCAAAGATCTCATCTTAAGTGGAGAAAGG GTTTTACAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGTACCGAT TATGGCACTCAGGAAAGTATCTCATTACTGGAAGTTAGCACTCTAGGGAAGGCAAAAACA GAACGAAATAAATGTATGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCAT GGTTGTTCTGAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGAAGTGAA GTTAACCACAGTCAGGAAACAAGCATAGAAATAGAAGAAAGTGAACTTGATACTCAGTAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCCTTTGCTCTGTTTTCAAATCCAGGA GAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAAAGTT ACTTCTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAAACAGTCTAATATCAAGCCT GTACAGACAGTTAATATCACTGCAGGCTTTTCTGTGGTTTGTCAGAAAGATAAGCCAGTT GATAATGCCAAATGTATCAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTACTCCAAATAAACATGGACTGTTACAAAACCCATACCATATA CCACCACTTTTTCCTGTCAAGTCATTTGTTAAAACTAAATGTAACAAAAACCTGCTAGAG GAAAACTCTGAGGAACATTCAGTGTCACCTGAAAGAGCAGTGGGAAACAAGAACATTCCA AGTACAGTGAGCACAATTAGCCATAATAACAGAGAAAATGCTTTTAAAGAAGCCAGCTCG AGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGTGGGCTCCAGTATTAATGAAGTA GGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGAAACAGAGGGCCAAAATTGAAT GCTGTGCTTAGATTAGGGCTTTTGCAACCTGAGGTCTGTAAACAAAGTCTTCCTATAAGT AATTGTAAGCATCCTGAAATAAAAAAGCAAGAACATGAAGAATTAGTTCAGACTGTTAAT ACAGACTTCTCTCCATGTCTGATTTCAGATAACCTAGAACAGCCTATGGGAAGTAGTCAT GCGTCTGAGGTTTGTTCTGAGACTCCTGATGATCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAAAATGACATTAAGGAGAGTTCTGCTGTTTTTAGCAAAAGCATC CAGAGAGGAGAGCTCAGCAGGAGCCCTAGCCCTTTCACCCATACACATTTAGCTCAGGGT TACCGAAAAGAGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Orangutan TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTATTACTCACTAAA GACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCA AGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAACATGTAATGATAGGCAGACTCCC AGCACAGAAAAAAAGGTAGACCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAATGGAAT AAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGATACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGACGAACTGTTAGGT TCTGATGACTCACATGATGGGAGGTCTGAATCAAATGCCAAAGTAGCGGATGTATTGGAC GTTCTAAATGAGGTAGATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATGAGGCTTTAATTTGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGT AATATTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAACTTA AGCCATGTAACTGAAAATCTAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCCT GAGGATTTTATCAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAATGATAAATCAG GGAACTAACCAAATGGAGCAGAATGGTCAAGTGATGAATATTACTAATAGTGGTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAAAAATCCTAACCCAATAGAATCACTC GAAAAAGAATCTTTCAAAACAAAAGCTGAACCTATAAGCAGCAGTATAAGCAATATGGAA CTCGAATTAAATATCCATAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGAAGTCT TCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAT TGTACTGAATTGCAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAAATAC AACCAAATGCCAGTCAGGCACAGCAGAAACCTACAACTCATGGAAGATAAAGAACCTGCA ACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGACATGACAGCGAT ACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACC AGTGAGAAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAACTAGGA ACAGTTAAAGTGTCTAATAATGCCAAAGACCCCAAAGATCTCATGTTAAGTGGAGGAAGG GTTTTGCAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGTACTGAT TATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGGAAGGCAAAAACA GAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGAACTAATTCAT GGTTGTTTCAAAGATACTAGAAATGACACAGAAGGGTTTAAGTATCCATTGGGACATGAA GTTAACCACAGTCAGGAAACAAGCATAGAAATGGAAGAAAGTGAACTTGATACTCAGTAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTGTTTTCAAATCCAGGA GAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAAAGTC ACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCT GTACAGACAGCTAATATCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAGCCAGTT GATTATGCCAAATGTATCAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTCACAAAACCCATATCATATA CCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAACCTGCTAGAG GAAAACTCTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAACGAGAACATTCCA AGTACAGTGAGCATAATTAGCCGTAATAACAGAGAAAATGTTTTTAAAGAAGCCAGCTCA AGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGTGGGCTCCAGTATTAATGAAGTA GGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGAAGCAGAGGGCCAAAATTGAAT GCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTTTTCCTGGAAGT AATGGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAAGTACTTCAGACTGTTAAT ACAGACTTCTCTCCATGTCTGATTTCAGATAACCTAGAACAGCCTATGAGAAGTAGTCAT GCATCTCAGGTTTGTTCTGAGACACCTAATGACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTTAGCAAAAGCGTC CAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCATACACATTTGGCTCAGGGT TACCGAAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Gorilla TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTATTACTCACTAAA GACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAACAAACAGCCTGGCTTAGCA AGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAACATGTAATGATAGGCGGACTCCC AGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAACGAATGGAAT AAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGATACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGT TCTGATGACTCACATGATGGGGGGTCTGAATCAAATGCCAAAGTAGCTGATGTATTGGAC GTTCTAAATGAGGTAGATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGT AATATTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAGCTTA AGCCATGTAACTGAAAATCTAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCCT GAGGATTTTATCAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAATGATAAATCAG GGAACTAACCAAATGGAGCAGAATGGTCAAGTGATGAATATTACTAATAGTGGTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAAAAATCCTAACCCAATAGAATCACTA GAAAAAGAATCTTTCAAAACGAAAGCTGAACCTATAAGCAGCAGTATAAGCAATATGGAA CTCGAATTAAATATCCACAATTCAAAAGCGCCTAAAAAGAATAGGCTGAGGAGGAAGTCT TCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAT TGTACTGAATTGCAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAAGTAC AACCAAATGCCAGTCAGGCACAGCAGAAACCTACAGCTCATGGAAGATAAAGAACCTGCA ACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGACATGACAGCGAT ACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACC AGTGAAAAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAACTAGAA ACAGTTAAAGTGTCTAATAATGCCGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG GTTTTGCAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGTACTGAT TATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGGAAGGCAAAAACA GAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCAT GGTTGTTCCAAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAA GTTAACCACAGTCGGGAAACAAGCATAGAAATGGAAGAAAGTGAACTTGATGCTCAGTAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTGTTTTCAAATCCAGGA GAGGAATGTGCAACATTCTCTGCCCACTCTAGGTCCTTAAAGAAACAAAGTCCAAAAGTC ACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCT GTACAGACAGTTAATATCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAGCCAGTT GATTATGCCAAATGTATCAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAACCCATATCATATA CCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAACCTGCTAGAG GAAAACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAACATTCCA AGTACAGTGAGCACAATTAGCCGTAATAACAGAGAAAATGTTTTTAAAGAAGCCAGCTCA AGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGTGGGCTCCAGTATTAATGAAGTA GGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGAAACAGAGGGCCAAAATTGAAT GCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTTCCTGGAAGT AATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAAGTAGTTCAGACTGTTAAT ACAGATTTCTCTCCATGTCTGATTTCAGATAACTTAGAACAGCCTATGGGAAGTAGTCAT GCATCTCAGGTTTGTTCTGAGACACCTGATGACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTAAAAATGACATTAAGGAAAGTTCTGCTGTTTTTAGCAAAAGCGTC CAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCATACACATTTGGCTCAGGGT TACCGAAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Human TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTATTACTCACTAAA GACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCA AGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAACATGTAATGATAGGCGGACTCCC AGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAATGGAAT AAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGATACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGT TCTGATGACTCACATGATGGGGAGTCTGAATCAAATGCCAAAGTAGCTGATGTATTGGAC GTTCTAAATGAGGTAGATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGT GATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGT AATATTGAAGACAAAATATTTGGGAAAACCTATCGGAAGAAGGCAAGCCTCCCCAACTTA AGCCATGTAACTGAAAATCTAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGACCTACATCAGGCCTTCATCCT GAGGATTTTATCAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAATGATAAATCAG GGAACTAACCAAACGGAGCAGAATGGTCAAGTGATGAATATTACTAATAGTGGTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAAAAATCCTAACCCAATAGAATCACTC GAAAAAGAATCTTTCAAAACGAAAGCTGAACCTATAAGCAGCAGTATAAGCAATATGGAA CTCGAATTAAATATCCACAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGAAGTCT TCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAT TGTACTGAATTGCAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAAGTAC AACCAAATGCCAGTCAGGCACAGCAGAAACCTACAACTCATGGAAGGTAAAGAACCTGCA ACTGGAGCCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGACATGACAGCGAT ACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAAGTGTTCAAATACC AGTGAAAAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAAAAGAAGAGAAACTAGAA ACAGTTAAAGTGTCTAATAATGCTGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG GTTTTGCAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGTACTGAT TATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGGAAGGCAAAAACA GAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCAT GGTTGTTCCAAAGATAATAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAA GTTAACCACAGTCGGGAAACAAGCATAGAAATGGAAGAAAGTGAACTTGATGCTCAGTAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCCGTTTTCAAATCCAGGA GAGGAATGTGCAACATTCTCTGCCCACTCTGGGTCCTTAAAGAAACAAAGTCCAAAAGTC ACTTTTGAATGTGAACAAAAGGAAGAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCT GTACAGACAGTTAATATCACTGCAGGCTTTCCTGTGGTTGGTCAGAAAGATAAGCCAGTT GATAATGCCAAATGTATCAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAACCCATATCGTATA CCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAATCTGCTAGAG GAAAACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAACATTCCA AGTACAGTGAGCACAATTAGCCGTAATAACAGAGAAAATGTTTTTAAAGAAGCCAGCTCA AGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGTGGGCTCCAGTATTAATGAAATA GGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGAAACAGAGGGCCAAAATTGAAT GCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTTCCTGGAAGT AATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAAGTAGTTCAGACTGTTAAT ACAGATTTCTCTCCATATCTGATTTCAGATAACTTAGAACAGCCTATGGGAAGTAGTCAT GCATCTCAGGTTTGTTCTGAGACACCTGATGACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTTAGCAAAAGCGTC CAGAAAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCATACACATTTGGCTCAGGGT TACCGAAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG >Chimpanzee TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGTTTATTACTCACTAAA GACAGAATGAATGTAGAAAAGGCTGAATTCTGTAATAAAAGCAAACAGCCTGGCTTAGCA AGGAGCCAACATAACAGATGGGCTGGAAGTAAGGAAACATGTAATGATAGGCGGACTCCC AGCACAGAAAAAAAGGTAGATCTGAATGCTGATCCCCTGTGTGAGAGAAAAGAATGGAAT AAGCAGAAACTGCCATGCTCAGAGAATCCTAGAGATACTGAAGATGTTCCTTGGATAACA CTAAATAGCAGCATTCAGAAAGTTAATGAGTGGTTTTCCAGAAGTGATGAACTGTTAGGT TCTGATGACTCACATGATGGGGGGTCTGAATCAAATGCCAAAGTAGCTGATGTATTGGAC GTTCTAAATGAGGTAGATGAATATTCTGGTTCTTCAGAGAAAATAGACTTACTGGCCAGC GATCCTCATGAGGCTTTAATATGTAAAAGTGAAAGAGTTCACTCCAAATCAGTAGAGAGT AATACTGAAGACAAAATATTTGGGAAAACCTATCGGAGGAAGGCAAGCCTCCCCAACTTA AGCCATGTAACTGAAAATCTAATTATAGGAGCATTTGTTACTGAGCCACAGATAATACAA GAGCGTCCCCTCACAAATAAATTAAAGCGTAAAAGGAGAGCTACATCAGGCCTTCATCCT GAGGATTTTATCAAGAAAGCAGATTTGGCAGTTCAAAAGACTCCTGAAATGATAAATCAG GGAACTAACCAAATGGAGCAGAATGGTCAAGTGATGAATATTACTAATAGTGGTCATGAG AATAAAACAAAAGGTGATTCTATTCAGAATGAGAAAAATCCTAACCCAATAGAATCACTC GAAAAAGAATCTTTCAAAACGAAAGCTGAACCTATAAGCAGCAGTATAAGCAATATGGAA CTCGAATTAAATATCCACAATTCAAAAGCACCTAAAAAGAATAGGCTGAGGAGGAAGTCT TCTACCAGGCATATTCATGCGCTTGAACTAGTAGTCAGTAGAAATCTAAGCCCACCTAAT TGTACTGAATTGCAAATTGATAGTTGTTCTAGCAGTGAAGAGATAAAGAAAAAAAAGTAC AACCAAATGCCAGTCAGGCACAGCAGAAACCTACAACTCATGGAAGATAAAGAACCTGCA ACTGGAGTCAAGAAGAGTAACAAGCCAAATGAACAGACAAGTAAAAGACATGACAGCGAT ACTTTCCCAGAGCTGAAGTTAACAAATGCACCTGGTTCTTTTACTAACTGTTCAAATACC AGTGAAAAAGAATTTGTCAATCCTAGCCTTCCAAGAGAAGAAGAAGAAGAGAAACTAGAA ACAGTTAAAGTGTCTAATAATGCCGAAGACCCCAAAGATCTCATGTTAAGTGGAGAAAGG GTTTTGCAAACTGAAAGATCTGTAGAGAGTAGCAGTATTTCATTGGTACCTGGTACTGAT TATGGCACTCAGGAAAGTATCTCGTTACTGGAAGTTAGCACTCTAGGGAAGGCAAAAACA GAACCAAATAAATGTGTGAGTCAGTGTGCAGCATTTGAAAACCCCAAGGGACTAATTCAT GGTTGTTCCAAAGATACTAGAAATGACACAGAAGGCTTTAAGTATCCATTGGGACATGAA GTTAACCACAGTCGGGAAACAAGCATAGAAATGGAAGAAAGTGAACTTGATGCTCAGTAT TTGCAGAATACATTCAAGGTTTCAAAGCGCCAGTCATTTGCTCTGTTTTCAAATCCAGGA GAGGAATGTGCAACATTCTCTGCCCACTGTAGGTCCTTAAAGAAACAAAGTCCAAAAGTC ACTTTTGAACGTGAACAAAAGGAACAAAATCAAGGAAAGAATGAGTCTAATATCAAGCCT GTACAGACAGTTAATATCACTGCAGGCTTTCCTGTGGTTTGTCAGAAAGATAAGCCAGTT GATTATGCCAAATGTATCAAAGGAGGCTCTAGGTTTTGTCTATCATCTCAGTTCAGAGGC AACGAAACTGGACTCATTACTCCAAATAAACATGGACTTTTACAAAACCCATATCATATA CCACCACTTTTTCCCATCAAGTCATTTGTTAAAACTAAATGTAAGAAAAACCTGCTAGAG GAAAACTTTGAGGAACATTCAATGTCACCTGAAAGAGAAATGGGAAATGAGAACATTCCA AGTACAGTGAGCACAATTAGCCGTAATAACAGAGAAAATGTTTTTAAAGAAGCCAGCTCA AGCAATATTAATGAAGTAGGTTCCAGTACTAATGAAGTGGGCTCCAGTATTAATGAAGTA GGTTCCAGTGATGAAAACATTCAAGCAGAACTAGGTAGAAACAGAGGGCCAAAATTGAAT GCTATGCTTAGATTAGGGGTTTTGCAACCTGAGGTCTATAAACAAAGTCTTCCTGAAAGT AATTGTAAGCATCCTGAAATAAAAAAGCAAGAATATGAAGAAGTAGTTCAGACTGTTAAT ACAGATTTCTCTCCATGTCTGATTTCAGATAACTTAGAACAGCCTATGGGAAGTAGTCAT GCATCTCAGGTTTGTTCTGAGACACCTGATGACCTGTTAGATGATGGTGAAATAAAGGAA GATACTAGTTTTGCTGAAAATGACATTAAGGAAAGTTCTGCTGTTTTTAGCAAAAGCGTC CAGAGAGGAGAGCTTAGCAGGAGTCCTAGCCCTTTCACCCATACACATTTGGCTCAGGGT TACCGAAGAGGGGCCAAGAAATTAGAGTCCTCAGAAGAGAACTTATCTAGTGAG cogent3-scinexus-b16bc06/tests/data/primates_brca1.fasta000066400000000000000000000713471525761402500233320ustar00rootroot00000000000000>TreeShrew tgtggcataaatacttatgccagctcattacagcatgagaacagcagtttattactcact aaggacagaatgaatgtagaaaaggctgaattgtgtaataaaagccaacaacctgactta gcaaggagccagcagagcagatggactgaaaataaggaaacatgtaatgataggcagatt cccagcacagaaaaaaaggtagatctaaatgctgatcccctgtgtgggaagaaaaaacaa gctaagcagaaacygctatgttctaacagtcctagagatnnngaccaagattctccttgg ataactctaaatagtagcattcagaaagttaatgaatggttttccagaagtgatgaaatg ttaacttctaacgactcacatgatggtgagtctgaannnnnnnnnnnnatagctggtgca ttygaagytccaaataaagtagatgaatattctggttcttcagaaaaaatagacttaatg gccaacaatcttcatgatgctttaataagtaaaagtgaaggaatctactccaaaccagta gagggtaatattgaagataaaatatttgggaaaacctatcggaggaaagcaagtcttcct aacttgagccgtgtaactgatgatctaattagaggggcatttgttacagagcctgagata actcgagagcgtcccttcacaaataaattaaagcggaaaaggagaactatatcaggcctt catcctgaagattttatcaagaaaacagatttggcagttgttcaaaagactcctgaaaag ataaatcagagaactgaccaaatagagcataatggtcaggtgatgagtattgctaatagt ggtcatgagaatgaaacaaaaggtgattatatttcgaaagagaaaaatgctaacccaatg gaatcattagaaaaagaatctgctctcagaactaaagctgagcccataagcagcagtgta agcaatatggaactagaaataaataaccacagttcagaagcacctaagaagaataggctg aggagaaagttttctgctaggcatattcgcacacttgaactagtagacaataaaagtcca agcccacctaatcgtactgaactacaaattgacagttattctagcggtgaagagagaaag aaaaagnnnggtgagcaaatgccagttggacacagcagaaagtttcaacttgaggaagag aaagaacctacaactggagccaagaaaaataaccagccaaatacagaaataagtgaaaga catgccagtggtgttatcccagatctgaagttaacaaacatacctggttttttcacaaac tcttcgagttctaataaacttccagaatttgtccatcgtagccttcaaagagaannnaaa gaagagaacnnncgagaaacaattcaaatatccagtagtaccaaannnnnnnnngatctg gtattaaggggagaannnaggggtttgcaagatgtaaggtctgcagagagtaccagtatt tctttggtacctgatactgatgataacacccaggatagcatctcattactagatgctaac cccctagctaggaaggcaaaaacagcaccaaatcaatgtgtaaatcagagtgcaacaact gaaaaccccaaggaacttatacacagttgttctaaaactactaggaatnnnnnngaaggc ttcaaggatccattgaaaagtgaagttaatcatattcaggagatgagtgtagaaatggag gagagtgaacttgatactcagtatttacagaatacattcaggagttcaaagcgtcagtca tttgctctgtcttcaaatccaggaaatccagaaaaggaacatgtctgtgttnnnnnnnnn nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn aaagaaagtctgaaagagtctaacatccaacatatacaggcagttagtaccatggttnnn nnnnnnnnnnnntttcagaaagataagnnnctaggtgattttgctacatctggcattaaa gaagtccctagactttgtccatcatctcagttcagaggcaatgaaactgatctcattact gcaaataaacctgaagtttcacaaaacccgtatcatatgccattactttatcctgtcaag tcacctattataactaaaagtaagaaaagcctgtcagaggaagggtttgaggaacaggca atgtcacttgaaagagcaatggaaaatgagaacatcattcaaagtacagtgagcacaatt agccaagataacattagagaaggtgcttttaaagaagccagctcaagcagtattaatgaa ataggtcctagtactaatgaaggaagctctagtattaatgaggtaggttccagtnnnnnn nnnnnnnnnnnnnnnggtgaaaacattcaagcagaactaggtaaaaagagaggatccaaa ttaaatgctgtgcttagattaggtcttatgcaacccgaagtctataagcaaagtcttcct ttaagtaatcataatgatcctgaaatgaaaagacaagaaaaaaatgaaggaggagttcag gctattaaannngatttacctccatgtctaatttcagataatcaagagcatnnnatggga agtagccatgcttctcagatttgttctgagacacctgatgatctgttagatgatgatgaa ggaaaagaaaatnnnagctttgctgaggttgatgttaaggaaagatctgctgtttttggc aaaactgtccagagaagagagttaagaaggagctctagccctttaactcgtgcatgtttg actgagggtcagcaaacaggagcccagaaattagattcatcagaagagaacctatctagt gag >Orangutan tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccaacataacagatgggctggaagtaaggaaacatgtaatgataggcagact cccagcacagaaaaaaaggtagacctgaatgctgatcccctgtgtgagagaaaagaatgg aataagcagaaactgccatgctcagagaatcctagagatnnnactgaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgagtggttttccagaagtgacgaactg ttaggttctgatgactcacatgatgggaggtctgaatcaaatgccaaagtagcggatgta ttggacgttctaaatgaggtagatgaatattctggttcttcagagaaaatagacttactg gccagtgatcctcatgaggctttaatttgtaaaagtgaaagagttcactccaaatcagta gagagtaatattgaagacaaaatatttgggaaaacctatcggaggaaggcaagcctcccc aacttaagccatgtaactgaaaatctaattataggagcatttgttactgagccacagata atacaagagcgtcccctcacaaataaattaaagcgtaaaaggagagctacatcaggcctt catcctgaggattttatcaagaaagcagatttggcagttnnncaaaagactcctgaaatg ataaatcagggaactaaccaaatggagcagaatggtcaagtgatgaatattactaatagt ggtcatgagaataaaacaaaaggtgattctattcagaatgagaaaaatcctaacccaata gaatcactcgaaaaagaatctgctttcaaaacaaaagctgaacctataagcagcagtata agcaatatggaactcgaattaaatatccataattcaaaagcacctaaaaagaataggctg aggaggaagtcttctaccaggcatattcatgcgcttgaactagtagtcagtagaaatcta agcccacctaattgtactgaattgcaaattgatagttgttctagcagtgaagagataaag aaaaaaaaatacaaccaaatgccagtcaggcacagcagaaacctacaactcatggaagat aaagaacctgcaactggagccaagaagagtaacaagccaaatgaacagacaagtaaaaga catgacagcgatactttcccagagctgaagttaacaaatgcacctggttcttttactaac tgttcaaataccagtgagcttaaagaatttgtcaatcctagccttccaagagaagaaaaa gaagagaaannnctaggaacagttaaagtgtctaataatgccaaagaccccaaagatctc atgttaagtggaggannnagggttttgcaaactgaaagatctgtagagagtagcagtatt tcattggtacctggtactgattatggcactcaggaaagtatctcgttactggaagttagc actctagggnnnaaggcaaaaacagaaccaaataaatgtgtgagtcagtgtgcagcattt gaaaaccccaaggaactaattcatggttgtttcaaagatactagaaatgacacagaaggg tttaagtatccattgggacatgaagttaaccacagtcaggaaacaagcatagaaatggaa gaaagtgaacttgatactcagtatttgcagaatacattcaaggtttcaaagcgccagtca tttgctctgttttcaaatccaggaaatccagaagaggaatgtgcaacattctctgcccac tctaggtccttaaagaaacaaagtccaaaagtcacttttgaatgtgaacaaaaggaagaa aatcaaggaaagaatgagtctaatatcaagcctgtacagacagctaatatcactgcaggc tttcctgtggtttgtcagaaagataagnnnccagttgattatgccaaatgtagtatcaaa ggaggctctaggttttgtctatcatctcagttcagaggcaacgaaactggactcattact ccaaataaacatggactttcacaaaacccatatcatataccaccactttttcccatcaag tcatttgttaaaactaaatgtaagaaaaacctgctagaggaaaactctgaggaacattca atgtcacctgaaagagaaatgggaaacgagaacnnnattccaagtacagtgagcataatt agccgtaataacattagagaaaatgtttttaaagaagccagctcaagcaatattaatgaa gtaggttccagtactaatgaagtgggctccagtattaatgaagtaggttccagtnnnnnn nnnnnnnnnnnnnnngatgaaaacattcaagcagaactaggtagaagcagagggccaaaa ttgaatgctatgcttagattaggggttttgcaacctgaggtctataaacaaagttttcct ggaagtaatggtaagcatcctgaaataaaaaagcaagaatatgaagaannngtacttcag actgttaatacagacttctctccatgtctgatttcagataacctagaacagcctatgaga agtagtcatgcatctcaggtttgttctgagacacctaatgacctgttagatgatggtgaa ataaaggaagatactagttttgctgaaaatgacattaaggaaagttctgctgtttttagc aaaagcgtccagagaggagagcttagcaggagtcctagccctttcacccatacacatttg gctcagggttaccgaagaggggccaagaaattagagtcctcagaagagaacttatctagt gag >Rhesus tgtggcacaaatactcatgccagctcattacagcatgagaacnnnagtttgttactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggcttg gcaaggagccaacataacagatggactggaagtaaggaaacatgtaatgataggcagact cccagcacagagaaaaaggtagatctgaatgctaatgccctgtatgagagaaaagaatgg aataagcaaaaactgccatgctctgagaatcctagagacnnnactgaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgagtggttttccagaagtgatgaactg ttaagttctgatgactcacatgatggggggtctgaatcaaatgccaaagtagctgatgta ttggacgttctaaatgaggtagatgaatattctggttcttcagagaaaatagacttactg gccagtgatcctcatgagcctttaatatgtaaaagtgaaagagttcactccagttcagta gagagtaatattaaagacaaaatatttgggaaaacctatcggaggaaggcaaaccttccc aatttaagccatgtaactgaaaatctaattataggagcacttgttactgagtcacagata atgcaagagcgtcccctcacaaataaattaaagcgtaaaaggagaactacatcaggtctt catcctgaggattttataaagaaagcagatttggcagttnnncaaaagactcctgaaata ataaatcagggaactaaccaaatggagcagaatggtcaagtgatgaatattactaatagt gctcatgagaataaaacaaaaggtgattctattcagaatgagaaaaatcctaacccaata gaatcactggaagaagaatctgctttcaaaactaaagctgaacctataagcagcagtata aacaatatggaactagaattaaatatccacaattcaaaagcacctaaaaaaaataggctg aggaggaagtcttctaccaggcatattcatgcgcttgaactagtagtcagtagaaatcta agcccacctaactgtactgaactacaaattgatagttgttctagcagtgaagagataaag aaaaaaaattacaaccaaatgccagtcaggcacagcagaaacctacaactcatggaagat aaagaatctgcaactggagccaagaagagtaacaagccaaatgaacagacaagtaaaaga catgccagtgatactttcccagaactgaagttaacaaaggtacctggttcttttactaac tgttcannnaatactagtgaaaaagaatttgtcaatcctagcctttcaagagaagaaaaa gaagagaaannnctagaaacagttaaagtgtctaataatgccaaagaccccaaagatctc atcttaagtggagaannnagggttttacaaactgaaagatctgtagagagtagcagtatt tcattggtacctggtaccgattatggcactcaggaaagtatctcattactggaagttagc actctagggnnnaaggcaaaaacagaacgaaataaatgtatgagtcagtgtgcagcattt gaaaaccccaaggaactaattcatggttgttctgaagatactagaaatgacacagaaggc tttaagtatccattgggaagtgaagttaaccacagtcaggaaacaagcatagaaatagaa gaaagtgaacttgatactcagtatttgcagaatacattcaaggtttcaaagcgccagtcc tttgctctgttttcaaatccaggaaatccagaagaggaatgtgcaacattctctgcccac tctaggtccttaaagaaacaaagtccaaaagttacttctgaatgtgaacaaaaggaagaa aatcaaggaaagaaacagtctaatatcaagcctgtacagacagttaatatcactgcaggc ttttctgtggtttgtcagaaagataagnnnccagttgataatgccaaatgtagtatcaaa ggaggctctaggttttgtctatcatctcagttcagaggcaacgaaactggactcattact ccaaataaacatggactgttacaaaacccataccatataccaccactttttcctgtcaag tcatttgttaaaactaaatgtaacaaaaacctgctagaggaaaactctgaggaacattca gtgtcacctgaaagagcagtgggaaacaagaacatcattccaagtacagtgagcacaatt agccataataacattagagaaaatgcttttaaagaagccagctcgagcaatattaatgaa gtaggttccagtactaatgaagtgggctccagtattaatgaagtaggttccagtnnnnnn nnnnnnnnnnnnnnngatgaaaacattcaagcagaactaggtagaaacagagggccaaaa ttgaatgctgtgcttagattagggcttttgcaacctgaggtctgtaaacaaagtcttcct ataagtaattgtaagcatcctgaaataaaaaagcaagaacatgaagaannnttagttcag actgttaatacagacttctctccatgtctgatttcagataacctagaacagcctatggga agtagtcatgcgtctgaggtttgttctgagactcctgatgatctgttagatgatggtgaa ataaaggaagatactagttttgctgaaaatgacattaaggagagttctgctgtttttagc aaaagcatccagagaggagagctcagcaggagccctagccctttcacccatacacattta gctcagggttaccgaaaagaggccaagaaattagagtcctcagaagagaacttatctagt gag >Chimpanzee tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccaacataacagatgggctggaagtaaggaaacatgtaatgataggcggact cccagcacagaaaaaaaggtagatctgaatgctgatcccctgtgtgagagaaaagaatgg aataagcagaaactgccatgctcagagaatcctagagatnnnactgaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgagtggttttccagaagtgatgaactg ttaggttctgatgactcacatgatggggggtctgaatcaaatgccaaagtagctgatgta ttggacgttctaaatgaggtagatgaatattctggttcttcagagaaaatagacttactg gccagcgatcctcatgaggctttaatatgtaaaagtgaaagagttcactccaaatcagta gagagtaatactgaagacaaaatatttgggaaaacctatcggaggaaggcaagcctcccc aacttaagccatgtaactgaaaatctaattataggagcatttgttactgagccacagata atacaagagcgtcccctcacaaataaattaaagcgtaaaaggagagctacatcaggcctt catcctgaggattttatcaagaaagcagatttggcagttnnncaaaagactcctgaaatg ataaatcagggaactaaccaaatggagcagaatggtcaagtgatgaatattactaatagt ggtcatgagaataaaacaaaaggtgattctattcagaatgagaaaaatcctaacccaata gaatcactcgaaaaagaatctgctttcaaaacgaaagctgaacctataagcagcagtata agcaatatggaactcgaattaaatatccacaattcaaaagcacctaaaaagaataggctg aggaggaagtcttctaccaggcatattcatgcgcttgaactagtagtcagtagaaatcta agcccacctaattgtactgaattgcaaattgatagttgttctagcagtgaagagataaag aaaaaaaagtacaaccaaatgccagtcaggcacagcagaaacctacaactcatggaagat aaagaacctgcaactggagtcaagaagagtaacaagccaaatgaacagacaagtaaaaga catgacagcgatactttcccagagctgaagttaacaaatgcacctggttcttttactaac tgttcaaataccagtgaacttaaagaatttgtcaatcctagccttccaagagaagaagaa gaagagaaannnctagaaacagttaaagtgtctaataatgccgaagaccccaaagatctc atgttaagtggagaannnagggttttgcaaactgaaagatctgtagagagtagcagtatt tcattggtacctggtactgattatggcactcaggaaagtatctcgttactggaagttagc actctagggnnnaaggcaaaaacagaaccaaataaatgtgtgagtcagtgtgcagcattt gaaaaccccaagggactaattcatggttgttccaaagatactagaaatgacacagaaggc tttaagtatccattgggacatgaagttaaccacagtcgggaaacaagcatagaaatggaa gaaagtgaacttgatgctcagtatttgcagaatacattcaaggtttcaaagcgccagtca tttgctctgttttcaaatccaggaaatccagaagaggaatgtgcaacattctctgcccac tgtaggtccttaaagaaacaaagtccaaaagtcacttttgaacgtgaacaaaaggaacaa aatcaaggaaagaatgagtctaatatcaagcctgtacagacagttaatatcactgcaggc tttcctgtggtttgtcagaaagataagnnnccagttgattatgccaaatgtagtatcaaa ggaggctctaggttttgtctatcatctcagttcagaggcaacgaaactggactcattact ccaaataaacatggacttttacaaaacccatatcatataccaccactttttcccatcaag tcatttgttaaaactaaatgtaagaaaaacctgctagaggaaaactttgaggaacattca atgtcacctgaaagagaaatgggaaatgagaacnnnattccaagtacagtgagcacaatt agccgtaataacattagagaaaatgtttttaaagaagccagctcaagcaatattaatgaa gtaggttccagtactaatgaagtgggctccagtattaatgaagtaggttccagtnnnnnn nnnnnnnnnnnnnnngatgaaaacattcaagcagaactaggtagaaacagagggccaaaa ttgaatgctatgcttagattaggggttttgcaacctgaggtctataaacaaagtcttcct gaaagtaattgtaagcatcctgaaataaaaaagcaagaatatgaagaannngtagttcag actgttaatacagatttctctccatgtctgatttcagataacttagaacagcctatggga agtagtcatgcatctcaggtttgttctgagacacctgatgacctgttagatgatggtgaa ataaaggaagatactagttttgctgaaaatgacattaaggaaagttctgctgtttttagc aaaagcgtccagagaggagagcttagcaggagtcctagccctttcacccatacacatttg gctcagggttaccgaagaggggccaagaaattagagtcctcagaagagaacttatctagt gag >Gorilla tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaaacaaacagcctggctta gcaaggagccaacataacagatgggctggaagtaaggaaacatgtaatgataggcggact cccagcacagaaaaaaaggtagatctgaatgctgatcccctgtgtgagagaaacgaatgg aataagcagaaactgccatgctcagagaatcctagagatnnnactgaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgagtggttttccagaagtgatgaactg ttaggttctgatgactcacatgatggggggtctgaatcaaatgccaaagtagctgatgta ttggacgttctaaatgaggtagatgaatattctggttcttcagagaaaatagacttactg gccagtgatcctcatgaggctttaatatgtaaaagtgaaagagttcactccaaatcagta gagagtaatattgaagacaaaatatttgggaaaacctatcggaggaaggcaagcctcccc agcttaagccatgtaactgaaaatctaattataggagcatttgttactgagccacagata atacaagagcgtcccctcacaaataaattaaagcgtaaaaggagagctacatcaggcctt catcctgaggattttatcaagaaagcagatttggcagttnnncaaaagactcctgaaatg ataaatcagggaactaaccaaatggagcagaatggtcaagtgatgaatattactaatagt ggtcatgagaataaaacaaaaggtgattctattcagaatgagaaaaatcctaacccaata gaatcactagaaaaagaatctgctttcaaaacgaaagctgaacctataagcagcagtata agcaatatggaactcgaattaaatatccacaattcaaaagcgcctaaaaagaataggctg aggaggaagtcttctaccaggcatattcatgcgcttgaactagtagtcagtagaaatcta agcccacctaattgtactgaattgcaaattgatagttgttctagcagtgaagagataaag aaaaaaaagtacaaccaaatgccagtcaggcacagcagaaacctacagctcatggaagat aaagaacctgcaactggagccaagaagagtaacaagccaaatgaacagacaagtaaaaga catgacagcgatactttcccagagctgaagttaacaaatgcacctggttcttttactaac tgttcaaataccagtgaacttaaagaatttgtcaatcctagccttccaagagaagaaaaa gaagagaaannnctagaaacagttaaagtgtctaataatgccgaagaccccaaagatctc atgttaagtggagaannnagggttttgcaaactgaaagatctgtagagagtagcagtatt tcattggtacctggtactgattatggcactcaggaaagtatctcgttactggaagttagc actctagggnnnaaggcaaaaacagaaccaaataaatgtgtgagtcagtgtgcagcattt gaaaaccccaagggactaattcatggttgttccaaagatactagaaatgacacagaaggc tttaagtatccattgggacatgaagttaaccacagtcgggaaacaagcatagaaatggaa gaaagtgaacttgatgctcagtatttgcagaatacattcaaggtttcaaagcgccagtca tttgctctgttttcaaatccaggaaatccagaagaggaatgtgcaacattctctgcccac tctaggtccttaaagaaacaaagtccaaaagtcacttttgaatgtgaacaaaaggaagaa aatcaaggaaagaatgagtctaatatcaagcctgtacagacagttaatatcactgcaggc tttcctgtggtttgtcagaaagataagnnnccagttgattatgccaaatgtagtatcaaa ggaggctctaggttttgtctatcatctcagttcagaggcaacgaaactggactcattact ccaaataaacatggacttttacaaaacccatatcatataccaccactttttcccatcaag tcatttgttaaaactaaatgtaagaaaaacctgctagaggaaaactttgaggaacattca atgtcacctgaaagagaaatgggaaatgagaacnnnattccaagtacagtgagcacaatt agccgtaataacattagagaaaatgtttttaaagaagccagctcaagcaatattaatgaa gtaggttccagtactaatgaagtgggctccagtattaatgaagtaggttccagtnnnnnn nnnnnnnnnnnnnnngatgaaaacattcaagcagaactaggtagaaacagagggccaaaa ttgaatgctatgcttagattaggggttttgcaacctgaggtctataaacaaagtcttcct ggaagtaattgtaagcatcctgaaataaaaaagcaagaatatgaagaannngtagttcag actgttaatacagatttctctccatgtctgatttcagataacttagaacagcctatggga agtagtcatgcatctcaggtttgttctgagacacctgatgacctgttagatgatggtgaa ataaaggaagatactagttttgctaaaaatgacattaaggaaagttctgctgtttttagc aaaagcgtccagagaggagagcttagcaggagtcctagccctttcacccatacacatttg gctcagggttaccgaagaggggccaagaaattagagtcctcagaagagaacttatctagt gag >FlyingLem tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattacgcact aaagacagaataaatgttgaaaagactgaattctgtaataaaagcaaacagcctggctta gcaaggagccaggagagcagatgggctgaaagtaaggaaacatgtaatgataggcagacg cccagcacagagaaaaagatagatctaaatgctgattcccagcatgggagaaaagaacgg aatatgcagaaacctccataccctgagagtcctagagatnnnacccaagatgttccttgg ataacactaaacagcagcattcagaaagttaatgagtggttttccagaagtgatgaaatt ttagcttctgatgactcacgtgacagggtgtctgaatcaaatgccaaagtagctggtgca ttagaagttccaaatgatgtagatggatattctgattcttcagagaaagttgatttaatg gccagtgatcctcatgatgctttaatatgtaaaagtgaaagaatccactccagaccagta gagagtaatatcaaagataaaatatttgggaaaacctatcagaggaagacaagcctccct aacttgagccacgtaaatgaagatctaattataggagcatttgttacagaaccacagata acacaagagcgtcccctcacaaataaggtaaagcctaaaaggagaactacatcaggcctt catcctgaggattttatcaagaaagcagacttggcagttgttcaaaaaactcctgaaaag ataaatcagggaattgaccaaatggagcagaatgatcgagtgatgaatattattaatagt ggtcatgagaatgaaacaaaggatgattatgttcagaaagagaaaaatgctaacccaaca gaatcattggaaaaagaatctgctttcagaactaaagcagaacctataagcagcagtata agcaatatggaaatagaattaaatatccacaattcaaaaccatctaagaagaataggctg aggaagatgtcctctactaggcatattcatgcacttgaactagtagtcaatagaaatcca agcccacctaattatactgaactacaaattgatagttgttctagcagtgaagaaatagag aaaaaaaattccagccaaatgccagtcaggcacagcagaaagcttcaactcatggaaaat aaagaacctgcaactggagccaagaagagtaacaagccaaatgaacaaataagtagaaga cattccagtaatgctttcccagaactgcggttaacaaatgtacctgttttttttgctaac tgttcaagttctaataaacttcaagaatttatcgatcctagccttcaaagagaagaaata gaagagaacnnnctagaaacaattcatgtgtctaatagtgccaaagaccccaaagatttg gtgttaagtggggagnnnaagggtttgcaaactgaaagatctgtagagagtaccagtatt tcattagtacctgatactgattatggcactcaagacagtatctcaatattagaagctaac atcctagggnnnaaggcaaaaacagcaccaagtcaacatgcaaatcagtgtgcagcaatt gaaaaccccaaagaacttatccatggttgtcctaaaggtactagaaatgacacagaggat tttaaggatccattgagatgtggagttgaccacattcagaagacaagcatagaaatgcaa gagagtgaacttgatactcagtatttacaaaatatattcaaggtttcaaaacgtcagtca tttgctctcttttcaaatccaggaaatccagaaaaggagtgtgcaacagtctatgcccac tccaggttgttaaggaaacaaagtccaaaagtcactcctgaatgtgaacaaaaagaagaa aatgagggaaataaagagtctaaaatcaagcacatacaggcagttaataccactgtgggc ttttctgtcctttgtcagaatgttaagaagccaggtgattatgccaaatttagcattaaa ggagtctctaggcattgttcatcatctcagttcagaggcaatgaaactgaactcattact gcaaataaacatggaattttacaaaactcatgtcatatgtcatcactttcccccatcagg tcatctgttaaaattaaatgtaagaagaacctgtcagaggaaaggtttgaggaacattca gtgtcacctgaaagagcaatggcaaacaagagaatcattcaaagtacagtgaacacaatt agccaaaataacattagagacagtgcttctaaagaagccagctcaagcagtattaatgaa gtaggttccagtactaatgaagtaggctccagtattaatgaagtaggtcccagtnnnnnn nnnnnnnnnnnnnnnggtgaaaacattcaagcagaactaggtagaaacagaggacctaaa ttaagtgctatgcttagattaggcctcatgcaacctgaagtttacaagcaaaatcttcct ttaggtaattgtaaacatcctgaaataaggnnncaagaagaaaatgaaggaatagttcag gctgttaatacaaatctgtctctgtgcctaatttcacataacctcgaacaacctatggaa agtagtcatgcttcccaggtttgttctgagacacctgatgacctgttagatggtgatgag ataaaggaaaacaccagctttgctgaaagtgacagtaaggaaagatctgctgtttttagc aaaagtgtccagagaggagagttaagcaggagccctagcccttttgcccaaacatgtttg gctcagggtcaccaaagaggagccaggaaattagagtcttctgaagagaacgtatctagt gag >Galago tgtggcaaaaatactcatgccagctcattacagcatgagagcagcagtttattactcact aaagacaaaatgaatgtagaaaaggctgaattttgtaataaaagcaaacagcctggctta gcaaggagccaacagagcagatcggctcaaagtaaggaaacatgcaatgataggcacact tgcagccctgagcaaaaggtagatctgaatactgctcccccatatgggagaaaagaacag aataaggagaaacttctatgctccaagaatcctagagatnnnagccaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgaatggttttctagaagtgatgaaatg ttaacttctgatgactcacatgatgagggttctgaatcacatgctgaagtagctggagcc ttagaagttccaagtgaagtagatggatattccagttcctcagagaaaatagacttactg gccagtgatcctcattatcctataatatgtaaaagtgaaagagttcactccaaaccaata aagagtaaagttgaagataaaatatttgggaaaacttatcggaggaaggcaagcctccct aacttaagccatgtaactgaaaatctaattataagagcagctgctactgagccacagata acacaagagtgttccctcacaaataaattaaaacgtaaaaggagaactacatcaggtctt tgtcctgaggattttatcaagaaggcagatttggcagttgttcaaaagacacctgaaaag agaattcagggaactaaccaagtggatcagaatagtcacgtggtaaatattactaatagt ggttatgagaatgaaacaaaaggtgattatgttcagaatgaaaaaaatgctaactcaaca gaatcattggaaaaagaatcttctctcggaactaaagctgaacctataagcagcagtata agtaatatgaaattagaattaaatattcacaattcaaaagcaagtaaaaagaaaaggctg aggaagaagtcttctagcaggcatattcgtgcacttgaactagtagtcaataaaaatcca agccctcctaatcataccaacctacaaattgacagttgttctagcagtgaagaaataaag gataaaagttctgaccaaataccagtcaggcatagcagaaagcctggactcatggaagat agagaacctgcaactggagccaagaaaagtaacaagccaaatgagcaaataagtaaaaga catgtcagtgatactttcccagaagtggcattaacaaatatatctagtttttttactaac tgttcaggttctaatagacttaaagaatttgtcaatcctagccttcaaagaaaaaaaaca gaagagaacttagaagaaacaattcaagtgtctaatagtaccaaaggtccggtgttaagt ggagaaagggttttgnnncaaattgaaagtgaagaaagatctataaaaagcaccagtatt tcattggtacctgatactgattatggtactcaggacagtaactcgttactgaaagttaaa gtcttacggnnnaaggtgaaaacagcaccaaataaacatgcaagtcagggtacagccact gaaaaccccaaggaactaatccatggttgctctaaagatactggaaatgacacagagggc tataaggatccattgagacatgaaattaaccacattcagaagataagcatggaaatggaa gacagtgaacttgatactcagtatttacagaatacattcaagttttcaaagcgtcagtcg tttgctctgttttcaaacctannnnnnnnnggaaaggaatgtgcaacagtctgtgcccag tctctctctgcgtccttaagaaaaggttcaaaagtcattcttgaatgtgaacaaatagaa aatccaggaatgaaagagcctaaaatcaagcatatacagggaaataatatcaatacaggc ttctctgtagtttgtcagaaagataagaaaacagatgattatgccaaatacagcatcaaa gaagcatctaggttttgtttgtcaaatcagtttcgagacaatgaaactgaatccattact gtaaataaacttggaattttacaaaacctctatcatataccaccactttctcctatcagg ctatttgataaaactaaatgtaatacaaacctgttagaggaaaggtttgaagaacattca gtgttacctgaaaaagcagtaggaaacgagaacaccgttccaagtacaatgaatacaatt aaccaaaataacnnnagagaaagtgcttataaagaagccagttcaagcagtatcaatgaa gtaagctcgagtactaatgaagtgggctccagtgttaacgaagtaggccccagtnnnnnn nnnnnnnnnnnnnnnagtgaaaacattcaagcagaactagataaaaacagaggacctaag ttgaatgctgtgcttagattaggtcttatgcaacctgaagtctataaacaaaatcttcct ataagtaattgtgaacatcctaaaataaaagggcaagaagaaaatggannngtagttcaa cctgttaatccagatttttcttcatgtctaatttcagataacctagaacaacctacgaga agtagtcatgcttctcagctttgttctgagacacctgatgacttattagttgatgatgaa ctaaaggaaaataccagttttgctgaaaataacattaaggaaagatctgctgtttttagc aaaaatgtcatgagaagagagattagcaggagccctagccctttagcccatatacatttg actcaggctcaccaaagagaggttaggaaattagagtcctcagaagagaacatgtctagt gaa >Human tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttattactcact aaagacagaatgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccaacataacagatgggctggaagtaaggaaacatgtaatgataggcggact cccagcacagaaaaaaaggtagatctgaatgctgatcccctgtgtgagagaaaagaatgg aataagcagaaactgccatgctcagagaatcctagagatnnnactgaagatgttccttgg ataacactaaatagcagcattcagaaagttaatgagtggttttccagaagtgatgaactg ttaggttctgatgactcacatgatggggagtctgaatcaaatgccaaagtagctgatgta ttggacgttctaaatgaggtagatgaatattctggttcttcagagaaaatagacttactg gccagtgatcctcatgaggctttaatatgtaaaagtgaaagagttcactccaaatcagta gagagtaatattgaagacaaaatatttgggaaaacctatcggaagaaggcaagcctcccc aacttaagccatgtaactgaaaatctaattataggagcatttgttactgagccacagata atacaagagcgtcccctcacaaataaattaaagcgtaaaaggagacctacatcaggcctt catcctgaggattttatcaagaaagcagatttggcagttnnncaaaagactcctgaaatg ataaatcagggaactaaccaaacggagcagaatggtcaagtgatgaatattactaatagt ggtcatgagaataaaacaaaaggtgattctattcagaatgagaaaaatcctaacccaata gaatcactcgaaaaagaatctgctttcaaaacgaaagctgaacctataagcagcagtata agcaatatggaactcgaattaaatatccacaattcaaaagcacctaaaaagaataggctg aggaggaagtcttctaccaggcatattcatgcgcttgaactagtagtcagtagaaatcta agcccacctaattgtactgaattgcaaattgatagttgttctagcagtgaagagataaag aaaaaaaagtacaaccaaatgccagtcaggcacagcagaaacctacaactcatggaaggt aaagaacctgcaactggagccaagaagagtaacaagccaaatgaacagacaagtaaaaga catgacagcgatactttcccagagctgaagttaacaaatgcacctggttcttttactaag tgttcaaataccagtgaacttaaagaatttgtcaatcctagccttccaagagaagaaaaa gaagagaaannnctagaaacagttaaagtgtctaataatgctgaagaccccaaagatctc atgttaagtggagaannnagggttttgcaaactgaaagatctgtagagagtagcagtatt tcattggtacctggtactgattatggcactcaggaaagtatctcgttactggaagttagc actctagggnnnaaggcaaaaacagaaccaaataaatgtgtgagtcagtgtgcagcattt gaaaaccccaagggactaattcatggttgttccaaagataatagaaatgacacagaaggc tttaagtatccattgggacatgaagttaaccacagtcgggaaacaagcatagaaatggaa gaaagtgaacttgatgctcagtatttgcagaatacattcaaggtttcaaagcgccagtca tttgctccgttttcaaatccaggaaatgcagaagaggaatgtgcaacattctctgcccac tctgggtccttaaagaaacaaagtccaaaagtcacttttgaatgtgaacaaaaggaagaa aatcaaggaaagaatgagtctaatatcaagcctgtacagacagttaatatcactgcaggc tttcctgtggttggtcagaaagataagnnnccagttgataatgccaaatgtagtatcaaa ggaggctctaggttttgtctatcatctcagttcagaggcaacgaaactggactcattact ccaaataaacatggacttttacaaaacccatatcgtataccaccactttttcccatcaag tcatttgttaaaactaaatgtaagaaaaatctgctagaggaaaactttgaggaacattca atgtcacctgaaagagaaatgggaaatgagaacnnnattccaagtacagtgagcacaatt agccgtaataacattagagaaaatgtttttaaagaagccagctcaagcaatattaatgaa gtaggttccagtactaatgaagtgggctccagtattaatgaaataggttccagtnnnnnn nnnnnnnnnnnnnnngatgaaaacattcaagcagaactaggtagaaacagagggccaaaa ttgaatgctatgcttagattaggggttttgcaacctgaggtctataaacaaagtcttcct ggaagtaattgtaagcatcctgaaataaaaaagcaagaatatgaagaannngtagttcag actgttaatacagatttctctccatatctgatttcagataacttagaacagcctatggga agtagtcatgcatctcaggtttgttctgagacacctgatgacctgttagatgatggtgaa ataaaggaagatactagttttgctgaaaatgacattaaggaaagttctgctgtttttagc aaaagcgtccagaaaggagagcttagcaggagtcctagccctttcacccatacacatttg gctcagggttaccgaagaggggccaagaaattagagtcctcagaagagaacttatctagt gag >Mouse tgtggcacagatgctcatgccagctcattacagcctgagaccagcagtttattgctcatt gaagacagaatgaatgcagaaaaggctgaattctgtaataaaagcaaacagcctggcata gcagtgagccagcagagcagatgggctgcaagtaaaggaacatgtaacgacaggcaggtt cccagcactggggaaaaggtaggtccaaacgctgactcccttagtgatagagagaagtgg actcacccgcaaagtctgtgccctgagaattctggagctnnnaccaccgatgttccttgg ataacactaaatagcagcgttcagaaagttaatgagtggttttccagaactggtgaaatg ttaacttctgacagcgcatctgccaggaggcacgagtcaaatgctgaagcagctgttgtg ttggaagtttcaaacgaagtggatgggggttttagttcttcaaggaaaacagacttagta acccccgacccccatcatactttaatgtgtaaaagtggaagagacttctccaaaccagta gaggataatatcagtgataaaatatttgggaaatcctatcagagaaagggaagccgccct cacctgaaccatgtgactgaaattataggcacannnnnntttattacagaaccacagata acacaagagcagcccttcacaaataaattaaaacgtaagagannnnnnagtacatccctt caacctgaggacttcatcaagaaagcagattcagcaggtgttcaaaggactcctgacaac ataaatcagggaactgacctaatggagccaaatgagcaagcagtgagtactaccagtaac tgtcaggagaacaaaatagcaggtagtaatctccagaaagagaaaagcgctcatccaact gaatcattgagaaaggaacctgcttccacagcaggagccaaatctataagcaacagtgta agtgatttggaggtagaattaaacgtccacagttcaaaagcacctaagaaaaataggctg aggaggaagtcttctatcaggtgtgctcttccacttgaaccannnatcagtagaaatcca agcccacctacttgtgctgagcttcaaatcgatagttgtggtagcagtgaagaaacaaag aaaaaccattccaaccaacagccagccgggcaccttagagagcctcaactcatcgaagac actgaacctgcagcggatgccaagaagaacgagnnnccaaatgaacacataaggaagaga cgtgccagcgatgctttcccagaagagaaattaatgaacaaagctggtttattaactagc tgttcaagtcctagaaaatctcaagggcctgtcaatcccagccctcagagaacaggaaca gagcaannnnnncttgaaacacgccaaatgtctgacagtgccaaagaactcggggatcgg gtcctaggaggagagcccagtggcaaaaccactgaccgatctgaggagagcaccagcgta tccttggtacctgacactgactacgacactcagaacagtgtctcagtcctggacgctcac actgtcagannntatgcaagaacaggatccgctcagtgtatgactcagtttgtagcaagc gaaaaccccaaggaactcgtccatggctctaacaatgctgggnnnagtggcacagagggt ctcaagccccccttgagacacgcgcttaacctcagtcaggagaaannngtagaaatggaa gacagtgaacttgatactcagtatttgcagaatacatttcaagtttcaaagcgtcagtca tttgctttattttcaaaacctagaagtccccaaaaggactgtnnnnnnnnnnnngctcac tctgtgccctcaaaggaactgagtccaaaggtgacagctaaaggtaaacaaaaagaacgt cagggacaggaagaatttgaaatcagtcacgtacaagcagttgcggccacagtgggcnnn ttacctgtgccctgtcaagaaggtaagnnnctagctgctgatacaatgtgtnnnnnngat agaggttgtaggctttgtccatcatctcattacagaagcggggagaatggactcagcgcc acaggtaaatcaggaatttcacaaaactcacattttaaacaatcagtttctcccatcagg tcatctataaaaactgacaataggaaacctctgacagagggacgatttgagagacataca tcatcaactgagatggcggtgggaaatgagaacattcttcagagtacagtgcacacagtt agcctgaataacnnnagaggaaatgctnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn nnnnnnnnntgtcaagaagccggctcgggcagtattcatgaagtatgttccactnnnnnn nnnnnnnnnnnnnnnggtgactccttcccaggacaactaggtagaaacagagggcctaag gtgaacactgtgcctccattagatagtatgcagcctggtgtctgtcagcaaagtgttcct gtaagtgatnnnaagtatcttgaaataaaaaagnnnnnnnnnnnnnnncaggagggtgag gctgtctgtgcagacttctctccatgtctattctcagaccatcttgagcaatctatgagt ggtnnnaaggtttttcaggtttgctctgagacacctgatgacctgctggatgatgttgaa atacagggacatactagctttggtgaaggtgacataatggagagatctgctgtctttaac ggaagcatcctgagaagggagtccagtaggagccctagtcctgtaacccatgcatcgaag tctcagagtctccacagagcgtctaggaaattagaatcgtcagaagagagcgactccact gag >HowlerMon tgtggcacaaatactcatgccagctcattacagcatgagaacagcagtttgttactcact aaagacacactgaatgtagaaaaggctgaattctgtaataaaagcaaacagcctggctta gcaaggagccaacataacagatgggctgaaagtgaggaaacatgtaatgataggcagact cccagcacagagaaaaaggtagatgtggatgctgatcccctgcatgggagaaaagaatgg aataagcagaaacctccgtgctctgagaatcctagagatgatactgaagatgttgcttgg ataatgctaaatagcagcattcagaaagttaatgagtggttttccagaagtgatgaactg ttaacttctgatgactcacatgatggggggtctgaatcaaatgccaaagtagctgaagca ttggaagttctaaatgaggtagatggatattctagttcttcagagaaaatagacttactg gccagtgatcctcatgatcatttgatatgtaaaagtgaaagagttcactgcaaatcagta gagagtagtattgaagataaaatatttgggaaaacctatcggaggaaggcaagcctccct aacttgagccacgtaactgaaaatctaattataggagcatttgttactgagccacagata atacaagagcatcctctcacaaataaattaaagcgtaaaaggagagttacatcaggactt catcctgaggattttatcaagaaagcagatttggcagttnnncaaaagactcctgaaaag ataaatcagggaactaaccaaacagagcggaatgatcaagtgatgaatattactaacagt ggtcatgagaataaaacaaaaggtgattctattcagaatgagaacaatcctaacccagta gaatcactggaaaaagaannntcattcaaaagtaaagctgaacctataagcagtagtata agcaatatggaattagaattgaatgtccacaattccaaagcatctaaaaagaataggctg agaaggaagtcttctaccaggcatattcatgagcttgaactagtagtcagtagaaatcta agcccacctaattatactgaagtacaaattgatagttgttctagcagtgaagagataaag aaaaaaaattacaaccaaatgccagtcaggcacagcagaaagctacaactcatggaagat aaagaacgtgcagctagagccaaaaagagtagcaagccaaatgaacaaacaagtaaaaga catgccagtgatactttcccagaactgaggttaacaaacatacctggttcttttactaac tgttcaaatactaatgaatttaaagaatttgtcaatcctagccttccaagagaacaaaca gaagagaaannnctagaaacagttaaactgtctaataatgccaaagaccccaaagatctc atgttaagtggagaannnagtgttttgcaaattgaaagatctgtagagagtagcagtatt ttgttgatacctggtactgattatggcactcaggaaagtatctcattactggaagttagc actctggggnnnaaggcaaaaacagaaccaaataaatgtgtgagtcagtgtgcagcattt gaaaaccccaaggaactaattcatggttgttctaaagatactagaaatggcacagaaggc ttgaagtatccattgggacctgaagttaactacagtcaggaaacaagcatagatatgaga gaaagtgaacttgatactcaatatttgcagaatacattcaaggtttcaaagcgccagtca tttgctctgttttcaaatccaggaaatccagaaaaggaatgtgcaacattctctgcctgc tctaggtccttaaagaaacaaagtccaaaggtcactcctgaatgtgaacaaaaggaagaa aatcaaggagagaaagagtctaatatcgagcttgtagagacagttaataccactgcaggc tttcctatggtttgtcagaaagataagnnnccagttgattatgccagatgtatcgaannn ggaggctctaggctttgtctatcatctcagttcagaggcaacgaaactggactcattatt ccaaataaacatggacttttacagaacccatatcatatgtcaccgcttattcccaccagg tcatttgttaaaactaaatgtaagaaaaacctgctagaagaaaactctgaggaacattca atgtcacctgaaagagcaatgggaaacaagaacatcattccaagtacagtgagcacaatt agccataataacnnnagagaaaatgcttttaaagaaaccagctcaagcagtatttatgaa gtaggttccagtactaatgaagcaggttctagtactaatgaagtaggctccagtattaat gaagtaggttccagtgatgaaaacattcaagcagagctaggtagaaacagaaggccaaaa ttgaatgctatgcttagattagggcttctgcaacctgagatttgtaagcaaagtcttcct ataagtgattgtaaacatcctgaaattaaaaagcaagaacatgaagaannngtagttcag actgttaatacagacgtctctctatgtctgatttcatataacctagaacagcatatggga agcagtcatacatctcaggtttgttctgagacacctgacaacctgttagatgatggtgaa ataaaggaagatactagttttgctgaatatggcattaaggagacttctactgtttttagc aaaagtgtccagagaggagagctcagcaggagccctagccctttcacccatacacatttg gctcaggtttaccaaagaggggccaagaaattagagtcctcggaagagaatttatctagt gag cogent3-scinexus-b16bc06/tests/data/sample.tsv000066400000000000000000000000641525761402500214210ustar00rootroot00000000000000id name value 1 alpha 10.5 2 beta 20.3 3 gamma 30.1 cogent3-scinexus-b16bc06/tests/data/scitrack.log000066400000000000000000000124151525761402500217130ustar00rootroot000000000000002019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO system_details : system=Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO python : 3.7.3 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO user : gavin 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO command_string : /Users/gavin/miniconda3/envs/c3dev/lib/python3.7/site-packages/ipykernel_launcher.py -f /Users/gavin/Library/Jupyter/runtime/kernel-b12c134a-a2d4-4a8d-b7d2-f226e12fe628.json 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO composable function : load_unaligned(type='sequences', moltype='dna', format='fasta') + take_named_seqs(type='sequences', names=('Algerian mouse', 'Mouse', 'Rat'), negate=False) + min_length(type='sequences', length=600, motif_length=1, subtract_degen=True, moltype=None) + progressive_align(type='sequences') + omit_degenerates(type='aligned', moltype='dna', gap_is_degen=True, motif_length=3) + write_db(type='output', data_path='../data/rodent_cds/aligned_xy.tinydb', name_callback=None, create=True, if_exists='overwrite', suffix='json') 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000031379.fa 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 3c740b2f8b4713754e23d612e2489768 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000031379.json 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 07babd6d7eedfe27fa1648213f4609c9 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000031239.fa 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 43c78bd597e3edf7765e883aea8344b3 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000031239.json 2019-07-03 12:17:26 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 236beef72a6f6f5aaa7afabf79fc3e5d 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000031328.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : f255615238a754aa30fa316d126fa651 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000031328.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : f93ccf09e267a513b72472afdb6e7229 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000063663.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 07fb8fff9da3cd37f5c3b1057236a44b 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO FALSE : min_length : 543 < min_length 600 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000042225.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 701311f0178ba16e848ed08a57a7aa47 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000042225.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : bc0878d93bff8bc032feeebe57af642d 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000035847.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : bf4a639e534aa97d2bbc215c3b1c7b23 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000035847.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : bb218b33c4bc1fd9b1dff27a1351c8b2 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000054453.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 79b9e38b2e2d4129fb593503ea50a79f 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000054453.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 7c7edeb9a25fb49dff7298a4736a0a7b 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000073010.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 122875a99fcb55055f850a1de3cf09ec 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000073010.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 8da59b555fd94d22dd8a05574cd81af6 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000025261.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : 5eab1a81a7e62e08607f65ff0437a14f 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000025261.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 9c9eba09762838f832c40383983d4221 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input : ../data/rodent_cds/raw_xy/ENSMUSG00000034055.fa 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO input md5sum : d91d611ae9f66c5152b4af0aa2f421e6 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output : ENSMUSG00000034055.json 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO output md5sum : 0f309bcc4d8856249d0f5c26fa6f455c 2019-07-03 12:17:39 eratosthenes.uds.anu.edu.au:92022 INFO TIME TAKEN : 13.204617977142334 cogent3-scinexus-b16bc06/tests/test_composable.py000066400000000000000000003247641525761402500222460ustar00rootroot00000000000000import inspect import pickle import shutil from copy import copy from pathlib import Path from pickle import dumps, loads from unittest.mock import Mock import pytest from citeable import Software from numpy import array, ndarray from scitrack import CachingLogger try: import cogent3 as c3 from cogent3.app import typing as c3types from cogent3.util.union_dict import UnionDict except ImportError: c3 = None c3types = None UnionDict = None from scinexus import open_data_store from scinexus import typing as snx_types from scinexus.composable import ( GENERIC, LOADER, NON_COMPOSABLE, WRITER, ComposableApp, LoaderApp, NonComposableApp, NotCompleted, NotCompletedType, WriterApp, _get_raw_hints, _make_logfile_name, _proxy_input, define_app, is_app, is_app_composable, propagate_source, source_proxy, ) from scinexus.data_store import ( DataMember, DataStoreDirectory, Mode, get_unique_id, set_id_from_source, ) from scinexus.deserialise import deserialise_object from scinexus.sqlite_data_store import DataStoreSqlite def _typed_main(self, val: int) -> int: return val def test_composable(): """correctly form string""" @define_app class app_dummyclass_1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val @define_app class app_dummyclass_2: def __init__(self, b): self.b = b def main(self, val: int) -> int: return val aseqfunc1 = app_dummyclass_1(1) aseqfunc2 = app_dummyclass_2(2) comb = aseqfunc1 + aseqfunc2 expect = "app_dummyclass_1(a=1) + app_dummyclass_2(b=2)" got = str(comb) assert got == expect def test_composables_reuse(): """apps can be reused in multiple compositions""" @define_app class app_dummyclass_1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val @define_app class app_dummyclass_2: def __init__(self, b): self.b = b def main(self, val: int) -> int: return val @define_app class app_dummyclass_3: def __init__(self, c): self.c = c def main(self, val: int) -> int: return val one = app_dummyclass_1(1) two = app_dummyclass_2(2) three = app_dummyclass_3(3) one + three two + three # reuse of three now works # originals are not mutated assert one.input is None assert two.input is None assert three.input is None def test_composable_to_self(): """this should raise a ValueError""" @define_app class app_dummyclass_1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val app1 = app_dummyclass_1(1) with pytest.raises(ValueError): _ = app1 + app1 def test_err_result(): """excercise creation of NotCompletedResult""" result = NotCompleted(NotCompletedType.FAIL, "this", "some obj") assert not result assert result.origin == "this" assert result.message == "some obj" assert result.source is None # check source correctly deduced from provided object fake_source = Mock() fake_source.source = "blah" del fake_source.info result = NotCompleted(NotCompletedType.FAIL, "this", "err", source=fake_source) assert result.source == "blah" try: _ = 0 msg = "error message" raise ValueError(msg) except ValueError as err: result = NotCompleted(NotCompletedType.FAIL, "this", err.args[0]) assert result.message == "error message" def test_not_completed_result(): """should survive roundtripping pickle""" err = NotCompleted(NotCompletedType.FAIL, "mytest", "can we roundtrip") p = dumps(err) new = loads(p) assert err.type == new.type assert err.message == new.message assert err.source == new.source assert err.origin == new.origin def test_composable_variable_positional_args(): """correctly associate argument vals with their names when have variable positional args""" @define_app class pos_var_pos1: def __init__(self, a, b, *args): self.a = a self.b = b self.args = args def main(self, val: int) -> int: return val instance = pos_var_pos1(2, 3, 4, 5, 6) assert instance._init_vals == {"a": 2, "b": 3, "args": (4, 5, 6)} def test_composable_minimum_parameters(): """correctly associate argument vals with their names when have variable positional args and kwargs""" def test_func1(arg1) -> int: return 1 with pytest.raises(ValueError): _, _ = _get_raw_hints(test_func1, 2) def test_composable_return_type_hint(): """correctly associate argument vals with their names when have variable positional args and kwargs""" def test_func1(arg1): return 1 with pytest.raises(TypeError): _, _ = _get_raw_hints(test_func1, 1) def test_composable_firstparam_type_hint(): """correctly associate argument vals with their names when have variable positional args and kwargs""" def test_func1(arg1) -> int: return 1 with pytest.raises(TypeError): _, _ = _get_raw_hints(test_func1, 1) def test_composable_firstparam_type_is_None(): """correctly associate argument vals with their names when have variable positional args and kwargs""" def test_func1(arg1: None) -> int: return 1 with pytest.raises(TypeError): _, _ = _get_raw_hints(test_func1, 1) def test_composable_return_type_is_None(): """correctly associate argument vals with their names when have variable positional args and kwargs""" def test_func1(arg1: int) -> None: return with pytest.raises(TypeError): _, _ = _get_raw_hints(test_func1, 1) def test_composable_variable_positional_args_and_kwargs(): """correctly associate argument vals with their names when have variable positional args and kwargs""" @define_app class pos_var_pos_kw2: def __init__(self, a, *args, c=False): self.a = a self.c = c self.args = args def main(self, val: int) -> int: return val instance = pos_var_pos_kw2(2, 3, 4, 5, 6, c=True) assert instance._init_vals == {"a": 2, "args": (3, 4, 5, 6), "c": True} def test_supplied_args_distinguishes_default_from_passed(): """an argument equal to its own default is still recorded as supplied""" sentinel = object() @define_app class takes_default: def __init__(self, a, b=sentinel): self.a = a self.b = b def main(self, val: int) -> int: return val assert takes_default(1)._supplied_args == {"a": 1} assert takes_default(1, b=sentinel)._supplied_args == {"a": 1, "b": sentinel} assert takes_default(1, 2)._supplied_args == {"a": 1, "b": 2} def test_supplied_args_for_function_app(): """a decorated function records supplied arguments the same way""" @define_app def scaled(val: int, factor: int = 2) -> int: return val * factor assert scaled()._supplied_args == {} assert scaled(3)._supplied_args == {"factor": 3} def test_supplied_args_unpacks_var_keyword(): """a name absorbed by **kwargs is reported under that name, not as 'kwargs'""" @define_app class absorbs: def __init__(self, a=1, **kwargs): self.a = a self.kwargs = kwargs def main(self, val: int) -> int: return val assert absorbs()._supplied_args == {} assert absorbs(id_from_source=len)._supplied_args == {"id_from_source": len} assert absorbs(2, b=3)._supplied_args == {"a": 2, "b": 3} def test_supplied_args_var_positional(): @define_app class variadic: def __init__(self, a, *args): self.a = a self.args = args def main(self, val: int) -> int: return val assert variadic(1)._supplied_args == {"a": 1} assert variadic(1, 2)._supplied_args == {"a": 1, "args": (2,)} def test_supplied_args_first_parameter_not_named_self(): """the parameter that took cls is excluded whatever the app named it""" @define_app class renamed: def __init__(this, x=1): this.x = x def main(this, val: int) -> int: return val assert renamed()._supplied_args == {} assert renamed(2)._supplied_args == {"x": 2} def test_supplied_args_function_app_parameter_named_self(): """a function app binds no cls, so a parameter called self is a real argument""" @define_app def selfish(val: int, self: int = 1) -> int: return val + self assert selfish()._supplied_args == {} assert selfish(5)._supplied_args == {"self": 5} def test_supplied_args_survives_pickling(): """__new__ runs with no arguments on unpickling, so the record must be state""" assert loads(dumps(func2app(2)))._supplied_args == {"exponent": 2} def test_app_decoration_fails_with_slots(): with pytest.raises(NotImplementedError): @define_app class app_not_supported_slots1: __slots__ = ("a",) def __init__(self, a): self.a = a def main(self, val: int) -> int: return val def test_repeated_decoration(): @define_app class app_decorated_repeated1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val with pytest.raises(TypeError): define_app(app_decorated_repeated1) def test_recursive_decoration(): @define_app class app_docorated_recursive1: def __init__(self, a): self.a = a def main(self, val: int) -> int: define_app(app_docorated_recursive1) return val with pytest.raises(TypeError): app_docorated_recursive1().main(1) def test_inheritance_from_decorated_class(): @define_app class app_decorated_first1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val with pytest.raises(TypeError): @define_app class app_inherits_decorated1(app_decorated_first1): def __init__(self, a): self.a = a def main(self, val: int) -> int: return val def test_super_in_decorated_class_with_base(): """super() works when decorated class inherits from an undecorated base.""" class MyBase: def __init__(self, x: int) -> None: self.x = x @define_app class my_app(MyBase): def __init__(self, x: int = 1) -> None: super().__init__(x) def main(self, data: int) -> int: return data + self.x app = my_app(x=5) assert app.x == 5 assert app(3) == 8 def test_super_in_composableapp_subclass_with_base(): """super() works when ComposableApp subclass also inherits from another base.""" class MyBase: def __init__(self, x: int) -> None: self.x = x class my_app(ComposableApp[int, int], MyBase): def __init__(self, x: int = 1) -> None: super().__init__(x) def main(self, data: int) -> int: return data + self.x app = my_app(x=5) assert app.x == 5 assert app(3) == 8 # have to define this at module level for pickling to work @define_app def func2app(arg1: int, exponent: int) -> float: return arg1**exponent @define_app def float2int(val: float) -> int: return int(val) def test_decorate_app_function(): """works on functions now""" sqd = func2app(exponent=2) assert sqd(3) == 9 assert inspect.isclass(func2app) def test_roundtrip_decorated_function(): """decorated function can be pickled/unpickled""" sqd = func2app(exponent=2) u = pickle.loads(pickle.dumps(sqd)) assert u(4) == 16 def test_roundtrip_composed_app(): """composed app can be pickled/unpickled""" composed = func2app(exponent=2) + float2int() u = pickle.loads(pickle.dumps(composed)) assert u(3) == 9 def test_decorated_func_optional(): @define_app(app_type=NON_COMPOSABLE) def power(val: int, pow: int = 1) -> int: return val**pow sqd = power(2) assert sqd(3) == 9 def test_decorated_func_repr(): def kw(val: int = 1) -> int: return val**val def kw_kw(val: int = 1, pow: int = 1) -> int: # noqa: A002 return val**pow def pos(val: int) -> int: return val**val def pos_pos(val: int, pow: int) -> int: # noqa: A002 return val**pow def pos_kw(val: int, pow: int = 1) -> int: # noqa: A002 return val**pow fns = {fn: func for fn, func in locals().items() if callable(func)} args = {"pos": 4, "kw": {"pow": 3}} for name, func in fns.items(): app = define_app(func) if len(name.split("_")) == 1: instance = app() expect = f"{name}()" elif name.endswith("kw"): instance = app(**args["kw"]) expect = f"{name}(pow={args['kw']['pow']})" else: instance = app(args["pos"]) expect = f"{name}(pow={args['pos']})" assert repr(instance) == expect, name def test_decorated_func_just_args(): @define_app(app_type=NON_COMPOSABLE) def power(val: int, pow: int) -> int: # noqa: A002 return val**pow sqd = power() assert sqd(3, 3) == 27 def test_decorated_app_is_app(): """check is_app for define_app decorated apps""" @define_app class app_test_isapp1: def main(self, data: int) -> int: return data assert is_app(app_test_isapp1) def test_undecorated_app_is_not_an_app(): """check is_app for non-decorated apps""" class app_not_composable1: def main(self, data: int) -> int: return data assert not is_app(app_not_composable1) def test_add_non_composable_apps(): @define_app(app_type=NON_COMPOSABLE) class app_non_composable1: def __init__(self): pass def main(self, val: int) -> int: return val @define_app(app_type=NON_COMPOSABLE) class app_non_composable2: def __init__(self): pass def main(self, val: int) -> int: return val app_non_composable1.__add__ = ComposableApp.__add__ app_non_composable2.__add__ = ComposableApp.__add__ app1 = app_non_composable1() app2 = app_non_composable2() with pytest.raises(TypeError): app1 + app2 _types_null = (list, []), (tuple, ()) @pytest.mark.parametrize(("in_type", "input_"), _types_null) def test_handles_null_series_input(in_type, input_): """apps correctly handle null output""" @define_app def null_in(val: in_type, pow: int) -> int: # noqa: A002, ARG001 return 2 app = null_in(pow=2) got = app(input_) assert isinstance(got, NotCompleted) @pytest.mark.parametrize("ret_type", [0, array([]), [], {}]) def test_handles_null_output(ret_type): """apps correctly handle null output""" @define_app def null_out(val: ndarray, pow: int) -> int: # noqa: A002, ARG001 return ret_type app = null_out(pow=2) d = array([3, 3]) got = app(d) assert isinstance(got, type(ret_type)) def test_handles_None(): """apps correctly handle null output""" @define_app def none_out(val: ndarray, pow: int) -> int: # noqa: A002, ARG001 return None @define_app def take_int(val: int) -> int: return val app = none_out(pow=2) d = array([3, 3]) got = app(d) assert isinstance(got, NotCompleted) app = none_out(pow=2) + take_int() d = array([3, 3]) got = app(d) assert isinstance(got, NotCompleted) def test_validate_data_type_not_completed_pass_through(): # returns the instance of a NotCompleted created by an input @define_app def take_int1(val: int) -> int: # noqa: ARG001 return NotCompleted( NotCompletedType.ERROR, "take_int1", "external to app", source="unknown" ) @define_app def take_int2(val: int) -> int: return val app = take_int1() + take_int2() got = app(2) assert got.origin == "take_int1" @pytest.mark.parametrize( ("first", "ret"), [(tuple[set[str]], int), (int, tuple[set[str]])], ) def test_complex_type(first, ret): # deep nesting now allowed (typeguard handles arbitrary nesting) @define_app class x: def main(self, data: first) -> ret: return data @pytest.mark.parametrize("hint", [tuple[set[str]], tuple[tuple[set[str]]]]) def test_complex_type_depths(hint): # deep nesting now allowed (typeguard handles arbitrary nesting) @define_app class x: def main(self, data: hint) -> bool: # noqa: ARG002 return True @pytest.mark.parametrize("hint", [int, set[str]]) def test_complex_type_allowed_depths(hint): # allowed <=2-deep nesting of types @define_app class x: def main(self, data: hint) -> int: # noqa: ARG002 return int @pytest.mark.parametrize( "meth", [ "__call__", "__repr__", "__str__", "__new__", "__copy__", "__eq__", "__add__", "input", "apply_to", "set_logger", "_validate_data_type", "as_completed", "_get_citations", "disconnect", ], ) def test_forbidden_methods_composable_app(meth): class app_forbidden_methods1: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val def function1(): pass setattr(app_forbidden_methods1, meth, function1) with pytest.raises(TypeError): define_app(app_type=WRITER)(app_forbidden_methods1) @pytest.mark.parametrize( "meth", [ "__call__", "__repr__", "__str__", "__new__", "__copy__", "__eq__", "_validate_data_type", "as_completed", "_get_citations", ], ) def test_forbidden_methods_non_composable_app(meth): class app_forbidden_methods2: def __init__(self, a): self.a = a def main(self, val: int) -> int: return val def function1(): pass setattr(app_forbidden_methods2, meth, function1) with pytest.raises(TypeError): define_app(app_type=NON_COMPOSABLE)(app_forbidden_methods2) def test_forbidden_input_attribute(): """user-defined input attribute (non-function) is rejected on composable apps""" class app_with_input_attr: input = "some_value" def main(self, val: int) -> int: return val with pytest.raises(TypeError): define_app(app_with_input_attr) @pytest.mark.parametrize( "meth", [ "__call__", "__repr__", "__str__", "__new__", "__copy__", "__eq__", "__add__", "_validate_data_type", "as_completed", "_get_citations", "disconnect", ], ) def test_forbidden_methods_subclass_composable(meth): def forbidden(): pass with pytest.raises(TypeError): type("bad_app", (ComposableApp,), {"main": _typed_main, meth: forbidden}) @pytest.mark.parametrize( "meth", [ "__call__", "__repr__", "__str__", "__new__", "__copy__", "__eq__", "_validate_data_type", "as_completed", "_get_citations", ], ) def test_forbidden_methods_subclass_non_composable(meth): def forbidden(): pass with pytest.raises(TypeError): type("bad_app", (NonComposableApp,), {"main": _typed_main, meth: forbidden}) @pytest.mark.parametrize( "meth", ["apply_to", "set_logger"], ) def test_forbidden_methods_subclass_writer(meth): def forbidden(): pass with pytest.raises(TypeError): type("bad_app", (WriterApp,), {"main": _typed_main, meth: forbidden}) def test_forbidden_input_attribute_subclass(): with pytest.raises(TypeError): type( "bad_app", (ComposableApp,), {"main": _typed_main, "input": "some_value"}, ) @pytest.mark.parametrize( "prop", ["check_data_type", "citations", "bib"], ) def test_forbidden_properties_subclass(prop): with pytest.raises(TypeError): type( "bad_app", (ComposableApp,), {"main": _typed_main, prop: property(lambda self: None)}, ) @pytest.mark.parametrize( "prop", ["check_data_type", "citations", "bib"], ) def test_forbidden_properties_define_app(prop): class my_app: def main(self, val: int) -> int: return val setattr(my_app, prop, property(lambda self: None)) with pytest.raises(TypeError): define_app(my_app) def test_skip_not_completed(): @define_app(skip_not_completed=False) def takes_not_completed(val: snx_types.SerialisableType) -> dict: return val.to_rich_dict() app = takes_not_completed() nc = NotCompleted(NotCompletedType.ERROR, "test", "for tracing", source="blah") got = app(nc) assert isinstance(got, dict) assert got == nc.to_rich_dict() def test_copies_doc_from_func(): @define_app def delme(val: snx_types.SerialisableType) -> dict: """my docstring""" return val.to_rich_dict() assert delme.__doc__ == "my docstring" @define_app def delme2(val: snx_types.SerialisableType) -> dict: """my docstring Notes ----- body """ return val.to_rich_dict() assert delme2.__doc__ == "my docstring" assert delme2.__init__.__doc__.split() == ["Notes", "-----", "body"] def test_bad_wrap(): def foo(a: "str") -> int: return int(a) with pytest.raises(NotImplementedError): define_app(foo) def bar(a: str) -> "int": return int(a) with pytest.raises(NotImplementedError): define_app(bar) def _make_cite(**kwargs): defaults = { "author": ["Doe, J"], "title": "test", "year": 2024, "url": "https://example.com", "version": "1.0", "license": "MIT", "doi": "10.0/test", "publisher": "test", } defaults.update(kwargs) return Software(**defaults) def test_single_app_with_citation(): cite = _make_cite() @define_app(cite=cite) class cited_app: def main(self, val: int) -> int: return val app = cited_app() assert app._cite is cite assert app.citations == (cite,) def test_single_app_without_citation(): @define_app class uncited_app: def main(self, val: int) -> int: return val app = uncited_app() assert app._cite is None assert app.citations == () def test_composed_apps_all_with_citations(): cite_a = _make_cite(title="A") cite_b = _make_cite(title="B") @define_app(cite=cite_a) class app_a: def main(self, val: int) -> int: return val @define_app(cite=cite_b) class app_b: def main(self, val: int) -> int: return val composed = app_a() + app_b() assert composed.citations == (cite_b, cite_a) def test_composed_apps_some_with_citations(): cite_a = _make_cite(title="A") @define_app(cite=cite_a) class app_c: def main(self, val: int) -> int: return val @define_app class app_d: def main(self, val: int) -> int: return val composed = app_c() + app_d() assert composed.citations == (cite_a,) def test_composed_apps_none_with_citations(): @define_app class app_e: def main(self, val: int) -> int: return val @define_app class app_f: def main(self, val: int) -> int: return val composed = app_e() + app_f() assert composed.citations == () def test_composed_three_app_chain(): cite_l = _make_cite(title="L") cite_g = _make_cite(title="G") cite_w = _make_cite(title="W") @define_app(cite=cite_l) class chain_l: def main(self, val: int) -> int: return val @define_app(cite=cite_g) class chain_g: def main(self, val: int) -> int: return val @define_app(cite=cite_w) class chain_w: def main(self, val: int) -> int: return val composed = chain_l() + chain_g() + chain_w() assert composed.citations == (cite_w, cite_g, cite_l) def test_duplicate_citation_deduplication(): cite = _make_cite() @define_app(cite=cite) class dup_a: def main(self, val: int) -> int: return val @define_app(cite=cite) class dup_b: def main(self, val: int) -> int: return val composed = dup_a() + dup_b() assert composed.citations == (cite,) def test_non_composable_app_with_citation(): cite = _make_cite() @define_app(app_type=NON_COMPOSABLE, cite=cite) class nc_cited: def main(self, val: int) -> int: return val app = nc_cited() assert app.citations == (cite,) def test_cite_sets_app_attribute(): cite = _make_cite() @define_app(cite=cite) class my_special_app: def main(self, val: int) -> int: return val app = my_special_app() assert app.citations[0].app == "my_special_app" def test_cite_shared_across_apps(): cite = _make_cite() @define_app(cite=cite) class app_one: def main(self, val: int) -> int: return val @define_app(cite=cite) class app_two: def main(self, val: int) -> int: return val a = app_one() b = app_two() assert a.citations[0].app == "app_one" assert b.citations[0].app == "app_two" def test_bib_single_app_with_citation(): cite = _make_cite() @define_app(cite=cite) class bib_app: def main(self, val: int) -> int: return val app = bib_app() assert app.bib == str(cite) def test_bib_app_without_citation(): @define_app class no_bib_app: def main(self, val: int) -> int: return val app = no_bib_app() assert app.bib == "" def test_bib_composed_apps(): cite_a = _make_cite(title="A") cite_b = _make_cite(title="B") @define_app(cite=cite_a) class bib_a: def main(self, val: int) -> int: return val @define_app(cite=cite_b) class bib_b: def main(self, val: int) -> int: return val composed = bib_a() + bib_b() assert str(cite_b) in composed.bib assert str(cite_a) in composed.bib assert "\n\n" in composed.bib def test_app_copy(): """shallow copy creates new instance sharing attribute references""" @define_app class app_copy_test: def __init__(self, data): self.data = data def main(self, val: int) -> int: return val original = app_copy_test([1, 2, 3]) copied = copy(original) assert copied is not original assert copied.data is original.data assert copied._init_vals is original._init_vals def test_composition_does_not_mutate_originals(): """composition uses copies so originals stay independent""" @define_app class app_mut_a: def main(self, val: int) -> int: return val @define_app class app_mut_b: def main(self, val: int) -> int: return val a = app_mut_a() b = app_mut_b() _ = a + b assert a.input is None assert b.input is None def test_app_reuse_in_multiple_compositions(): """same app instance can be used in multiple compositions""" @define_app class app_reuse_a: def main(self, val: int) -> int: return val @define_app class app_reuse_b: def main(self, val: int) -> int: return val @define_app class app_reuse_c: def main(self, val: int) -> int: return val a = app_reuse_a() b = app_reuse_b() c = app_reuse_c() comb1 = a + b comb2 = a + c comb3 = c + b assert comb1(1) == 1 assert comb2(1) == 1 assert comb3(1) == 1 def test_app_eq(): """__eq__ checks attribute identity""" @define_app class app_eq_test: def __init__(self, x): self.x = x def main(self, val: int) -> int: return val a = app_eq_test(1) b = copy(a) assert a == b c = app_eq_test(1) assert a != c # different instances with different attribute objects assert a != "not an app" def test_check_data_type_default_true(): @define_app class app_cdt: def main(self, val: int) -> int: return val assert app_cdt().check_data_type is True def test_check_data_type_true_rejects_bad_type(): @define_app class app_cdt: def main(self, val: int) -> int: return val app = app_cdt() app.check_data_type = True got = app("not_an_int") assert isinstance(got, NotCompleted) def test_check_data_type_false_skips_validation(): @define_app class app_cdt: def main(self, val: int) -> int: return val app = app_cdt() app.check_data_type = False got = app("42") assert got == "42" def test_check_data_type_propagates_in_composition(): @define_app class app_cdt_a: def main(self, val: int) -> int: return val @define_app class app_cdt_b: def main(self, val: int) -> int: return val @define_app class app_cdt_c: def main(self, val: int) -> int: return val composed = app_cdt_a() + app_cdt_b() + app_cdt_c() assert composed.check_data_type is True assert composed.input.check_data_type is True assert composed.input.input.check_data_type is True composed.check_data_type = False assert composed.check_data_type is False assert composed.input.check_data_type is False assert composed.input.input.check_data_type is False def test_check_data_type_does_not_affect_originals(): @define_app class app_cdt_a: def main(self, val: int) -> int: return val @define_app class app_cdt_b: def main(self, val: int) -> int: return val a = app_cdt_a() b = app_cdt_b() composed = a + b composed.check_data_type = False assert a.check_data_type is True assert b.check_data_type is True def test_check_data_type_false_composed_end_to_end(): @define_app class app_cdt_a: def main(self, val: int) -> int: return val @define_app class app_cdt_b: def main(self, val: int) -> int: return val composed = app_cdt_a() + app_cdt_b() composed.check_data_type = False got = composed("not_an_int") assert got == "not_an_int" def test_check_data_type_re_enable(): @define_app class app_cdt: def main(self, val: int) -> int: return val app = app_cdt() app.check_data_type = False assert app("not_int") == "not_int" app.check_data_type = True got = app("not_int") assert isinstance(got, NotCompleted) def test_make_logfile_name(): @define_app class logname_app: def main(self, val: int) -> int: return val app = logname_app() name = _make_logfile_name(app) assert name.startswith("logname_app") assert name.endswith(".log") def test_not_completed_repr(): nc = NotCompleted(NotCompletedType.ERROR, "origin", "msg", source="src") r = repr(nc) assert "ERROR" in r assert "origin" in r assert "msg" in r def test_not_completed_source_exception(): """source that raises in get_data_source results in None""" nc = NotCompleted(NotCompletedType.ERROR, "test", "msg", source=42) assert nc.source is None def test_call_with_none(): @define_app class none_app: def main(self, val: int) -> int: return val app = none_app() got = app(None) assert isinstance(got, NotCompleted) assert "None" in got.message def test_validate_data_type_source_proxy(): @define_app class proxy_app: def main(self, val: int) -> int: return val app = proxy_app() sp = source_proxy(42) got = app(sp) assert got == 42 def test_add_writer_lhs_raises(): @define_app(app_type=WRITER) class writer_app: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data @define_app class generic_app: def main(self, val: int) -> int: return val ds = Mock() ds.source = "/tmp" w = writer_app(data_store=ds) g = generic_app() with pytest.raises(TypeError, match="writer"): w + g def test_add_loader_rhs_raises(): @define_app(app_type=LOADER) class loader_app: def main(self, val: str) -> int: return int(val) @define_app class generic_app: def main(self, val: int) -> int: return val g = generic_app() lo = loader_app() with pytest.raises(TypeError, match="loader"): g + lo def test_add_incompatible_types(): @define_app class str_app: def main(self, val: int) -> str: return str(val) @define_app class int_only_app: def main(self, val: int) -> int: return val with pytest.raises(TypeError, match="incompatible"): str_app() + int_only_app() def test_is_app_composable_true(): @define_app class comp_app: def main(self, val: int) -> int: return val assert is_app_composable(comp_app) def test_is_app_composable_false_non_composable(): @define_app(app_type=NON_COMPOSABLE) class nc_app: def main(self, val: int) -> int: return val assert not is_app_composable(nc_app) def test_is_app_composable_false_not_app(): assert not is_app_composable("not an app") def test_define_app_on_non_class(): with pytest.raises(ValueError, match="not a class"): define_app(42) def test_source_proxy_basic(): obj = [1, 2, 3] sp = source_proxy(obj) assert sp.obj is obj assert sp.source is obj assert isinstance(sp.uuid, str) def test_source_proxy_set_obj(): sp = source_proxy([1, 2]) sp.set_obj([3, 4]) assert sp.obj == [3, 4] assert sp.source == [1, 2] def test_source_proxy_source_setter(): sp = source_proxy("original") # property setter must be called directly because __setattr__ intercepts type(sp).source.fset(sp, "new_source") assert sp.source == "new_source" def test_source_proxy_getattr(): sp = source_proxy([1, 2, 3]) assert sp.count(1) == 1 def test_source_proxy_setattr(): obj = Mock() sp = source_proxy(obj) sp.value = 42 assert obj.value == 42 def test_source_proxy_bool(): assert bool(source_proxy([1])) assert not bool(source_proxy([])) def test_source_proxy_repr_str(): sp = source_proxy([1, 2]) assert repr(sp) == repr([1, 2]) assert str(sp) == str([1, 2]) def test_source_proxy_eq(): sp = source_proxy(42) assert sp == 42 assert sp != 43 def test_source_proxy_len(): sp = source_proxy([1, 2, 3]) assert len(sp) == 3 def test_source_proxy_pickle(): sp = source_proxy("hello") restored = pickle.loads(pickle.dumps(sp)) assert restored.obj == "hello" assert restored.source == "hello" def test_proxy_input_with_source(): """an input knowing its own origin is still proxied, the result may not""" item = Mock() item.source = "test" item.__bool__ = lambda self: True result = _proxy_input([item]) assert len(result) == 1 assert isinstance(result[0], source_proxy) assert result[0].source is item def test_proxy_input_without_source(): result = _proxy_input(["a", "b"]) assert len(result) == 2 assert all(isinstance(r, source_proxy) for r in result) def test_proxy_input_skips_falsy(): result = _proxy_input([0, "", "valid"]) assert len(result) == 1 def test_propagate_source_non_proxy(): @define_app class prop_app: def main(self, val: int) -> int: return val * 2 app = prop_app() ps = propagate_source(app, get_unique_id) got = ps(4) assert got == 8 def test_propagate_source_proxy_with_source(): @define_app class prop_app2: def main(self, val: str) -> str: return val.upper() app = prop_app2() class HasSource: def __init__(self, v, src): self.value = v self.source = src def upper(self): return HasSource(self.value.upper(), self.source) obj = HasSource("hello", "my_source") sp = source_proxy(obj) ps = propagate_source(app, get_unique_id) got = ps(sp) # result has source via get_unique_id, so returned directly assert not isinstance(got, source_proxy) or got.obj is not obj def test_propagate_source_proxy_no_source(): @define_app class prop_app3: def main(self, val: int) -> int: return val + 1 app = prop_app3() sp = source_proxy(10) ps = propagate_source(app, get_unique_id) got = ps(sp) assert isinstance(got, source_proxy) assert got.obj == 11 def test_as_completed_serial(tmp_path): @define_app class ac_app: def main(self, val: str) -> str: return val.upper() app = ac_app() results = list(app.as_completed(["a", "b", "c"], show_progress=False)) objs = [r.obj if isinstance(r, source_proxy) else r for r in results] assert "A" in objs assert len(objs) == 3 def test_as_completed_empty(): @define_app class ac_empty: def main(self, val: int) -> int: return val app = ac_empty() results = list(app.as_completed([], show_progress=False)) assert results == [] def test_as_completed_string_input(): @define_app class ac_str: def main(self, val: str) -> str: return val app = ac_str() results = list(app.as_completed("hello", show_progress=False)) assert len(results) == 1 def test_as_completed_with_progress_instance(): from scinexus.progress import NoProgress @define_app class ac_prog: def main(self, val: str) -> str: return val.upper() app = ac_prog() results = list(app.as_completed(["a", "b"], show_progress=NoProgress())) objs = [r.obj if isinstance(r, source_proxy) else r for r in results] assert len(objs) == 2 assert "A" in objs def test_as_completed_with_tqdm_progress(): from scinexus.progress import TqdmProgress @define_app class ac_tqdm: def main(self, val: int) -> int: return val * 2 app = ac_tqdm() results = list( app.as_completed([1, 2, 3], show_progress=TqdmProgress(disable=True)) ) objs = [r.obj if isinstance(r, source_proxy) else r for r in results] assert len(objs) == 3 def test_writer_set_logger_default(tmp_path): @define_app(app_type=WRITER) class w_logger: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data ds = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = w_logger(data_store=ds) app.set_logger() assert app.logger is not None app.logger.shutdown() def test_writer_set_logger_false(tmp_path): @define_app(app_type=WRITER) class w_logger2: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data ds = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = w_logger2(data_store=ds) app.set_logger(logger=False) assert app.logger is None def test_writer_set_logger_invalid_type(tmp_path): @define_app(app_type=WRITER) class w_logger3: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data ds = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = w_logger3(data_store=ds) with pytest.raises(TypeError, match="CachingLogger"): app.set_logger(logger="not a logger") def test_writer_apply_to(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) result = process.apply_to(dstore, logger=False, show_progress=False) assert len(result) == 3 def test_writerapp_subclass_apply_to_writes_once(tmp_path) -> None: """An inheritance-defined writer runs main() exactly once per input. Regression test for the original bug where ``class X(WriterApp)`` ended up with ``app_type=GENERIC``, causing ``apply_to`` to feed the writer's own output back into ``main`` on a second pass. """ src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") call_count: list[str] = [] @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() class inherited_writer(WriterApp): def __init__(self, data_store: DataStoreDirectory) -> None: self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: call_count.append(identifier) return self.data_store.write(unique_id=identifier, data=data) assert inherited_writer.app_type is WRITER process = reader() + inherited_writer(data_store=out_dstore) result = process.apply_to(dstore, logger=False, show_progress=False) assert len(result) == 3 assert len(call_count) == 3 def test_apply_to_uses_registered_id_from_source( tmp_path, reset_id_from_source: None, ) -> None: """`apply_to` consults the globally registered ID extractor by default.""" src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") seen_ids: list[str | None] = [] def custom(obj: object) -> str | None: result = get_unique_id(obj) seen_ids.append(result) return result set_id_from_source(custom) @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store: DataStoreDirectory) -> None: self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) process.apply_to(dstore, logger=False, show_progress=False) # `apply_to`'s input-id loop calls `custom` directly with input paths, # so the seen IDs include the input filenames (suffix-stripped). assert seen_ids assert any(sid is not None and sid.startswith("item_") for sid in seen_ids) def test_apply_to_explicit_id_from_source_overrides_registered( tmp_path, reset_id_from_source: None, ) -> None: """An explicit ``id_from_source=`` argument wins over the registered func.""" registered_calls: list[object] = [] explicit_calls: list[object] = [] def registered(obj: object) -> str | None: registered_calls.append(obj) return get_unique_id(obj) def explicit(obj: object) -> str | None: explicit_calls.append(obj) return get_unique_id(obj) set_id_from_source(registered) src = tmp_path / "src" src.mkdir() (src / "a.txt").write_text("data") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store: DataStoreDirectory) -> None: self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) process.apply_to( dstore, id_from_source=explicit, logger=False, show_progress=False, ) assert explicit_calls, "explicit override should have been called" assert not registered_calls, ( "registered func must not be called when explicit override is supplied" ) def test_not_completed_uses_registered_id_from_source( reset_id_from_source: None, ) -> None: """`NotCompleted` normalises its source via the registered extractor.""" class WithSource: source = "path/to/file.fasta" def custom(obj: object) -> str | None: return f"custom::{obj!r}" set_id_from_source(custom) nc = NotCompleted(NotCompletedType.ERROR, "test", "msg", source=WithSource()) assert nc.source is not None assert nc.source.startswith("custom::") def test_not_completed_default_source_is_unique_id( reset_id_from_source: None, ) -> None: """Without a registration, `NotCompleted.source` is the unique ID form. Locks in the behaviour change: the default extractor strips file-format suffixes (``seqs.fasta`` → ``seqs``), where the prior implementation (calling ``get_data_source`` directly) returned the un-stripped form. """ class WithSource: source = "path/to/seqs.fasta" nc = NotCompleted(NotCompletedType.ERROR, "test", "msg", source=WithSource()) assert nc.source == "seqs" def test_get_main_hints_no_main(): from scinexus.composable import _get_main_hints class NoMain: pass with pytest.raises(ValueError, match="main"): _get_main_hints(NoMain) def test_source_proxy_hash(): sp = source_proxy("hello") assert isinstance(hash(sp), int) sp2 = source_proxy("hello") assert hash(sp) != hash(sp2) def test_init_subclass_slots(): with pytest.raises(NotImplementedError, match="slots"): class BadApp(ComposableApp[int, int]): __slots__ = ("x",) def main(self, val: int) -> int: return val def test_writerapp_subclass_implies_writer() -> None: """Inheriting WriterApp yields a WRITER without any class kwarg.""" class my_writer(WriterApp): def __init__(self, data_store: DataStoreDirectory) -> None: self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data assert my_writer.app_type is WRITER def test_loaderapp_subclass_implies_loader() -> None: """Inheriting LoaderApp yields a LOADER and is not stripped of input.""" class my_loader(LoaderApp): def main(self, path: str) -> str: return path assert my_loader.app_type is LOADER # _init_subclass_setup only clears `cls.input` for non-LOADER apps. assert "input" not in my_loader.__dict__ def test_noncomposableapp_subclass_implies_non_composable() -> None: """Inheriting NonComposableApp yields a NON_COMPOSABLE.""" class my_app(NonComposableApp[int, int]): def main(self, val: int) -> int: return val * 2 assert my_app.app_type is NON_COMPOSABLE def test_explicit_composableapp_subclass_is_generic() -> None: """Direct ComposableApp subclasses still resolve to GENERIC.""" class my_app(ComposableApp[int, int]): def main(self, val: int) -> int: return val assert my_app.app_type is GENERIC def test_validate_data_type_not_completed_skip_true(): @define_app class skip_app: def main(self, val: int) -> int: return val app = skip_app() nc = NotCompleted(NotCompletedType.ERROR, "test", "msg") got = app._validate_data_type(nc) assert isinstance(got, NotCompleted) def test_as_completed_with_datastore(tmp_path): src = tmp_path / "src" src.mkdir() for i in range(2): (src / f"f_{i}.txt").write_text(f"content {i}") dstore = DataStoreDirectory(src, suffix="txt") @define_app class read_app: def main(self, val: str) -> str: return val app = read_app() results = list(app.as_completed(dstore, show_progress=False)) assert len(results) == 2 def test_as_completed_parallel(): app = func2app(exponent=2) results = list(app.as_completed([1, 2, 3], parallel=True, show_progress=False)) objs = [r.obj if isinstance(r, source_proxy) else r for r in results] assert sorted(objs) == [1, 4, 9] def test_writer_apply_to_no_input(tmp_path): @define_app(app_type=WRITER) class lone_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> int: return data ds = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = lone_writer(data_store=ds) with pytest.raises(RuntimeError, match="no composed input"): app.apply_to(["something"], logger=False) def test_apply_to_empty_dstore(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "empty" src.mkdir() dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) with pytest.raises(ValueError, match="empty"): process.apply_to(dstore, logger=False, show_progress=False) def test_apply_to_skip_existing(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) process.apply_to(dstore, logger=False, show_progress=False) assert len(out_dstore) == 3 # run again — existing items should be skipped result = process.apply_to(dstore, logger=False, show_progress=False) assert len(result) == 3 def test_apply_to_input_with_own_source_keeps_identity(tmp_path): """an input carrying .source is named after itself, not after the result""" class Record: def __init__(self, payload: str, source: str) -> None: self.payload = payload self.source = source out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: Record) -> str: return val.payload @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) process.apply_to( [Record("hello", "in_0.txt"), Record("world", "in_1.txt")], logger=False, show_progress=False, ) assert sorted(m.unique_id for m in out_dstore.completed) == [ "in_0.txt", "in_1.txt", ] def test_apply_to_cogent3_inputs_keep_identity(tmp_path): """loaded alignments carry .source, which a str result must not displace""" src = tmp_path / "src" src.mkdir() for name, second in (("aln_0", "ACGA"), ("aln_1", "ACGG")): (src / f"{name}.fasta").write_text(f">a\nACGT\n>b\n{second}\n") loader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") alignments = [loader(m) for m in DataStoreDirectory(src, suffix="fasta").completed] # without this the inputs would be proxied for the mundane reason that # they have no source, and the test would pass against the old behaviour assert all(a.source for a in alignments) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="fasta") @define_app class to_text: def main(self, aln: c3types.AlignedSeqsType) -> str: return aln.to_fasta() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) (to_text() + writer(data_store=out_dstore)).apply_to( alignments, logger=False, show_progress=False ) assert sorted(m.unique_id for m in out_dstore.completed) == [ "aln_0.fasta", "aln_1.fasta", ] def test_apply_to_str_pipeline_uses_input_identifiers(tmp_path): """output ids come from the input, not from the str the pipeline returns""" src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class shout: def main(self, text: str) -> str: return text.upper() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + shout() + writer(data_store=out_dstore) result = process.apply_to(dstore, logger=False, show_progress=False) assert sorted(m.unique_id for m in result.completed) == [ "item_0.txt", "item_1.txt", "item_2.txt", ] def test_apply_to_append_mode_rerun_skips_completed(tmp_path): """a rerun against an append-mode store skips records already written""" src = tmp_path / "src" src.mkdir() for i in range(2): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) out = tmp_path / "out" first = DataStoreDirectory(out, mode=Mode.w, suffix="txt") (reader() + writer(data_store=first)).apply_to( dstore, logger=False, show_progress=False ) second = DataStoreDirectory(out, mode=Mode.a, suffix="txt") result = (reader() + writer(data_store=second)).apply_to( dstore, logger=False, show_progress=False ) assert sorted(m.unique_id for m in result.completed) == [ "item_0.txt", "item_1.txt", ] def test_not_completed_without_source_gets_input_identity(tmp_path): """a NotCompleted carrying no source is attributed to the input""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class reject: def main(self, text: str) -> str: return NotCompleted("FAIL", self, "no good") got = list((reader() + reject()).as_completed(dstore)) assert len(got) == 1 assert got[0].source == "item_0" construction = got[0].to_rich_dict()["not_completed_construction"] assert construction["kwargs"]["source"] == "item_0" def test_raised_failure_attributed_to_input(tmp_path): """an exception past the head of a pipeline names the input, not the data""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class boom: def main(self, text: str) -> str: msg = "no good" raise ValueError(msg) got = list((reader() + boom()).as_completed(dstore)) assert len(got) == 1 assert got[0].source == "item_0" def test_type_failure_past_head_attributed_to_input(tmp_path): """a runtime type mismatch past the head names the input, not the data""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str | int: return val.read() @define_app class wants_int: def main(self, n: int) -> int: return n + 1 got = list((reader() + wants_int()).as_completed(dstore)) assert len(got) == 1 assert got[0].source == "item_0" def test_apply_to_files_raised_failure_under_input_id(tmp_path): """a raised failure is stored against the input identifier""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class boom: def main(self, text: str) -> str: msg = "no good" raise ValueError(msg) @define_app(app_type=WRITER, skip_not_completed=False) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember | None: if isinstance(data, NotCompleted): return self.data_store.write_not_completed( unique_id=identifier, data=data.to_json() ) return self.data_store.write(unique_id=identifier, data=data) process = reader() + boom() + writer(data_store=out_dstore) result = process.apply_to(dstore, logger=False, show_progress=False) assert [m.unique_id for m in result.not_completed] == ["not_completed/item_0.json"] def test_direct_call_failure_attributed_to_input(tmp_path): """a failure names the input even with no source_proxy in the path""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class boom: def main(self, text: str) -> str: msg = "no good" raise ValueError(msg) app = reader() + boom() assert app(next(iter(dstore))).source == "item_0" def test_apply_to_failure_on_unproxied_input(tmp_path): """inputs already carrying .source are never proxied, so the identity must be taken before the pipeline replaces it""" class Rec: def __init__(self, text: str, source: str) -> None: self.text = text self.source = source inputs = [Rec("payload one", "one.txt")] out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app class unpack: def main(self, rec: Rec) -> str: return rec.text @define_app class boom: def main(self, text: str) -> str: msg = "no good" raise ValueError(msg) @define_app(app_type=WRITER, skip_not_completed=False) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember | None: if isinstance(data, NotCompleted): return self.data_store.write_not_completed( unique_id=identifier, data=data.to_json() ) return self.data_store.write(unique_id=identifier, data=data) process = unpack() + boom() + writer(data_store=out_dstore) result = process.apply_to(inputs, logger=False, show_progress=False) assert [m.unique_id for m in result.not_completed] == ["not_completed/one.json"] def test_writer_main_without_identifier_is_left_alone(tmp_path): """a writer that names records itself is not handed an identifier""" out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=WRITER) class fixed_name_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str) -> DataMember: return self.data_store.write(unique_id="fixed", data=data) assert fixed_name_writer._main_takes_identifier is False fixed_name_writer(data_store=out_dstore)("payload") assert [m.unique_id for m in out_dstore.completed] == ["fixed.txt"] def test_writer_skip_not_completed_forced_off(): """apply_to drives a writer's main directly, so the flag cannot be honoured""" @define_app(app_type=WRITER) class default_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) @define_app(app_type=WRITER, skip_not_completed=True) class explicit_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) assert default_writer._skip_not_completed is False assert explicit_writer._skip_not_completed is False def test_writer_receives_not_completed_on_direct_call(tmp_path): """a writer sees NotCompleted whichever way the pipeline is invoked""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") seen = [] @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class reject: def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember | None: seen.append(type(data).__name__) if isinstance(data, NotCompleted): return None return self.data_store.write(unique_id=identifier, data=data) app = reader() + reject() + writer(data_store=out_dstore) app(next(iter(dstore))) assert seen == ["NotCompleted"] def test_writer_direct_call_matches_apply_to(tmp_path): """a one-off call names the record the way apply_to would""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class shout: def main(self, text: str) -> str: return text.upper() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) direct = DataStoreDirectory(tmp_path / "direct", mode=Mode.w, suffix="txt") (reader() + shout() + writer(data_store=direct))(next(iter(dstore))) batched = DataStoreDirectory(tmp_path / "batched", mode=Mode.w, suffix="txt") (reader() + shout() + writer(data_store=batched)).apply_to( dstore, logger=False, show_progress=False ) assert [m.unique_id for m in direct.completed] == ["item_0.txt"] assert [m.unique_id for m in direct.completed] == [ m.unique_id for m in batched.completed ] def test_writer_direct_call_failure_gets_identifier(tmp_path): """a NotCompleted reaching a writer directly is named, not left blank""" src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") seen = [] @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app class reject: def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember | None: seen.append(identifier) return None app = reader() + reject() + writer(data_store=out_dstore) app(next(iter(dstore))) assert seen == ["item_0"] def test_writer_direct_call_without_identity_raises(tmp_path): """no identifier and nothing to derive one from is a programming error""" out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=WRITER) class lone_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=str(data)) app = lone_writer(data_store=out_dstore) with pytest.raises(ValueError, match="lone_writer"): app(42) def test_writer_direct_call_explicit_identifier_wins(tmp_path): """a caller supplied identifier is not overridden""" out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=WRITER) class lone_writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: int, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=str(data)) app = lone_writer(data_store=out_dstore) app(42, identifier="by_keyword") app(7, "by_position") assert sorted(m.unique_id for m in out_dstore.completed) == [ "by_keyword.txt", "by_position.txt", ] def _prefixed(prefix: str): def extract(obj: object) -> str | None: unique_id = get_unique_id(obj) return None if unique_id is None else f"{prefix}_{unique_id}" return extract def _one_member_store(tmp_path): src = tmp_path / "src" src.mkdir() (src / "item_0.txt").write_text("data 0") return DataStoreDirectory(src, suffix="txt") @define_app(app_type=LOADER) class _reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class _named_writer: """a writer that names records itself, as cogent3's writers do""" def __init__(self, data_store, id_from_source=get_unique_id): self.data_store = data_store self._id_from_source = id_from_source def main(self, data: str, identifier: str = "") -> DataMember: identifier = identifier or self._id_from_source(data) return self.data_store.write(unique_id=identifier, data=data) def test_writer_supplied_id_from_source_names_the_record(tmp_path): """the extractor a writer was built with wins over the registered default""" dstore = _one_member_store(tmp_path) direct = DataStoreDirectory(tmp_path / "direct", mode=Mode.w, suffix="txt") (_reader() + _named_writer(direct, id_from_source=_prefixed("pre")))( next(iter(dstore)) ) batched = DataStoreDirectory(tmp_path / "batched", mode=Mode.w, suffix="txt") (_reader() + _named_writer(batched, id_from_source=_prefixed("pre"))).apply_to( dstore, logger=False, show_progress=False ) assert [m.unique_id for m in direct.completed] == ["pre_item_0.txt"] assert [m.unique_id for m in batched.completed] == ["pre_item_0.txt"] def test_writer_supplied_id_from_source_names_a_failure(tmp_path): """a NotCompleted record is named the same way as a completed one""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") seen: list[str] = [] @define_app class reject: def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") @define_app(app_type=WRITER) class recording_writer: def __init__(self, data_store, id_from_source=get_unique_id): self.data_store = data_store self._id_from_source = id_from_source def main(self, data: str, identifier: str = "") -> DataMember | None: seen.append(identifier or self._id_from_source(data)) return None app = ( _reader() + reject() + recording_writer(out_dstore, id_from_source=_prefixed("pre")) ) app(next(iter(dstore))) assert seen == ["pre_item_0"] def test_writer_default_id_from_source_does_not_beat_registered( tmp_path, reset_id_from_source: None, ) -> None: """an extractor left at its default must not demote set_id_from_source""" set_id_from_source(_prefixed("glob")) dstore = _one_member_store(tmp_path) direct = DataStoreDirectory(tmp_path / "direct", mode=Mode.w, suffix="txt") (_reader() + _named_writer(direct))(next(iter(dstore))) batched = DataStoreDirectory(tmp_path / "batched", mode=Mode.w, suffix="txt") (_reader() + _named_writer(batched)).apply_to( dstore, logger=False, show_progress=False ) assert [m.unique_id for m in direct.completed] == ["glob_item_0.txt"] assert [m.unique_id for m in batched.completed] == ["glob_item_0.txt"] def test_apply_to_explicit_id_from_source_beats_supplied(tmp_path): """an explicit apply_to argument outranks the writer's own extractor""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") (_reader() + _named_writer(out_dstore, id_from_source=_prefixed("pre"))).apply_to( dstore, id_from_source=_prefixed("arg"), logger=False, show_progress=False, ) assert [m.unique_id for m in out_dstore.completed] == ["arg_item_0.txt"] def test_as_completed_uses_supplied_id_from_source(tmp_path): """as_completed attributes a failure with the writer's own extractor""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app class reject: def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") app = ( _reader() + reject() + _named_writer(out_dstore, id_from_source=_prefixed("pre")) ) got = list(app.as_completed(dstore, show_progress=False)) assert [r.source for r in got] == ["pre_item_0"] def test_as_completed_ignores_id_from_source_on_a_non_writer(tmp_path): """id_from_source means whatever its author chose on a non-writer app""" dstore = _one_member_store(tmp_path) seen: list[object] = [] @define_app class rejecting: def __init__(self, id_from_source=None): self.keep = id_from_source def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") def unrelated(obj: object) -> str: seen.append(obj) return "wrong" app = _reader() + rejecting(id_from_source=unrelated) got = list(app.as_completed(dstore, show_progress=False)) assert not seen assert [r.source for r in got] == ["item_0"] def test_writer_explicit_identifier_beats_supplied_id_from_source(tmp_path): """a caller supplied identifier outranks the writer's own extractor""" out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = _named_writer(out_dstore, id_from_source=_prefixed("pre")) app("payload", identifier="explicit") assert [m.unique_id for m in out_dstore.completed] == ["explicit.txt"] @define_app class _reject: """fails every input, so as_completed yields a NotCompleted to inspect""" def main(self, text: str) -> str: return NotCompleted("FAIL", self, "not wanted") @define_app(app_type=WRITER) class _tolerant_writer: """writes whatever it is handed, so a failing pipeline can still be applied""" def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=str(data)) def _failing_pipeline(tmp_path): out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") return _reader() + _reject() + _tolerant_writer(out_dstore) def test_as_completed_explicit_id_from_source_beats_a_stored_one(tmp_path): """an explicit argument is not overruled by what an earlier apply_to stored""" dstore = _one_member_store(tmp_path) app = _failing_pipeline(tmp_path) app.apply_to( dstore, id_from_source=_prefixed("first"), logger=False, show_progress=False ) got = list( app.as_completed( dstore, id_from_source=_prefixed("second"), show_progress=False ) ) assert [r.source for r in got] == ["second_item_0"] def test_apply_to_id_from_source_holds_for_later_calls(tmp_path): """the argument is a setting on the app, not a one-off""" dstore = _one_member_store(tmp_path) app = _failing_pipeline(tmp_path) app.apply_to( dstore, id_from_source=_prefixed("first"), logger=False, show_progress=False ) got = list(app.as_completed(dstore, show_progress=False)) assert [r.source for r in got] == ["first_item_0"] def test_as_completed_id_from_source_holds_for_later_calls(tmp_path): """as_completed records its argument exactly as apply_to does""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = _reader() + _tolerant_writer(out_dstore) list( app.as_completed(dstore, id_from_source=_prefixed("first"), show_progress=False) ) app.apply_to(dstore, logger=False, show_progress=False) assert [m.unique_id for m in out_dstore.completed] == ["first_item_0.txt"] def test_as_completed_records_id_from_source_before_iteration(tmp_path): """the setting is stored when as_completed is called, not when it is consumed""" dstore = _one_member_store(tmp_path) app = _failing_pipeline(tmp_path) app.as_completed(dstore, id_from_source=_prefixed("first"), show_progress=False) got = list(app.as_completed(dstore, show_progress=False)) assert [r.source for r in got] == ["first_item_0"] def test_as_completed_id_from_source_applies_to_a_non_writer(tmp_path): """unlike a constructor argument, this one is the framework's on any app type""" dstore = _one_member_store(tmp_path) app = _reader() + _reject() list( app.as_completed(dstore, id_from_source=_prefixed("first"), show_progress=False) ) got = list(app.as_completed(dstore, show_progress=False)) assert [r.source for r in got] == ["first_item_0"] def test_recomposing_an_applied_writer_still_runs(tmp_path): """composition copies the writer, so nothing stale may survive into the copy""" dstore = _one_member_store(tmp_path) app = _failing_pipeline(tmp_path) app.apply_to(dstore, logger=False, show_progress=False) recomposed = _reader() + _reject() + app got = list(recomposed.as_completed(dstore, show_progress=False)) assert [r.source for r in got] == ["item_0"] def test_as_completed_id_from_source_beats_the_constructor_argument(tmp_path): """the argument outranks the extractor the writer was built with""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") app = ( _reader() + _reject() + _named_writer(out_dstore, id_from_source=_prefixed("ctor")) ) got = list( app.as_completed(dstore, id_from_source=_prefixed("arg"), show_progress=False) ) assert [r.source for r in got] == ["arg_item_0"] def test_apply_to_without_input_does_not_store_id_from_source(tmp_path): """a refused apply_to must not leave the app renamed""" dstore = _one_member_store(tmp_path) out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") writer = _tolerant_writer(out_dstore) with pytest.raises(RuntimeError): writer.apply_to( dstore, id_from_source=_prefixed("first"), logger=False, show_progress=False ) got = list( (_reader() + _reject() + writer).as_completed(dstore, show_progress=False) ) assert [r.source for r in got] == ["item_0"] def test_apply_to_on_colliding_names_does_not_store_id_from_source(tmp_path): """the same holds for a refusal that only shows up part way through""" src = tmp_path / "src" src.mkdir() for i in range(2): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") app = _failing_pipeline(tmp_path) def collides(obj: object) -> str: return "same" with pytest.raises(ValueError, match="non-unique identifier"): app.apply_to(dstore, id_from_source=collides, logger=False, show_progress=False) got = list(app.as_completed(dstore, show_progress=False)) assert {r.source for r in got} == {"item_0", "item_1"} def test_apply_to_with_logging(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "src" src.mkdir() (src / "item.txt").write_text("data") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) result = process.apply_to(dstore, show_progress=False) assert len(result) == 1 # log file should have been written to the data store assert any("log" in str(m) for m in out_dstore.logs) def test_apply_to_logger_true(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "src" src.mkdir() (src / "item.txt").write_text("data") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) result = process.apply_to(dstore, logger=True, show_progress=False) assert len(result) == 1 assert any("log" in str(m) for m in out_dstore.logs) def test_apply_to_logger_false(tmp_path): from scinexus.data_store import DataMember src = tmp_path / "src" src.mkdir() (src / "item.txt").write_text("data") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) result = process.apply_to(dstore, logger=False, show_progress=False) assert len(result) == 1 assert len(list(out_dstore.logs)) == 0 assert process.logger is None def test_not_completed_to_json(): """NotCompleted.to_json returns valid JSON""" import json nc = NotCompleted(NotCompletedType.ERROR, "origin", "a message") result = json.loads(nc.to_json()) assert result["type"] == "scinexus.composable.NotCompleted" def test_deserialise_not_completed(): """roundtrip NotCompleted through JSON deserialisation""" nc = NotCompleted(NotCompletedType.ERROR, "origin", "msg") data = nc.to_rich_dict() result = deserialise_object(data) assert isinstance(result, NotCompleted) assert result.message == "msg" def test_app_main_exception_returns_not_completed(): """exception in main() returns NotCompleted instead of raising""" @define_app class raises_app: def main(self, val: int) -> int: msg = "boom" raise ValueError(msg) app = raises_app() result = app(1) assert isinstance(result, NotCompleted) assert result.type is NotCompletedType.ERROR assert "boom" in result.message def test_not_completed_source_raises(): """source that raises in get_data_source sets source to None""" class BadSource: @property def source(self): msg = "broken" raise RuntimeError(msg) nc = NotCompleted(NotCompletedType.ERROR, "origin", "msg", source=BadSource()) assert nc.source is None def test_composed_skip_not_completed_input(): """NotCompleted passed as first input to composed pipeline is returned""" @define_app class first: def main(self, val: int) -> int: return val + 1 @define_app class second: def main(self, val: int) -> int: return val * 2 composed = first() + second() nc = NotCompleted(NotCompletedType.ERROR, "test", "fail") result = composed(nc) assert isinstance(result, NotCompleted) assert result is nc def test_apply_to_string_input(tmp_path): """apply_to accepts a single string path as dstore""" src = tmp_path / "src" src.mkdir() (src / "item.txt").write_text("data") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: str) -> str: from pathlib import Path return Path(val).read_text() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) result = process.apply_to(str(src / "item.txt"), logger=False, show_progress=False) assert len(result) == 1 def test_apply_to_duplicate_id(tmp_path): """apply_to raises ValueError for duplicate identifiers""" src = tmp_path / "src" src.mkdir() for i in range(2): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out_dstore = DataStoreDirectory(tmp_path / "out", mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) with pytest.raises(ValueError, match="non-unique identifier"): process.apply_to( dstore, id_from_source=lambda _: "same_id", logger=False, show_progress=False, ) def test_apply_to_skips_existing_items(tmp_path): """apply_to skips items already in the data store""" src = tmp_path / "src" src.mkdir() for i in range(3): (src / f"item_{i}.txt").write_text(f"data {i}") dstore = DataStoreDirectory(src, suffix="txt") out = tmp_path / "out" out_dstore = DataStoreDirectory(out, mode=Mode.w, suffix="txt") @define_app(app_type=LOADER) class reader: def main(self, val: DataMember) -> str: return val.read() @define_app(app_type=WRITER) class writer: def __init__(self, data_store): self.data_store = data_store def main(self, data: str, identifier: str = "") -> DataMember: return self.data_store.write(unique_id=identifier, data=data) process = reader() + writer(data_store=out_dstore) # pre-write one item so it already exists out_dstore.write(unique_id="item_0", data="existing") process.apply_to(dstore, logger=False, show_progress=False) # item_0 skipped, items 1 and 2 written → total 3 assert len(out_dstore.completed) == 3 def test_triggers_bugcatcher(): """a composable that does not trap failures returns NotCompletedResult requesting bug report""" @define_app(app_type=LOADER) class reader: def main(self, val: str) -> str: return val.read() read = reader() read.main = lambda x: None got = read("somepath.fasta") # pylint: disable=not-callable assert isinstance(got, NotCompleted) assert got.type is NotCompletedType.BUG @pytest.mark.parametrize("func", [str, repr]) def test_user_function_repr(func): @define_app def bar(val: float, num=3) -> float: return val * num got = func(bar(num=3)) assert got == "bar(num=3)" @pytest.fixture(params=[Path, None, str]) def source_type(DATA_DIR, request): fname = "brca1.fasta" dstore = open_data_store(DATA_DIR, suffix="fasta") if request.param is not None: return request.param(DATA_DIR / fname) return next((m for m in dstore if m.unique_id == fname), None) def test_composable_unwraps_source_proxy_as_completed(source_type): app = c3.get_app("load_unaligned", format_name="fasta", moltype="dna") result = next(iter(app.as_completed([source_type], show_progress=False))) got = result.source assert got.endswith("brca1.fasta") assert not isinstance(got, source_proxy) def test_composable_unwraps_source_proxy_call(source_type): app = c3.get_app("load_unaligned", format_name="fasta", moltype="dna") result = app(source_type) got = result.source assert got.endswith("brca1.fasta") assert not isinstance(got, source_proxy) def test_as_completed(DATA_DIR): """correctly applies iteratively""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) reader = c3.get_app("load_unaligned", format_name="fasta", moltype="dna") got = list(reader.as_completed(dstore, show_progress=False)) assert len(got) == len(dstore) # should also be able to apply the results to another composable func min_length = c3.get_app("sample.min_length", 10) got = list(min_length.as_completed(got, show_progress=False)) assert len(got) == len(dstore) # should work on a chained function proc = reader + min_length got = list(proc.as_completed(dstore, show_progress=False)) assert len(got) == len(dstore) # and works on a list of just strings got = list(proc.as_completed([str(m) for m in dstore], show_progress=False)) assert len(got) == len(dstore) # or a single string path = str(Path(dstore[0].data_store.source) / dstore[0].unique_id) got = list(proc.as_completed(path, show_progress=False)) assert len(got) == 1 assert got[0].__class__.__name__.endswith("SequenceCollection") @pytest.mark.parametrize("data", [(), ("", "")]) def test_as_completed_empty_data(data): """correctly applies iteratively""" reader = c3.get_app("load_unaligned", format_name="fasta", moltype="dna") min_length = c3.get_app("sample.min_length", 10) proc = reader + min_length # returns empty input got = list(proc.as_completed(data)) assert got == [] _as_completed_w_wout_source_params = [{"a": 2}] if c3 is not None: _as_completed_w_wout_source_params.extend( [ UnionDict(a=2, source="blah.txt"), c3.make_aligned_seqs( {"a": "ACGT"}, info={"source": "blah.txt"}, moltype="dna", ), ], ) @pytest.mark.parametrize("data", _as_completed_w_wout_source_params) def test_as_completed_w_wout_source(data): from cogent3.app.typing import AlignedSeqsType @define_app def pass_through(val: dict | UnionDict | AlignedSeqsType) -> dict: return val app = pass_through() # pylint: disable=not-callable,no-value-for-parameter got = list(app.as_completed([data], show_progress=False)) assert bool(got), got @pytest.mark.parametrize("klass", [DataStoreDirectory, DataStoreSqlite]) @pytest.mark.parametrize("cast", [str, Path]) def test_apply_to_strings(DATA_DIR, tmp_dir, klass, cast): """apply_to handles non-DataMember""" dname = "test_apply_to_strings" outpath = tmp_dir / dname dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) dstore = [cast(str(m)) for m in dstore] reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) kwargs = {"suffix": "fasta"} if klass == DataStoreDirectory else {} writer = c3.get_app("write_seqs", data_store=klass(outpath, mode="w", **kwargs)) process = reader + min_length + writer # create paths as strings _ = process.apply_to(dstore, show_progress=False) assert len(process.data_store.logs) == 1 process.data_store.close() @pytest.mark.parametrize("klass", [DataStoreDirectory, DataStoreSqlite]) @pytest.mark.parametrize("cast", [str, Path]) def test_as_completed_strings(DATA_DIR, tmp_dir, klass, cast): """as_completed handles non-DataMember""" dname = "test_apply_to_strings" outpath = tmp_dir / dname dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) dstore = [cast(str(m)) for m in dstore] reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) if klass == DataStoreDirectory: writer = c3.get_app("write_seqs", klass(outpath, mode="w", suffix="fasta")) else: writer = c3.get_app("write_seqs", klass(outpath, mode="w")) orig_length = len(writer.data_store) process = reader + min_length + writer # create paths as strings got = list(process.as_completed(dstore, show_progress=False)) assert len(got) > orig_length process.data_store.close() def test_apply_to_non_unique_identifiers(tmp_dir): """should fail if non-unique names""" dstore = [ "brca1.fasta", "brca1.fasta", ] reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) outpath = tmp_dir / "test_apply_to_non_unique_identifiers" writer = c3.get_app( "write_seqs", DataStoreDirectory(outpath, mode="w", suffix="fasta"), ) process = reader + min_length + writer with pytest.raises(ValueError): process.apply_to(dstore) def test_apply_to_logging(DATA_DIR, tmp_dir): """correctly creates log file""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) process = reader + min_length + writer process.apply_to(dstore, show_progress=False) # always creates a log assert len(process.data_store.logs) == 1 out_dstore.close() def test_apply_to_logger(DATA_DIR, tmp_dir): """correctly uses user provided logger""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) LOGGER = CachingLogger() reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) process = reader + min_length + writer process.apply_to(dstore, show_progress=False, logger=LOGGER) assert len(process.data_store.logs) == 1 out_dstore.close() def test_apply_to_no_logger(DATA_DIR, tmp_dir): """correctly uses user provided logger""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) process = reader + min_length + writer process.apply_to(dstore, show_progress=False, logger=False) assert len(process.data_store.logs) == 0 assert process.logger is None out_dstore.close() @pytest.mark.parametrize("logger_val", ["somepath.log"]) def test_apply_to_invalid_logger(DATA_DIR, tmp_dir, logger_val): """incorrect logger value raises TypeError""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length = c3.get_app("min_length", 10) out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) process = reader + min_length + writer with pytest.raises(TypeError): process.apply_to(dstore, show_progress=False, logger=logger_val) out_dstore.close() @pytest.fixture def fasta_dir(DATA_DIR, tmp_dir): filenames = DATA_DIR.glob("*.fasta") fasta_dir = tmp_dir / "fasta" fasta_dir.mkdir(parents=True, exist_ok=True) for fn in filenames: dest = fasta_dir / fn.name dest.write_text(fn.read_text()) return fasta_dir @pytest.fixture def log_data(DATA_DIR): path = DATA_DIR / "scitrack.log" return path.read_text() @pytest.fixture def ro_dstore(fasta_dir): return DataStoreDirectory(fasta_dir, suffix="fasta", mode="r") @pytest.fixture def completed_objects(ro_dstore): return {f"{Path(m.unique_id).stem}": m.read() for m in ro_dstore} @pytest.fixture def write_dir1(tmp_dir): write_dir1 = tmp_dir / "write1" write_dir1.mkdir(parents=True, exist_ok=True) yield write_dir1 shutil.rmtree(write_dir1) @pytest.fixture def write_dir2(tmp_dir): write_dir2 = tmp_dir / "write2" write_dir2.mkdir(parents=True, exist_ok=True) yield write_dir2 shutil.rmtree(write_dir2) @pytest.fixture def nc_objects(): return { f"id_{i}": NotCompleted("ERROR", "location", "message", source=f"id_{i}") for i in range(3) } @pytest.fixture def nc_dstore(tmp_dir, nc_objects): dstore = DataStoreDirectory(tmp_dir, suffix="fasta", mode="w") for id_, data in nc_objects.items(): dstore.write_not_completed(unique_id=id_, data=data.to_json()) return dstore def test_apply_to_input_only_not_completed(DATA_DIR, nc_dstore, tmp_dir): """correctly skips notcompleted""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) # trigger creation of notcompleted outpath = tmp_dir / "delme.sqlitedb" out_dstore = open_data_store(outpath, mode="w") writer = c3.get_app("write_db", out_dstore) process = ( c3.get_app("load_aligned", format_name="fasta", moltype="dna") + c3.get_app("min_length", 3000) + writer ) process.apply_to(dstore, show_progress=False) assert len(out_dstore.not_completed) == len(nc_dstore) out_dstore.close() def test_apply_to_makes_not_completed(DATA_DIR, tmp_dir): """correctly creates notcompleted""" dstore = open_data_store(DATA_DIR, suffix="fasta", limit=3) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") # trigger creation of notcompleted min_length = c3.get_app("min_length", 3000) out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) process = reader + min_length + writer process.apply_to(dstore, show_progress=False) assert len(out_dstore.not_completed) == 3 out_dstore.close() def test_apply_to_not_partially_done(DATA_DIR, tmp_dir): """correctly applies process when result already partially done""" dstore = open_data_store(DATA_DIR, suffix="fasta") num_records = len(dstore) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="w") writer = c3.get_app("write_db", out_dstore) # doing the first one # turning off warning as apps are callable _ = writer(reader(dstore[0])) # pylint: disable=not-callable writer.data_store.close() out_dstore = open_data_store(tmp_dir / "delme.sqlitedb", mode="a") writer = c3.get_app("write_db", out_dstore) process = reader + writer _ = process.apply_to(dstore, show_progress=False) assert len(out_dstore) == num_records out_dstore.close() @pytest.fixture def full_dstore(write_dir1, nc_objects, completed_objects, log_data): dstore = DataStoreDirectory(write_dir1, suffix="fasta", mode="w") for id_, data in nc_objects.items(): dstore.write_not_completed(unique_id=id_, data=data.to_json()) for id_, data in completed_objects.items(): dstore.write(unique_id=id_, data=data) dstore.write_log(unique_id="scitrack.log", data=log_data) return dstore # @pytest.mark.xfail(reason="passes except when run in full test suite") @pytest.mark.parametrize("show", [True, False]) def test_as_completed_progress(full_dstore, capsys, show): loader = c3.get_app("load_unaligned", format_name="fasta", moltype="dna") omit = c3.get_app("omit_degenerates") app = loader + omit list(app.as_completed(full_dstore.completed, show_progress=show)) result = capsys.readouterr().err.splitlines() if show: assert len(result) > 0 assert "6/6" in result[-1] else: assert len(result) == 0 def test_composite_pickleable(): """composable functions should be pickleable""" read = c3.get_app("load_aligned", moltype="dna") dumps(read) trans = c3.get_app("select_translatable") dumps(trans) aln = c3.get_app("progressive_align", "nucleotide") dumps(aln) just_nucs = c3.get_app("omit_degenerates", moltype="dna") dumps(just_nucs) limit = c3.get_app("fixed_length", 1000, random=True) dumps(limit) mod = c3.get_app("model", "HKY85") dumps(mod) qt = c3.get_app("quick_tree") dumps(qt) proc = read + trans + aln + just_nucs + limit + mod dumps(proc) def test_user_function(): """composable functions should be user definable""" @define_app def foo(val: c3types.AlignedSeqsType, *args, **kwargs) -> c3types.AlignedSeqsType: return val[:4] u_function = foo() aln = c3.make_aligned_seqs( [("a", "GCAAGCGTTTAT"), ("b", "GCTTTTGTCAAT")], moltype="dna", ) got = u_function(aln) assert got.to_dict() == {"a": "GCAA", "b": "GCTT"} def test_user_function_without_arg_kwargs(): """composable functions should be user definable""" @define_app def foo_without_arg_kwargs( val: c3types.AlignedSeqsType, ) -> c3types.AlignedSeqsType: return val[:4] u_function = foo_without_arg_kwargs() aln = c3.make_aligned_seqs( [("a", "GCAAGCGTTTAT"), ("b", "GCTTTTGTCAAT")], moltype="dna", ) got = u_function(aln) assert got.to_dict() == {"a": "GCAA", "b": "GCTT"} def test_user_function_multiple(): """user defined composable functions should not interfere with each other""" @define_app def foo(val: c3types.AlignedSeqsType, *args, **kwargs) -> c3types.AlignedSeqsType: return val[:4] @define_app def bar(val: c3types.AlignedSeqsType, num=3) -> c3types.PairwiseDistanceType: return val.distance_matrix(calc="hamming") u_function_1 = foo() u_function_2 = bar() aln_1 = c3.make_aligned_seqs( [("a", "GCAAGCGTTTAT"), ("b", "GCTTTTGTCAAT")], moltype="dna", ) data = {"s1": "ACGTACGTA", "s2": "GTGTACGTA"} aln_2 = c3.make_aligned_seqs(data, moltype="dna") got_1 = u_function_1(aln_1) got_2 = u_function_2(aln_2) assert got_1.to_dict() == {"a": "GCAA", "b": "GCTT"} assert got_2 == {("s1", "s2"): 2.0, ("s2", "s1"): 2.0} def test_concat_not_composable(): concat = c3.get_app("concat") assert not is_app_composable(concat) def test_cogent3_serialisable_compatible_with_serialisabletype(tmp_path): @define_app def foo(arg: str) -> c3types.TreeType: # we're checking composability only return c3.make_tree(arg, source="blah") foo_instance = foo() out = open_data_store(tmp_path / "out.sqlitedb", mode="w") writer = c3.get_app("write_db", data_store=out) loader = c3.get_app("load_db") app = foo_instance + writer result = app("(A:0.1,B:0.2);") # pylint: disable=not-callable got = loader(result) assert not isinstance(got, NotCompleted) out.close() @pytest.mark.parametrize("klass", [DataStoreDirectory, DataStoreSqlite]) def test_apply_to_writes_citations(DATA_DIR, tmp_dir, klass): """apply_to writes citations to the data store""" cite = _make_cite(title="Test Citation") dstore = open_data_store(DATA_DIR, suffix="fasta", limit=2) reader = c3.get_app("load_aligned", format_name="fasta", moltype="dna") @define_app(cite=cite) class cited_step: def main(self, val: c3types.AlignedSeqsType) -> c3types.AlignedSeqsType: return val outpath = ( tmp_dir / "cite_out" if klass == DataStoreDirectory else tmp_dir / "cite_out.sqlitedb" ) kwargs = {"suffix": "fasta"} if klass == DataStoreDirectory else {} out_dstore = open_data_store(outpath, mode="w", **kwargs) writer = c3.get_app("write_seqs", data_store=out_dstore) process = reader + cited_step() + writer result_dstore = process.apply_to(dstore, show_progress=False) loaded = result_dstore._load_citations() assert len(loaded) >= 1 titles = [c.title for c in loaded] assert "Test Citation" in titles out_dstore.close() @pytest.fixture def half_dstore1(write_dir1, nc_objects, completed_objects, log_data): dstore = DataStoreDirectory(write_dir1, suffix="fasta", mode="w") i = 0 for id_, data in nc_objects.items(): dstore.write_not_completed(unique_id=id_, data=data.to_json()) i += 1 if i >= len(nc_objects.items()) / 2: break i = 0 for id_, data in completed_objects.items(): dstore.write(unique_id=id_, data=data) i += 1 if i >= len(completed_objects.items()) / 2: break dstore.write_log(unique_id="scitrack.log", data=log_data) return dstore @pytest.fixture def half_dstore2(write_dir2, nc_objects, completed_objects, log_data): dstore = DataStoreDirectory(write_dir2, suffix="fasta", mode="w") for i, (id_, data) in enumerate(nc_objects.items()): if i >= len(nc_objects.items()) / 2: dstore.write_not_completed(unique_id=id_, data=data.to_json()) for i, (id_, data) in enumerate(completed_objects.items()): if i >= len(completed_objects.items()) / 2: dstore.write(unique_id=id_, data=data) dstore.write_log(unique_id="scitrack.log", data=log_data) return dstore def test_apply_to_only_appends(half_dstore1, half_dstore2): half_dstore1 = open_data_store( half_dstore1.source, suffix=half_dstore1.suffix, mode="a", ) reader1 = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length1 = c3.get_app("min_length", length=10) writer1 = c3.get_app("write_seqs", data_store=half_dstore1) process1 = reader1 + min_length1 + writer1 # create paths as strings dstore1 = [ str(Path(m.data_store.source) / m.unique_id) for m in half_dstore1.completed ] # check does not modify dstore when applied to same records orig_members = {m.unique_id for m in half_dstore1.members} got = process1.apply_to(dstore1) assert {m.unique_id for m in got.members} == orig_members half_dstore2 = open_data_store( half_dstore2.source, suffix=half_dstore2.suffix, mode="a", ) reader2 = c3.get_app("load_aligned", format_name="fasta", moltype="dna") min_length2 = c3.get_app("min_length", length=10) writer2 = c3.get_app("write_seqs", data_store=half_dstore2) process2 = reader2 + min_length2 + writer2 # check not fail on append new records _ = process2.apply_to(dstore1) @define_app def app_1(a: int) -> list[int]: return [a] @define_app def app_2(b: list[int]) -> int: return b[0] def test_composed_app_case(): a = app_1() b = app_2() composed = a + b assert isinstance(composed, ComposableApp) cogent3-scinexus-b16bc06/tests/test_data_store.py000066400000000000000000002557111525761402500222420ustar00rootroot00000000000000import copy import functools import json import os.path import pathlib import shutil import sys import threading from concurrent.futures import ThreadPoolExecutor from itertools import product from pathlib import Path, PureWindowsPath from pickle import dumps, loads import pytest from citeable import Software from scitrack import get_text_hexdigest try: import cogent3 as c3 from cogent3.util.union_dict import UnionDict except ImportError: c3 = None UnionDict = None from scinexus import data_store, io_util, open_data_store from scinexus.composable import NotCompleted, NotCompletedType from scinexus.data_store import ( APPEND, CITATIONS_FILE, COMPLETED_CHECKSUM, MD5_TABLE, NOT_COMPLETED_CHECKSUM, NOT_COMPLETED_TABLE, OVERWRITE, READONLY, DataStoreDirectory, ReadOnlyDataStoreZipped, _is_record, _member_id, get_data_source, get_id_from_source, get_summary_display, get_unique_id, load_record_from_json, make_record_for_json, set_id_from_source, set_summary_display, summary_not_completeds, ) # over-ride cogent3 setting set_summary_display(None) @pytest.fixture def tmp_dir(tmp_path_factory): return Path(tmp_path_factory.mktemp("datastore")) @pytest.fixture def fasta_dir(DATA_DIR, tmp_dir): tmp_dir = Path(tmp_dir) filenames = DATA_DIR.glob("*.fasta") fasta_dir = tmp_dir / "fasta" fasta_dir.mkdir(parents=True, exist_ok=True) for fn in filenames: dest = fasta_dir / fn.name dest.write_text(fn.read_text()) return fasta_dir @pytest.fixture def write_dir(tmp_dir): tmp_dir = Path(tmp_dir) write_dir = tmp_dir / "write" write_dir.mkdir(parents=True, exist_ok=True) yield write_dir shutil.rmtree(write_dir, ignore_errors=True) @pytest.fixture def w_dstore(write_dir): return DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) @pytest.fixture def ro_dstore(fasta_dir): return DataStoreDirectory(fasta_dir, suffix="fasta", mode=READONLY) @pytest.fixture def completed_objects(ro_dstore): return {f"{Path(m.unique_id).stem}": m.read() for m in ro_dstore} @pytest.fixture def nc_objects(): return { f"id_{i}": NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"id_{i}" ) for i in range(3) } @pytest.fixture(scope="session") def log_data(DATA_DIR): path = DATA_DIR / "scitrack.log" return path.read_text() @pytest.fixture def full_dstore(write_dir, nc_objects, completed_objects, log_data): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) for id_, data in nc_objects.items(): dstore.write_not_completed(unique_id=id_, data=data.to_json()) for id_, data in completed_objects.items(): dstore.write(unique_id=id_, data=data) dstore.write_log(unique_id="scitrack.log", data=log_data) return dstore @pytest.fixture def nc_dir(tmp_dir): nc_dir = tmp_dir / "nc_test" nc_dir.mkdir(parents=True, exist_ok=True) yield nc_dir shutil.rmtree(nc_dir, ignore_errors=True) @pytest.fixture def nc_dstore(DATA_DIR, nc_dir): dstore = DataStoreDirectory(nc_dir, suffix="fasta", mode=OVERWRITE) log_filename = "scitrack.log" dstore.write_log(unique_id=log_filename, data=(DATA_DIR / log_filename).read_text()) nc = [ NotCompleted( NotCompletedType.FAIL, f"dummy{i}", f"dummy_message{i}", source=f"dummy_source{i}", ) for i in range(3) ] for i, item in enumerate(nc): dstore.write_not_completed(unique_id=f"nc{i + 1}", data=item.to_json()) assert len(dstore.not_completed) == 3 filenames = DATA_DIR.glob("*.fasta") for fn in filenames: identifier = fn.name dstore.write(unique_id=identifier, data=fn.read_text()) return dstore @pytest.fixture def sample_citations(): cite1 = Software( author=["Doe, J"], title="Tool One", year=2024, url="https://example.com/one", version="1.0", license="MIT", doi="10.0/one", publisher="test", ) cite2 = Software( author=["Smith, A"], title="Tool Two", year=2024, url="https://example.com/two", version="2.0", license="MIT", doi="10.0/two", publisher="test", ) return (cite1, cite2) def _get_member_data(members): return {m.unique_id: m.read() for m in members} @pytest.fixture def zipped_basic(fasta_dir): path = shutil.make_archive( base_name=str(fasta_dir.parent / fasta_dir.name), format="zip", base_dir=fasta_dir.name, root_dir=fasta_dir.parent, ) return pathlib.Path(path) @pytest.fixture def zipped_full(full_dstore): source = pathlib.Path(full_dstore.source) path = shutil.make_archive( base_name=str(source.parent / source.name), format="zip", base_dir=source.name, root_dir=source.parent, ) return ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") @pytest.fixture def zipped_hidden(fasta_dir): # create a hidden file hidden = fasta_dir / ".hidden.fasta" hidden.write_text(">s1\nACGT\n") path = shutil.make_archive( base_name=str(fasta_dir.parent / (fasta_dir.name + "_hidden")), format="zip", base_dir=fasta_dir.name, root_dir=fasta_dir.parent, ) hidden.unlink() return pathlib.Path(path) def test_data_member_eq(ro_dstore, fasta_dir): ro_dstore2 = DataStoreDirectory(fasta_dir, mode="r", suffix="fasta") name = "brca1.fasta" mem1 = next(m for m in ro_dstore.completed if m.unique_id == name) mem2 = next(m for m in ro_dstore2.completed if m.unique_id == name) assert mem1 != mem2 def test_fail_try_append(full_dstore, completed_objects): full_dstore._mode = APPEND id_, data = next(iter(completed_objects.items())) with pytest.raises(IOError): full_dstore.write(unique_id=id_, data=data) def test_contains(ro_dstore): """correctly identify when a data store contains a member""" assert "brca1.fasta" in ro_dstore assert "brca1" in ro_dstore def test_len(ro_dstore): """DataStore returns correct len""" expect = len(list(ro_dstore.source.glob("*.fasta"))) assert expect == len(ro_dstore) == len(ro_dstore.members) def test_getitem(ro_dstore): with pytest.raises(IndexError): _ = ro_dstore[len(ro_dstore)] last = ro_dstore[-1] first = ro_dstore[0] assert last.unique_id != first.unique_id def test_iterall(ro_dstore): expect = {fn.name for fn in ro_dstore.source.glob("*.fasta")} got = {m.unique_id for m in ro_dstore} assert expect == got def test_read(ro_dstore): """correctly read content""" expect = (ro_dstore.source / "brca1.fasta").read_text() got = ro_dstore.read("brca1.fasta") assert got == expect def test_pickleable_roundtrip(ro_dstore): """pickling of data stores should be reversible""" re_dstore = loads(dumps(ro_dstore)) assert str(ro_dstore) == str(re_dstore) assert ro_dstore[0].read() == re_dstore[0].read() def test_deepcopy_roundtrip(ro_dstore): """apps deepcopy their arguments on every call, so a store must survive it The member cache is guarded by a lock, and a lock cannot be pickled. deepcopy goes through the same reduce protocol as pickle, so a store passed to an app as an argument would otherwise raise on every call rather than only when someone pickled it. """ copied = copy.deepcopy(ro_dstore) assert str(copied) == str(ro_dstore) assert copied[0].read() == ro_dstore[0].read() assert len(copied.completed) == len(ro_dstore.completed) def test_pickleable_member_roundtrip(ro_dstore): """pickling of data store members should be reversible""" re_member = loads(dumps(ro_dstore[0])) data = re_member.read() assert len(data) > 0 def test_empty_directory(fasta_dir): dstore = DataStoreDirectory(fasta_dir, suffix=".txt") assert len(dstore) == 0 def test_no_logs(ro_dstore): assert len(ro_dstore.logs) == 0 def test_no_not_completed(ro_dstore): assert len(ro_dstore.not_completed) == 0 def test_logs(nc_dstore): assert len(nc_dstore.logs) == 1 log = nc_dstore.logs[0].read() assert isinstance(log, str) def test_not_completed(nc_dstore): assert len(nc_dstore.not_completed) == 3 nc = nc_dstore.not_completed[0].read() assert isinstance(nc, str) def test_drop_not_completed(nc_dstore): num_completed = len(nc_dstore.completed) num_not_completed = len(nc_dstore.not_completed) num_md5 = len(list((nc_dstore.source / MD5_TABLE).glob("*"))) assert num_not_completed == 3 assert num_completed == 6 assert len(nc_dstore) == 9 assert num_md5 == num_completed + num_not_completed nc_dstore.drop_not_completed() assert len(nc_dstore.not_completed) == 0 num_md5 = len(list((nc_dstore.source / MD5_TABLE).glob("*"))) assert num_md5 == num_completed def test_write_not_completed_twice_caches_one_member(w_dstore): """re-writing a not-completed record leaves one cached member, not two""" data = NotCompleted( NotCompletedType.ERROR, "location", "message", source="nc1", ).to_json() expect = [f"{NOT_COMPLETED_TABLE}/nc1.json"] w_dstore.write_not_completed(unique_id="nc1", data=data) # the attribute, not the property: the property rebuilds itself from # disk when empty, so it reports a member even if nothing recorded one assert [m.unique_id for m in w_dstore._not_completed] == expect # the second write rescans nothing, so the cached list is the same # object as before it and identity alone cannot tell the two apart w_dstore.write_not_completed(unique_id="nc1", data=data) nc_dir = w_dstore.source / NOT_COMPLETED_TABLE assert len(list(nc_dir.glob("*.json"))) == 1 assert [m.unique_id for m in w_dstore._not_completed] == expect w_dstore.drop_not_completed() assert list(nc_dir.glob("*.json")) == [] def test_append_mode_refuses_to_rewrite_a_not_completed_record(w_dstore): """APPEND refuses a second not-completed record, as it does a completed one""" data = NotCompleted( NotCompletedType.ERROR, "location", "message", source="nc1", ).to_json() w_dstore.write_not_completed(unique_id="nc1", data=data) path = w_dstore.source / NOT_COMPLETED_TABLE / "nc1.json" w_dstore._mode = APPEND with pytest.raises(OSError): w_dstore.write_not_completed(unique_id="nc1", data='{"replaced": true}') assert path.read_text() == data def test_append_mode_refuses_to_rewrite_a_respelled_not_completed(w_dstore): """the APPEND refusal follows a respelled identifier to the record it names""" # the identifier resolves to nc1.json, so the refusal has to recognise # the second write as the record already stored, not as a new one data = NotCompleted( NotCompletedType.ERROR, "location", "message", source="nc1", ).to_json() w_dstore.write_not_completed(unique_id="nc1.fasta", data=data) assert str(Path(NOT_COMPLETED_TABLE) / "nc1.json") in w_dstore w_dstore._mode = APPEND with pytest.raises(OSError): w_dstore.write_not_completed(unique_id="nc1.json", data="{}") def test_write_not_completed_beside_a_colliding_completed_record(write_dir): """a completed record of the same name does not suppress the write""" # a store whose own suffix is json is where the completed id nc1.json # and the not-completed one collide once the bare nc1 is completed dstore = DataStoreDirectory(write_dir, suffix="json", mode=OVERWRITE) dstore.write(unique_id="nc1.json", data='{"completed": true}') dstore._mode = APPEND member = dstore.write_not_completed(unique_id="nc1", data='{"failed": true}') assert member is not None assert (write_dir / NOT_COMPLETED_TABLE / "nc1.json").exists() def test_append_mode_refuses_a_rename_onto_an_existing_record(w_dstore): """APPEND refuses a write whose identifier is renamed onto one in the store""" # c1.txt carries the wrong suffix for this store and becomes c1.fasta # before anything is written, so only the renamed id can be checked w_dstore.write(unique_id="c1.fasta", data=">first\nAAAA\n") w_dstore._mode = APPEND with pytest.raises(OSError): w_dstore.write(unique_id="c1.txt", data=">second\nTTTT\n") def test_write_twice_caches_one_member_for_log_suffix(write_dir): """a store whose own suffix is log records a re-written member once""" # log is the one suffix _write exempts from its duplicate guard, so # this is the store where a repeat write reaches the cache at all dstore = DataStoreDirectory(write_dir, suffix="log", mode=OVERWRITE) dstore.write(unique_id="c1.log", data="first") dstore.write(unique_id="c1.log", data="second") assert [m.unique_id for m in dstore._completed] == ["c1.log"] assert len(dstore) == 1 @pytest.fixture def mixed_md5_dstore(tmp_dir): """a store where only one of two not-completed records has a checksum""" # a checksum is optional -- md5() returns None without one and # _validate counts it under md5_missing -- so a store assembled by hand # or by an earlier writer can hold both kinds, and a drop meets both source = tmp_dir / "mixed_md5" (source / NOT_COMPLETED_TABLE).mkdir(parents=True) (source / MD5_TABLE).mkdir(parents=True) for i in range(2): nc = NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"id_{i}", ) data = nc.to_json() (source / NOT_COMPLETED_TABLE / f"id_{i}.json").write_text(data) if i == 0: checksum = f"id_{i}.{NOT_COMPLETED_CHECKSUM}" (source / MD5_TABLE / checksum).write_text(get_text_hexdigest(data)) return DataStoreDirectory(source, suffix="fasta", mode=OVERWRITE) def test_drop_not_completed_without_md5_file(mixed_md5_dstore): """a record with no checksum file is dropped like any other""" source = mixed_md5_dstore.source assert len(mixed_md5_dstore.not_completed) == 2 # id_1 is the one without a checksum. dropping by identifier leaves the # cache in place, unlike a full drop, so the assertion below observes # the list itself rather than a rebuild of it mixed_md5_dstore.drop_not_completed(unique_id="id_1") expect = [f"{NOT_COMPLETED_TABLE}/id_0.json"] assert [m.unique_id for m in mixed_md5_dstore.not_completed] == expect assert not (source / NOT_COMPLETED_TABLE / "id_1.json").exists() mixed_md5_dstore.drop_not_completed() assert list((source / NOT_COMPLETED_TABLE).glob("*.json")) == [] assert list((source / MD5_TABLE).glob("*")) == [] def test_drop_not_completed_keeps_what_limit_hides(nc_dstore): """a full drop empties the limited view and keeps the rest""" nc_dstore._limit = 1 nc_dstore._not_completed = [] nc_dir = nc_dstore.source / NOT_COMPLETED_TABLE assert len(nc_dstore.not_completed) == 1 assert len(list(nc_dir.glob("*.json"))) == 3 nc_dstore.drop_not_completed() # records remain, so the directory is still in use assert nc_dir.exists() assert len(list(nc_dir.glob("*.json"))) == 2 def test_drop_not_completed_by_id_keeps_what_limit_hides(nc_dstore): """a record the limited view omits is not dropped by identifier""" nc_dstore._limit = 1 nc_dstore._not_completed = [] nc_dir = nc_dstore.source / NOT_COMPLETED_TABLE shown = {Path(m.unique_id).name for m in nc_dstore.not_completed} hidden = next(p for p in nc_dir.glob("*.json") if p.name not in shown) nc_dstore.drop_not_completed(unique_id=hidden.stem) assert hidden.exists() @pytest.mark.parametrize( "unique_id", ["nc1", "nc1.fasta", "nc1.json", "nc1.txt", "a.b.fasta"], ) def test_write_drops_the_twin_however_the_id_is_spelled(w_dstore, unique_id): """the record a write supersedes is found whatever extension the id carries""" # a compressed spelling is absent because this store does not write # one, so it is refused rather than stored under some other name record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") w_dstore.write_not_completed(unique_id=unique_id, data=record.to_json()) nc_dir = w_dstore.source / NOT_COMPLETED_TABLE assert len(list(nc_dir.glob("*.json"))) == 1 w_dstore.write(unique_id=unique_id, data=">s\nACGT\n") assert list(nc_dir.glob("*.json")) == [] def test_write_leaves_unrelated_not_completed_records(w_dstore): """superseding one record does not touch another whose name ends the same""" # write("c1.fasta") looks for not_completed/c1.json, and nc1.json and # abc1.json both end with that name for uid in ("nc1", "abc1", "c1"): record = NotCompleted(NotCompletedType.ERROR, "location", "message", source=uid) w_dstore.write_not_completed(unique_id=uid, data=record.to_json()) w_dstore.write(unique_id="c1.fasta", data=">s\nACGT\n") nc_dir = w_dstore.source / NOT_COMPLETED_TABLE assert sorted(p.name for p in nc_dir.glob("*.json")) == ["abc1.json", "nc1.json"] def test_write_keeps_the_checksum_of_the_record_it_wrote(w_dstore): """superseding a not-completed record leaves the new checksum in place""" # both kinds were kept under one name, so the drop that supersedes the # twin deleted the checksum _write had written moments earlier record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="id_0") w_dstore.write_not_completed(unique_id="id_0", data=record.to_json()) data = ">s\nACGT\n" w_dstore.write(unique_id="id_0.fasta", data=data) assert w_dstore.md5("id_0.fasta") == get_text_hexdigest(data) def test_the_two_kinds_of_record_keep_separate_checksums(w_dstore): """a completed and a not-completed record of one name each keep their own""" data = ">s\nACGT\n" record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="id_0") nc_data = record.to_json() w_dstore.write(unique_id="id_0.fasta", data=data) w_dstore.write_not_completed(unique_id="id_0", data=nc_data) assert w_dstore.md5("id_0.fasta") == get_text_hexdigest(data) nc_id = str(Path(NOT_COMPLETED_TABLE) / "id_0.json") assert w_dstore.md5(nc_id) == get_text_hexdigest(nc_data) @pytest.mark.parametrize("unique_id", ["id_0", "id_0.fasta.gz"]) def test_the_checksum_of_a_compressed_record_is_found(tmp_dir, unique_id): """the two-suffix strip agrees across writing, reading and dropping""" # a compressed store is where _record_stem has two suffixes to take # off, so a checksum name built with one of them looks right for # every uncompressed store and fails only here dstore = DataStoreDirectory(tmp_dir / "gz", suffix="fasta.gz", mode=OVERWRITE) data = ">s\nACGT\n" dstore.write(unique_id=unique_id, data=data) assert dstore.md5(unique_id) == get_text_hexdigest(data) @pytest.mark.parametrize("unique_id", ["id_0", "id_0.fasta", "id_0.genbank"]) def test_the_checksum_of_a_record_is_found_where_it_was_put(w_dstore, unique_id): """writing, reading and dropping agree on where a checksum lives""" # they were three separate computations of the name, and an extension # the store does not use made all three disagree. the store now names # the file, so every spelling here is the one record, id_0.fasta data = ">s\nACGT\n" w_dstore.write(unique_id=unique_id, data=data) assert w_dstore.md5(unique_id) == get_text_hexdigest(data) @pytest.mark.parametrize( "unique_id", ["id_0", "id_0.fasta", "id_0.genbank", "id_0.txt"], ) def test_the_store_suffix_decides_the_stored_name(w_dstore, unique_id): """however the identifier is spelled, the store names the file""" # a suffix the identifier carried used to survive into the name, so a # store of .fasta could hold an id_0.fasta.gz its own scan cannot see. # globbing every file, not just *.fasta, so a record left under some # other name is a failure rather than something the pattern hides w_dstore.write(unique_id=unique_id, data=">s\nACGT\n") stored = [p.name for p in w_dstore.source.glob("*") if p.is_file()] assert stored == ["id_0.fasta"] @pytest.mark.parametrize("unique_id", ["id_0", "id_0.fasta", "id_0.fasta.gz"]) def test_a_compound_suffix_is_appended_once(tmp_dir, unique_id): """a store of .fasta.gz stores id_0.fasta.gz, not id_0.fasta.fasta.gz""" dstore = DataStoreDirectory(tmp_dir / "gz", suffix="fasta.gz", mode=OVERWRITE) dstore.write(unique_id=unique_id, data=">s\nACGT\n") stored = [p.name for p in dstore.source.glob("*") if p.is_file()] assert stored == ["id_0.fasta.gz"] def test_a_compound_suffix_store_writes_compressed(tmp_dir): """the suffix the store names picks the engine the record is written with""" # _write chooses the mode from the name, and open_ the handler, so the # compression follows from the suffix rather than from the identifier import gzip dstore = DataStoreDirectory(tmp_dir / "gz", suffix="fasta.gz", mode=OVERWRITE) data = ">s\nACGT\n" dstore.write(unique_id="id_0", data=data) assert ( gzip.decompress((dstore.source / "id_0.fasta.gz").read_bytes()) == data.encode() ) @pytest.mark.parametrize( "suffix", ["fasta", "fasta.gz", "fasta.bz2", "fasta.xz", "fasta.zip"], ) def test_a_record_is_written_with_a_line_feed_whatever_the_suffix( tmp_dir, monkeypatch, suffix, ): """a record has the same bytes on disk on every platform""" # newline=None is the universal default of TextIOWrapper, which on a # write turns every \n into os.linesep, so a record left on it holds # \r\n on windows and \n elsewhere. the choice is made inside # TextIOWrapper from the platform it was built for, so the argument # open_ is called with is the only part of it visible from here. # both bindings are patched because a zip lands its bytes in the file # atomic_write opens through io_util's own name, not this one seen = [] def record_writes(module): real_open = module.open_ def recording_open(filename, mode="rt", **kwargs): if mode.startswith("w"): seen.append((Path(filename).name, kwargs.get("newline"))) return real_open(filename, mode=mode, **kwargs) monkeypatch.setattr(module, "open_", recording_open) record_writes(data_store) record_writes(io_util) dstore = DataStoreDirectory(tmp_dir / "store", suffix=suffix, mode=OVERWRITE) dstore.write(unique_id="id_0", data=">s\nACGT\n") assert seen assert all(newline == "\n" for _, newline in seen), seen @pytest.mark.parametrize( "unique_id", ["nc1", "nc1.json", "nc1.fasta", "nc1.txt"], ) def test_a_not_completed_record_is_stored_as_plain_json(w_dstore, unique_id): """not-completed records are json whatever the identifier carried""" # nc1.fasta.gz used to become nc1.json.json, because the store suffix # was replaced inside the name rather than the name being rebuilt record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") w_dstore.write_not_completed(unique_id=unique_id, data=record.to_json()) nc_dir = w_dstore.source / NOT_COMPLETED_TABLE assert [p.name for p in nc_dir.glob("*")] == ["nc1.json"] def test_an_identifier_containing_the_suffix_keeps_its_stem(w_dstore): """the suffix is replaced at the end of the name, not wherever it occurs""" # the store suffix used to be replaced by str.replace over the whole # name, so fasta_seqs was stored as json_seqs.json record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="x") w_dstore.write_not_completed(unique_id="fasta_seqs", data=record.to_json()) nc_dir = w_dstore.source / NOT_COMPLETED_TABLE assert [p.name for p in nc_dir.glob("*")] == ["fasta_seqs.json"] @pytest.mark.parametrize( "unique_id", ["id_0.fasta.gz", "id_0.gz", "id_0.fasta.bz2"], ) def test_a_compression_the_store_does_not_write_is_refused(w_dstore, unique_id): """an identifier naming a compression this store does not use is an error""" # the store decides the name, so it cannot honour the request with pytest.raises(ValueError): w_dstore.write(unique_id=unique_id, data=">s\nACGT\n") @pytest.mark.parametrize("unique_id", ["id_0.fasta.bz2"]) def test_another_compression_is_refused_by_a_compressed_store(tmp_dir, unique_id): """a .fasta.gz store does not accept an identifier naming bz2""" dstore = DataStoreDirectory(tmp_dir / "gz", suffix="fasta.gz", mode=OVERWRITE) with pytest.raises(ValueError): dstore.write(unique_id=unique_id, data=">s\nACGT\n") def test_a_compressed_not_completed_identifier_is_refused(w_dstore): """not-completed records are plain json, so a compressed id conflicts""" record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") with pytest.raises(ValueError): w_dstore.write_not_completed(unique_id="nc1.json.gz", data=record.to_json()) @pytest.mark.parametrize("unique_id", ["", " ", "\t", "."]) def test_an_identifier_with_nothing_to_name_the_record_by_is_refused( w_dstore, unique_id, ): """a record needs a stem, since the stem is the whole of its identity""" # an empty one gave the file the suffix and nothing else, .fasta in # this store, and a whitespace one gave " .fasta" with pytest.raises(ValueError): w_dstore.write(unique_id=unique_id, data=">s\nACGT\n") def test_a_store_whose_suffix_is_a_compression_refuses_it_too(tmp_dir): """the store that made this visible refuses it too""" # an empty id here gave a file called .gz holding plain text, so the # name claimed gzip and nothing had compressed it dstore = DataStoreDirectory(tmp_dir / "gz", suffix="gz", mode=OVERWRITE) with pytest.raises(ValueError): dstore.write(unique_id="", data="hello") def test_a_not_completed_record_needs_a_stem_too(w_dstore): """the refusal is about the identifier, so it covers every kind""" record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="x") with pytest.raises(ValueError): w_dstore.write_not_completed(unique_id="", data=record.to_json()) def test_a_log_needs_a_stem_too(w_dstore): """write_log goes through the same naming and the same refusal""" with pytest.raises(ValueError): w_dstore.write_log(unique_id="", data="a log line") def test_a_hidden_file_is_not_a_record(tmp_dir): """the directory scan passes over a hidden file as the zip scan does""" # a ._name sidecar from macOS, or an editor's .swp, is not data the # store was asked to keep. listing it made the same directory two # different stores once zipped, since the zip scan skips them source = tmp_dir / "hidden" source.mkdir(parents=True) (source / "brca1.fasta").write_text(">s\nACGT\n") (source / "._brca1.fasta").write_text("resource fork junk") dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert [m.unique_id for m in dstore.completed] == ["brca1.fasta"] def test_a_hidden_not_completed_file_is_not_a_record(tmp_dir): """the not-completed scan passes over them too""" source = tmp_dir / "hiddennc" nc_dir = source / NOT_COMPLETED_TABLE nc_dir.mkdir(parents=True) record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") (nc_dir / "nc1.json").write_text(record.to_json()) (nc_dir / "._nc1.json").write_text("junk") dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert [Path(m.unique_id).name for m in dstore.not_completed] == ["nc1.json"] @pytest.mark.parametrize("unique_id", [".brca1", "._brca1", ".brca1.fasta"]) def test_a_hidden_identifier_is_refused(w_dstore, unique_id): """what the scan will not read back, the store will not write""" with pytest.raises(ValueError): w_dstore.write(unique_id=unique_id, data=">s\nACGT\n") def test_the_two_stores_agree_about_hidden_files(tmp_dir, tmp_path): """a directory store and a zip of it have the same members""" import shutil source = tmp_dir / "agree" source.mkdir(parents=True) (source / "brca1.fasta").write_text(">s\nACGT\n") (source / ".hidden.fasta").write_text(">s\nTTTT\n") archive = shutil.make_archive(str(tmp_path / "agree"), "zip", str(tmp_dir), "agree") directory = DataStoreDirectory(source, suffix="fasta", mode=READONLY) zipped = ReadOnlyDataStoreZipped(pathlib.Path(archive), suffix="fasta") assert [m.unique_id for m in directory.completed] == [ m.unique_id for m in zipped.completed ] def test_a_read_only_store_says_so_before_judging_the_identifier(tmp_dir): """being read only is the more fundamental refusal, so it comes first""" source = tmp_dir / "ro" source.mkdir(parents=True) dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) with pytest.raises(OSError, match="readonly"): dstore.write(unique_id="id_0.fasta.gz", data=">s\nACGT\n") def test_contains_matches_a_member_id_across_separators(w_dstore): """the separator a member id was composed with is not part of the question""" # a member id is spelled with "/", but a caller may hand over either # spelling, and a string comparison would match only its own. # PureWindowsPath stands in for the platform, since the parts are what # __contains__ compares record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") member = w_dstore.write_not_completed(unique_id="nc1", data=record.to_json()) assert member.unique_id in w_dstore assert f"./{member.unique_id}" in w_dstore assert PureWindowsPath("not_completed/nc1.json").parts == ( PureWindowsPath(r"not_completed\nc1.json").parts ) def test_a_completed_member_id_is_the_file_name_alone(): """no subdirectory holds a completed record, so nothing is prefixed""" assert _member_id("", "id_0.fasta") == "id_0.fasta" def test_a_member_id_under_a_subdir_is_joined_with_a_slash(): assert _member_id(NOT_COMPLETED_TABLE, "nc1.json") == "not_completed/nc1.json" def test_member_ids_are_spelled_the_same_on_every_platform( w_dstore, write_dir, log_data, monkeypatch, ): """a member id names a record, so Windows spells it as POSIX does""" # the platform half of this cannot run here, so stand in for it the way # the normcase test above does. both stores are built before the patch: # PureWindowsPath has no exists() or mkdir(), which __init__ needs scan = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") monkeypatch.setattr(data_store, "Path", PureWindowsPath) written = w_dstore.write_not_completed(unique_id="nc1", data=record.to_json()) w_dstore.write_log(unique_id="scitrack.log", data=log_data) assert written.unique_id == "not_completed/nc1.json" assert [m.unique_id for m in scan.not_completed] == ["not_completed/nc1.json"] assert [m.unique_id for m in scan.logs] == ["logs/scitrack.log"] def test_dropping_one_not_completed_matches_on_every_platform(w_dstore, monkeypatch): """the id drop compares against is composed the same way the members are""" # it is an exact string comparison, so spelled with the platform # separator it would match nothing on Windows. no exception either: # write() would just stop superseding the not-completed twin record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") w_dstore.write_not_completed(unique_id="nc1", data=record.to_json()) monkeypatch.setattr(data_store, "Path", PureWindowsPath) w_dstore.drop_not_completed(unique_id="nc1") assert w_dstore._not_completed == [] assert not (w_dstore.source / NOT_COMPLETED_TABLE / "nc1.json").exists() def test_zipped_member_ids_are_spelled_the_same_on_every_platform( zipped_full, monkeypatch, ): """a zipped store names its members as the directory it was made from""" monkeypatch.setattr(data_store, "Path", PureWindowsPath) got = sorted(m.unique_id for m in zipped_full.not_completed) assert got == [f"not_completed/id_{i}.json" for i in range(3)] assert [m.unique_id for m in zipped_full.logs] == ["logs/scitrack.log"] def test_contains_keeps_the_kinds_and_the_case_apart(w_dstore): """`in` names one exact record, not every record sharing its stem""" # PureWindowsPath equality is case insensitive, so comparing Path # objects would make these one record on Windows and two on POSIX record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") w_dstore.write_not_completed(unique_id="nc1", data=record.to_json()) assert "nc1" not in w_dstore assert "nc1.fasta" not in w_dstore assert str(Path(NOT_COMPLETED_TABLE) / "NC1.json") not in w_dstore assert str(Path(NOT_COMPLETED_TABLE) / "nc1.json") in w_dstore @pytest.mark.parametrize("identifier", [None, 1, Path("nc1.json"), object()]) def test_contains_says_no_to_what_is_not_an_identifier( w_dstore, zipped_basic, identifier, ): """a non-string answers False rather than raising""" # the comparison builds a Path from what it is given, and Path(1) # raises, where the plain string comparison this replaced just missed. # both stores, because DataStoreDirectory refuses a non-string in its # own override and the zip store reaches the one in the base class record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") w_dstore.write_not_completed(unique_id="nc1", data=record.to_json()) zipped = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") assert identifier not in w_dstore assert identifier not in zipped def test_the_scan_matches_the_suffix_as_written(tmp_dir): """a store of .fasta does not claim an ID_0.FASTA""" # this passes on POSIX either way and is here for the Windows runner, # where Path.glob folds case and so answered the opposite of what the # naming path does. the only file is the upper case one on purpose: # writing a lower case twin beside it would land on the same file on # a case insensitive filesystem, leaving one entry named id_0.fasta # and a test that passes there whatever the scan does source = tmp_dir / "cased" source.mkdir(parents=True) (source / "ID_0.FASTA").write_text(">s\nTTTT\n") dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert dstore.completed == [] def test_the_scan_does_not_fold_case_the_way_the_platform_would(monkeypatch): """the matcher ignores os.path.normcase, which is what folds on Windows""" # the platform half of this cannot run here, so stand in for it: this # is exactly what fnmatch does and fnmatchcase does not monkeypatch.setattr(os.path, "normcase", str.lower) assert _is_record("id_0.fasta", "fasta") assert not _is_record("ID_0.FASTA", "fasta") def test_a_suffix_may_carry_a_trailing_wildcard(tmp_dir): """suffix="fasta*" takes the compressed spellings in as well""" # the scan is a pattern match, not a comparison, and this is the only # thing that needs it to be source = tmp_dir / "wild" source.mkdir(parents=True) for name in ("id_0.fasta", "id_1.fasta.gz", "id_2.fasta.bz2", "notes.txt"): (source / name).write_text(">s\nACGT\n") dstore = DataStoreDirectory(source, suffix="fasta*", mode=READONLY) assert sorted(m.unique_id for m in dstore.completed) == [ "id_0.fasta", "id_1.fasta.gz", "id_2.fasta.bz2", ] def test_limit_counts_matches_not_directory_entries(tmp_dir): """limit truncates the members, so what is not a member does not count""" # guards the accounting rather than fixing it: the scan sees every # entry now, where the pattern used to pre-select what was counted, # so limit had to start counting what it keeps source = tmp_dir / "limited" source.mkdir(parents=True) for i in range(3): (source / f"id_{i}.fasta").write_text(">s\nACGT\n") (source / f"note_{i}.txt").write_text("ignore me") dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY, limit=2) assert len(dstore.completed) == 2 def test_the_not_completed_scan_passes_over_what_is_not_a_record(tmp_dir): """a stray file in not_completed is not a not-completed record""" source = tmp_dir / "stray" nc_dir = source / NOT_COMPLETED_TABLE nc_dir.mkdir(parents=True) record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") (nc_dir / "nc1.json").write_text(record.to_json()) (nc_dir / "README.txt").write_text("notes about this directory") dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert [Path(m.unique_id).name for m in dstore.not_completed] == ["nc1.json"] def test_a_case_variant_extension_is_a_different_record(w_dstore): """FASTA is not the suffix of a .fasta store, so it stays in the stem""" # the suffix is matched as written. an extension that is not it, for # whatever reason, is part of the name the store appends its own to w_dstore.write(unique_id="id_0.fasta", data=">s\nACGT\n") w_dstore.write(unique_id="id_0.FASTA", data=">s\nTTTT\n") stored = sorted(p.name for p in w_dstore.source.glob("*") if p.is_file()) assert stored == ["id_0.FASTA.fasta", "id_0.fasta"] def test_an_identifier_carrying_a_directory_is_stored_by_its_name(w_dstore): """a path-like identifier names a record, it does not name a location""" # it used to be kept whole and handed to open_, which raised # FileNotFoundError for a subdirectory the store had not created w_dstore.write(unique_id="sub/id_0.fasta", data=">s\nACGT\n") assert [p.name for p in w_dstore.source.glob("*.fasta")] == ["id_0.fasta"] def test_md5_falls_back_to_the_older_checksum_name(tmp_dir): """a store written before the rename still reports its checksums""" source = tmp_dir / "legacy" (source / MD5_TABLE).mkdir(parents=True) data = ">s\nACGT\n" (source / "id_0.fasta").write_text(data) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(data)) dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert dstore.md5("id_0.fasta") == get_text_hexdigest(data) def test_dropping_removes_a_shared_checksum_only_it_can_claim(tmp_dir): """a drop takes the shared file when no completed record wants it""" source = tmp_dir / "sharedgone" (source / MD5_TABLE).mkdir(parents=True) (source / NOT_COMPLETED_TABLE).mkdir(parents=True) record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="id_0") failed = record.to_json() (source / NOT_COMPLETED_TABLE / "id_0.json").write_text(failed) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(failed)) dstore = DataStoreDirectory(source, suffix="fasta", mode=OVERWRITE) dstore.drop_not_completed() assert list((source / MD5_TABLE).glob("*")) == [] def test_dropping_keeps_a_shared_checksum_a_completed_record_may_own(tmp_dir): """a drop leaves the shared file when a completed record carries the stem""" # it holds whichever of the two wrote last, which was never recorded, # so taking it would be guessing and leaving it makes it answer for the # completed record. neither is right, so it is left and reported source = tmp_dir / "sharedkept" (source / MD5_TABLE).mkdir(parents=True) (source / NOT_COMPLETED_TABLE).mkdir(parents=True) done = ">s\nACGT\n" record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="id_0") (source / "id_0.fasta").write_text(done) (source / NOT_COMPLETED_TABLE / "id_0.json").write_text(record.to_json()) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(done)) dstore = DataStoreDirectory(source, suffix="fasta", mode=OVERWRITE) dstore.drop_not_completed() assert (source / MD5_TABLE / "id_0.txt").exists() assert dstore.md5("id_0.fasta") == get_text_hexdigest(done) def test_zipped_md5_falls_back_to_the_older_checksum_name(tmp_dir): """the same fallback works inside an archive, which cannot be renamed""" source = tmp_dir / "ziplegacy" (source / MD5_TABLE).mkdir(parents=True) data = ">s\nACGT\n" (source / "id_0.fasta").write_text(data) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(data)) path = shutil.make_archive( base_name=str(source.parent / source.name), format="zip", base_dir=source.name, root_dir=source.parent, ) dstore = ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") assert dstore.md5("id_0.fasta") == get_text_hexdigest(data) @pytest.fixture def legacy_md5_dstore(tmp_dir): """a store whose checksums are all under the name both kinds shared""" source = tmp_dir / "legacy_store" (source / MD5_TABLE).mkdir(parents=True) (source / NOT_COMPLETED_TABLE).mkdir(parents=True) md5_dir = source / MD5_TABLE # only a completed record carries this stem done = ">s\nACGT\n" (source / "solo_done.fasta").write_text(done) (md5_dir / "solo_done.txt").write_text(get_text_hexdigest(done)) # only a not-completed record carries this one record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="x") failed = record.to_json() (source / NOT_COMPLETED_TABLE / "solo_failed.json").write_text(failed) (md5_dir / "solo_failed.txt").write_text(get_text_hexdigest(failed)) # both kinds carry this one, so the file cannot be attributed (source / "both.fasta").write_text(done) (source / NOT_COMPLETED_TABLE / "both.json").write_text(failed) (md5_dir / "both.txt").write_text(get_text_hexdigest(done)) # no record carries this one at all (md5_dir / "gone.txt").write_text("orphaned") return source def test_migrate_checksums(legacy_md5_dstore): """the ones that can be attributed are renamed and the rest reported""" dstore = DataStoreDirectory(legacy_md5_dstore, suffix="fasta", mode=OVERWRITE) got = dstore.migrate_checksums() assert got == { "migrated": 2, "ambiguous": ["both"], "orphaned": ["gone"], "superseded": [], } md5_dir = legacy_md5_dstore / MD5_TABLE assert (md5_dir / f"solo_done.{COMPLETED_CHECKSUM}").exists() assert (md5_dir / f"solo_failed.{NOT_COMPLETED_CHECKSUM}").exists() assert sorted(p.name for p in md5_dir.glob("*.txt")) == ["both.txt", "gone.txt"] def test_migrate_checksums_is_idempotent(legacy_md5_dstore): """running it again finds only what it could not attribute""" dstore = DataStoreDirectory(legacy_md5_dstore, suffix="fasta", mode=OVERWRITE) first = dstore.migrate_checksums() got = dstore.migrate_checksums() assert got["migrated"] == 0 # it has not forgotten what it could not do assert got["ambiguous"] == first["ambiguous"] assert got["orphaned"] == first["orphaned"] def test_migrate_checksums_reads_past_the_limit(legacy_md5_dstore): """a limited view does not make a record look absent""" # limit truncates the member lists, and a stem seen in neither would # then be attributed to whichever kind was in view, or called orphaned dstore = DataStoreDirectory( legacy_md5_dstore, suffix="fasta", mode=OVERWRITE, limit=1, ) got = dstore.migrate_checksums() assert got["ambiguous"] == ["both"] assert got["orphaned"] == ["gone"] assert got["migrated"] == 2 def test_migrate_checksums_keeps_a_checksum_already_under_the_new_name(tmp_dir): """a record whose checksum this version wrote keeps that one""" # the shared file may belong to a record since dropped, while the one # under the current name was written for this record by this version source = tmp_dir / "superseded" (source / MD5_TABLE).mkdir(parents=True) dstore = DataStoreDirectory(source, suffix="fasta", mode=OVERWRITE) data = ">s\nACGT\n" dstore.write(unique_id="rec.fasta", data=data) (source / MD5_TABLE / "rec.txt").write_text("stale") got = dstore.migrate_checksums() assert got["superseded"] == ["rec"] assert got["migrated"] == 0 assert dstore.md5("rec.fasta") == get_text_hexdigest(data) def test_migrate_checksums_keeps_the_checksums_readable(legacy_md5_dstore): """a migrated record reports the same checksum it did before""" dstore = DataStoreDirectory(legacy_md5_dstore, suffix="fasta", mode=OVERWRITE) before = dstore.md5("solo_done.fasta") dstore.migrate_checksums() assert dstore.md5("solo_done.fasta") == before @pytest.mark.parametrize("mode", [READONLY, APPEND]) def test_migrate_checksums_needs_write_mode(legacy_md5_dstore, mode): """migrating rewrites records already there, so it takes mode w""" # read only cannot rewrite anything, and append undertakes not to touch # what is already in the store, which is exactly what this does dstore = DataStoreDirectory(legacy_md5_dstore, suffix="fasta", mode=mode) with pytest.raises(OSError, match='mode="w"'): dstore.migrate_checksums() assert (legacy_md5_dstore / MD5_TABLE / "solo_done.txt").exists() def test_validate_counts_checksums_in_the_older_layout(tmp_dir): """a store says how many of its checksums are still unattributed""" # so a caller learns there is migrating to do without having to run it source = tmp_dir / "counted" (source / MD5_TABLE).mkdir(parents=True) data = ">s\nACGT\n" (source / "id_0.fasta").write_text(data) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(data)) dstore = DataStoreDirectory(source, suffix="fasta", mode=READONLY) assert dstore._validate()["md5_legacy"] == 1 def test_validate_counts_no_legacy_checksums_in_a_new_store(w_dstore): """a store written since the rename has none of them""" w_dstore.write(unique_id="id_0.fasta", data=">s\nACGT\n") assert w_dstore._validate()["md5_legacy"] == 0 def test_validate_counts_legacy_checksums_in_an_archive(tmp_dir): """an archive reports them too, though it can never migrate them""" source = tmp_dir / "zipcounted" (source / MD5_TABLE).mkdir(parents=True) data = ">s\nACGT\n" (source / "id_0.fasta").write_text(data) (source / MD5_TABLE / "id_0.txt").write_text(get_text_hexdigest(data)) path = shutil.make_archive( base_name=str(source.parent / source.name), format="zip", base_dir=source.name, root_dir=source.parent, ) dstore = ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") assert dstore._validate()["md5_legacy"] == 1 def test_close_a_directory_store(w_dstore): """a directory store can be closed, and holds nothing back afterwards""" # it exists so a caller can close whatever open_data_store returned # without asking which backend it got. a directory store holds no # connection and no lock, so there is nothing for closing to end w_dstore.write(unique_id="c1.fasta", data=">s\nACGT\n") w_dstore.close() w_dstore.close() assert w_dstore.read("c1.fasta") == ">s\nACGT\n" assert [m.unique_id for m in w_dstore.completed] == ["c1.fasta"] def test_close_a_zipped_store(zipped_basic): """a read only zip store can be closed too""" dstore = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") dstore.close() assert len(dstore.completed) > 0 def test_write_read_only_datastore(ro_dstore): with pytest.raises(IOError): ro_dstore.write(unique_id="brca1.fasta", data="test data") def test_write(fasta_dir, w_dstore): """correctly write content""" expect = Path(fasta_dir / "brca1.fasta").read_text() identifier = "brca1.fasta" w_dstore.write(unique_id=identifier, data=expect) got = w_dstore.read(identifier) assert got == expect def test_multi_write(fasta_dir, w_dstore): """correctly write multiple files to data store""" expect_a = Path(fasta_dir / "brca1.fasta").read_text() expect_b = Path(fasta_dir / "primates_brca1.fasta").read_text() identifier_a = "brca2.fasta" identifier_b = "primates_brca2.fasta" w_dstore.write(unique_id=identifier_a, data=expect_a) w_dstore.write(unique_id=identifier_b, data=expect_b) got_a = w_dstore.read(identifier_a) got_b = w_dstore.read(identifier_b) assert got_a == expect_a assert got_b == expect_b def test_append(w_dstore): """correctly write content""" identifier = "test1.fasta" data = "test data" w_dstore.write(unique_id=identifier, data=data) got = w_dstore.read(identifier) assert got == data class _CountingMembersStore(DataStoreDirectory): """counts how often the member list gets built""" members_built = 0 @property def members(self): self.members_built += 1 return super().members @pytest.mark.parametrize("summary", [lambda d: d.validate(), str]) def test_summaries_build_the_member_list_once(write_dir, summary): """a summary works from one member list rather than rebuilding it""" # each build is a fresh concatenation of the two cached halves, so two # of them taken either side of a write disagree about what is held dstore = _CountingMembersStore(write_dir, suffix="fasta", mode=OVERWRITE) for i in range(3): dstore.write(unique_id=f"c{i}.fasta", data=f">s{i}\nACGT\n") record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") dstore.write_not_completed(unique_id="nc1", data=record.to_json()) dstore.members_built = 0 summary(dstore) assert dstore.members_built == 1 def test_no_not_completed_subdir(nc_dstore): expect = f"{len(nc_dstore.completed) + len(nc_dstore.not_completed)}x member" assert str(nc_dstore).startswith(expect) nc_dstore.drop_not_completed() not_dir = nc_dstore.source / NOT_COMPLETED_TABLE assert list(not_dir.glob("*.json")) == [] expect = f"{len(nc_dstore.completed)}x member" assert str(nc_dstore).startswith(expect) expect = f"{len(nc_dstore)}x member" assert str(nc_dstore).startswith(expect) assert len(nc_dstore) == len(nc_dstore.completed) def test_limit_datastore(nc_dstore): assert len(nc_dstore) == len(nc_dstore.completed) + len(nc_dstore.not_completed) nc_dstore._limit = len(nc_dstore.completed) // 2 nc_dstore._completed = [] nc_dstore._not_completed = [] assert len(nc_dstore.completed) == len(nc_dstore.not_completed) == nc_dstore.limit assert len(nc_dstore) == len(nc_dstore.completed) + len(nc_dstore.not_completed) nc_dstore.drop_not_completed() assert len(nc_dstore) == len(nc_dstore.completed) assert len(nc_dstore.not_completed) == 0 nc_dstore._limit = len(nc_dstore.completed) // 2 nc_dstore._completed = [] nc_dstore._not_completed = [] assert len(nc_dstore) == len(nc_dstore.completed) == nc_dstore.limit assert len(nc_dstore.not_completed) == 0 def test_md5_sum(nc_dstore): for m in nc_dstore.members: data = m.read() md5 = nc_dstore.md5(m.unique_id) assert md5 == get_text_hexdigest(data) def test_md5_none(fasta_dir): dstore = DataStoreDirectory(fasta_dir, suffix="fasta") for m in dstore.members: assert m.md5 is None def test_md5_missing(nc_dstore): assert nc_dstore.md5("unknown") is None def test_write_if_member_exists(full_dstore, write_dir): """correctly write content""" expect = Path(write_dir / "brca1.fasta").read_text() identifier = "brca1.fasta" len_dstore = len(full_dstore) full_dstore.write(unique_id=identifier, data=expect) assert len_dstore == len(full_dstore) got = full_dstore.read(identifier) assert got == expect full_dstore._mode = OVERWRITE full_dstore.write(unique_id=identifier, data=expect) assert len_dstore == len(full_dstore) got = full_dstore.read(identifier) assert got == expect def test_write_success_replaces_not_completed(full_dstore): """correctly write content""" nc = full_dstore.not_completed[0].unique_id data = full_dstore.completed[0].read() new_id = Path(nc.replace(".json", f".{full_dstore.suffix}")).name num = len(full_dstore) full_dstore.write(unique_id=new_id, data=data) assert len(full_dstore) == num @pytest.mark.parametrize("klass", [str, Path]) def test_get_data_source_attr(klass): """handles case where input has source attribute string object or pathlib object""" class dummy: source = None obj = dummy() value = klass("some/path.txt") obj.source = value got = get_data_source(obj) assert got == "path.txt" @pytest.mark.parametrize( "name", ["path/name.txt", "path/name.gz", "path/name.fasta.gz", "name.fasta.gz"], ) def test_get_unique_id(name): got = get_unique_id(name) assert got == "name" def test_get_unique_id_none(): got = get_unique_id(None) assert got is None @pytest.mark.parametrize( ("name", "expect"), [ ("(A:0.1,B:0.2);", "(A:0.1,B:0"), ("result (see fig. 2)", "result (see fig"), ("v1.0 [draft]", "v1"), ("counts.*", "counts"), ], ) def test_get_unique_id_suffix_is_text_not_a_pattern(name, expect): """the trailing suffix is stripped literally, never compiled""" assert get_unique_id(name) == expect def test_set_id_from_source_returns_default_initially( reset_id_from_source: None, ) -> None: """Default extractor is `get_unique_id` when nothing is registered.""" assert get_id_from_source() is get_unique_id def test_set_id_from_source_registers_and_clears( reset_id_from_source: None, ) -> None: """A registered function replaces the default; None restores it.""" def my_extractor(obj: object) -> str | None: return f"custom-{obj}" set_id_from_source(my_extractor) assert get_id_from_source() is my_extractor assert get_id_from_source()("foo") == "custom-foo" set_id_from_source(None) assert get_id_from_source() is get_unique_id @pytest.mark.parametrize("data", [{}, set(), {"info": {}}]) def test_get_data_source_none(data): assert get_data_source(data) is None def test_load_record_from_json(): """handle different types of input""" orig = {"data": "blah", "identifier": "some.json", "completed": True} data = orig.copy() data2 = data.copy() data2["data"] = json.dumps(data) for d in (data, json.dumps(data), data2): expected = "blah" if d != data2 else json.loads(data2["data"]) id_, data_, compl = load_record_from_json(d) assert id_ == "some.json" assert data_ == expected assert compl is True def test_zipped_ro_fail(zipped_basic): with pytest.raises(ValueError): ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta", mode="w") def test_zipped_ro_ioerror(): with pytest.raises(IOError): ReadOnlyDataStoreZipped("blah-1234.zip", suffix="fasta") def test_zipped_ro_basic(zipped_basic, ro_dstore): dstore = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") assert len(dstore.completed) == len(ro_dstore.completed) assert len(dstore) == len(ro_dstore) expect = _get_member_data(ro_dstore.completed) got = _get_member_data(dstore.completed) assert expect == got expect = _get_member_data(ro_dstore.not_completed) got = _get_member_data(dstore.not_completed) assert expect == got def test_zipped_ro_basic_hidden(zipped_hidden, zipped_basic): orig = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") dstore = ReadOnlyDataStoreZipped(zipped_hidden, suffix="fasta") assert len(dstore) == len(orig) assert all(not m.unique_id.startswith(".") for m in dstore) def test_zipped_ro_full(zipped_full, full_dstore): got_ids = {m.unique_id for m in zipped_full.completed} expect_ids = {m.unique_id for m in full_dstore.completed} assert got_ids == expect_ids got_ids = {m.unique_id for m in zipped_full.not_completed} expect_ids = {m.unique_id for m in full_dstore.not_completed} assert got_ids == expect_ids assert len(zipped_full) == len(full_dstore) expect = _get_member_data(full_dstore.completed) got = _get_member_data(zipped_full.completed) assert expect == got expect = _get_member_data(full_dstore.not_completed) got = _get_member_data(zipped_full.not_completed) assert expect == got def test_zipped_logs(zipped_full, full_dstore): assert len(zipped_full.logs) == len(full_dstore.logs) expect = _get_member_data(full_dstore.logs) got = _get_member_data(zipped_full.logs) assert expect == got def test_zipped_md5(zipped_full, full_dstore): expect = {m.unique_id: full_dstore.md5(m.unique_id) for m in full_dstore.completed} got = {m.unique_id: zipped_full.md5(m.unique_id) for m in zipped_full.completed} assert got == expect def test_write_citations_directory(write_dir, sample_citations): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) path = write_dir / CITATIONS_FILE assert path.exists() loaded = dstore._load_citations() assert len(loaded) == 2 assert loaded[0].title == "Tool One" assert loaded[1].title == "Tool Two" def test_write_citations_empty_directory(write_dir): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=()) path = write_dir / CITATIONS_FILE assert not path.exists() def test_write_bib_directory(write_dir, sample_citations): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) bib_path = write_dir / "refs.bib" dstore.write_bib(bib_path) assert bib_path.exists() content = bib_path.read_text() assert "Tool One" in content assert "Tool Two" in content def test_write_bib_no_citations(write_dir): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) bib_path = write_dir / "refs.bib" with pytest.warns(UserWarning, match="No citations stored"): dstore.write_bib(bib_path) assert not bib_path.exists() def test_load_citations_no_file(write_dir): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) assert dstore._load_citations() == [] def test_load_citations_zipped(write_dir, sample_citations): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) source = pathlib.Path(dstore.source) path = shutil.make_archive( base_name=str(source.parent / source.name), format="zip", base_dir=source.name, root_dir=source.parent, ) zipped = ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") loaded = zipped._load_citations() assert len(loaded) == 2 assert loaded[0].title == "Tool One" def test_citations_file_not_in_completed(write_dir, sample_citations): """The bibliography.citations file must not appear in the completed members list.""" dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write(unique_id="sample.fasta", data=">s1\nACGT\n") dstore.write_citations(data=sample_citations) assert (write_dir / CITATIONS_FILE).exists() dstore._completed = [] member_ids = {m.unique_id for m in dstore.completed} assert CITATIONS_FILE not in member_ids assert "sample.fasta" in member_ids @pytest.fixture def _restore_display(): """Ensure the global display function is reset after each test.""" yield set_summary_display(None) def test_summary_display_default_is_none(_restore_display): assert get_summary_display() is None def test_summary_display_set_and_get(_restore_display): def my_display(data, *, name=""): return data set_summary_display(my_display) assert get_summary_display() is my_display def test_summary_display_set_none_clears(_restore_display): set_summary_display(lambda data, **kw: data) set_summary_display(None) assert get_summary_display() is None def test_describe_without_display(ro_dstore, _restore_display): result = ro_dstore.describe assert isinstance(result, dict) assert "completed" in result def test_describe_with_display(ro_dstore, _restore_display): captured = {} def display(data, *, name=""): captured["data"] = data captured["name"] = name return f"DISPLAY:{name}" set_summary_display(display) result = ro_dstore.describe assert result == "DISPLAY:describe" assert isinstance(captured["data"], dict) assert "completed" in captured["data"] assert captured["name"] == "describe" def test_summary_logs_with_display(full_dstore, _restore_display): captured = {} def display(data, *, name=""): captured["data"] = data captured["name"] = name return "transformed" set_summary_display(display) result = full_dstore.summary_logs assert result == "transformed" assert captured["name"] == "summary_logs" assert isinstance(captured["data"], list) def test_validate_with_display(ro_dstore, _restore_display): captured = {} def display(data, *, name=""): captured["name"] = name return "validated" set_summary_display(display) result = ro_dstore.validate() assert result == "validated" assert captured["name"] == "validate" def test_protected_methods_bypass_display(ro_dstore, _restore_display): set_summary_display(lambda data, **kw: "SHOULD_NOT_SEE") assert isinstance(ro_dstore._describe(), dict) assert isinstance(ro_dstore._summary_logs(), list) assert isinstance(ro_dstore._summary_not_completed(), list) assert isinstance(ro_dstore._validate(), dict) def test_summary_citations_with_display(write_dir, sample_citations, _restore_display): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) captured = {} def display(data, *, name=""): captured["name"] = name captured["data"] = data return "citations_display" set_summary_display(display) result = dstore.summary_citations assert result == "citations_display" assert captured["name"] == "summary_citations" assert isinstance(captured["data"], list) def test_validate_incorrect_md5(write_dir): dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) dstore.write(unique_id="item.txt", data="original") # corrupt the md5 md5_path = write_dir / MD5_TABLE / f"item.{COMPLETED_CHECKSUM}" md5_path.write_text("wrong_md5_value") result = dstore._validate() assert result["md5_incorrect"] == 1 def test_readonly_nonexistent_dir(tmp_path): with pytest.raises(OSError, match="does not exist"): DataStoreDirectory(tmp_path / "nonexistent", suffix="txt", mode=READONLY) def test_not_completed_with_limit(write_dir): dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) for i in range(5): nc = NotCompleted(NotCompletedType.ERROR, "test", f"msg {i}", source=f"src_{i}") dstore.write_not_completed(unique_id=f"nc_{i}.json", data=nc.to_json()) limited = DataStoreDirectory(write_dir, suffix="txt", mode=READONLY, limit=2) assert len(limited.not_completed) == 2 def test_summary_not_completeds(write_dir): dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) for i in range(3): nc = NotCompleted( NotCompletedType.ERROR, "myapp", f"error msg {i}", source=f"s{i}" ) dstore.write_not_completed(unique_id=f"nc_{i}.json", data=nc.to_json()) rows = summary_not_completeds(dstore.not_completed) assert len(rows) >= 1 assert rows[0]["origin"] == "myapp" assert rows[0]["num"] == 3 def test_make_record_for_json(): result = make_record_for_json("id1", {"key": "value"}, True) assert result["identifier"] == "id1" assert result["completed"] is True assert isinstance(result["data"], str) parsed = json.loads(result["data"]) assert parsed == {"key": "value"} def test_make_record_for_json_with_rich_dict(): class FakeObj: def to_rich_dict(self): return {"type": "fake", "data": 42} result = make_record_for_json("id2", FakeObj(), True) parsed = json.loads(result["data"]) assert parsed == {"type": "fake", "data": 42} def test_zipped_readonly_write_methods(tmp_path): src = tmp_path / "src" src.mkdir() (src / "a.txt").write_text("data") zpath = shutil.make_archive(str(src), "zip", root_dir=src.parent, base_dir=src.name) zstore = ReadOnlyDataStoreZipped(zpath, suffix="txt") with pytest.raises(TypeError): zstore.write(unique_id="x", data="d") with pytest.raises(TypeError): zstore.write_not_completed(unique_id="x", data="d") with pytest.raises(TypeError): zstore.write_log(unique_id="x", data="d") with pytest.raises(TypeError): zstore.write_citations(data=()) with pytest.raises(TypeError): zstore.drop_not_completed() def test_zipped_md5_returns_none(tmp_path): src = tmp_path / "src" src.mkdir() (src / "a.txt").write_text("data") zpath = shutil.make_archive(str(src), "zip", root_dir=src.parent, base_dir=src.name) zstore = ReadOnlyDataStoreZipped(zpath, suffix="txt") assert zstore.md5("a.txt") is None def test_zipped_load_citations_missing(tmp_path): src = tmp_path / "src" src.mkdir() (src / "a.txt").write_text("data") zpath = shutil.make_archive(str(src), "zip", root_dir=src.parent, base_dir=src.name) zstore = ReadOnlyDataStoreZipped(zpath, suffix="txt") assert zstore._load_citations() == [] def test_zipped_completed_with_limit(tmp_path): src = tmp_path / "src" src.mkdir() for i in range(5): (src / f"f_{i}.txt").write_text(f"data {i}") zpath = shutil.make_archive(str(src), "zip", root_dir=src.parent, base_dir=src.name) zstore = ReadOnlyDataStoreZipped(zpath, suffix="txt", limit=2) assert len(zstore.completed) == 2 def test_summary_logs_continuation_line(write_dir): from scitrack import CachingLogger dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) logger = CachingLogger(create_dir=True) log_path = write_dir / "test.log" logger.log_file_path = str(log_path) logger.log_message("a long message\nthat continues", label="multi") logger.shutdown() dstore.write_log(unique_id="test.log", data=log_path.read_text()) rows = dstore._summary_logs() assert len(rows) == 1 def test_data_member_str(ro_dstore): member = ro_dstore[0] assert str(member) == member.unique_id def test_data_member_repr(ro_dstore): member = ro_dstore[0] r = repr(member) assert "DataMember" in r assert member.unique_id in r def test_datastore_repr(ro_dstore): r = repr(ro_dstore) assert "DataStoreDirectory" in r assert "source=" in r def test_contains_non_string(ro_dstore): assert 42 not in ro_dstore def test_write_not_completed_readonly(ro_dstore): with pytest.raises(OSError, match="readonly"): ro_dstore.write_not_completed(unique_id="x", data="d") def test_write_log_readonly(ro_dstore): with pytest.raises(OSError, match="readonly"): ro_dstore.write_log(unique_id="x", data="d") def test_summary_logs_malformed_continuation(): from unittest.mock import MagicMock log_text = "2024-01-01\t00:00:00\n\tcontinuation without key" member = MagicMock() member.read.return_value = log_text member.unique_id = "bad.log" class FakeDS(DataStoreDirectory): @property def logs(self): return [member] fake = FakeDS.__new__(FakeDS) fake._completed = [] fake._not_completed = [] fake._init_vals = {} with pytest.raises(ValueError, match="malformed log data"): fake._summary_logs() def test_tidy_and_check_suffix_empty(): from scinexus.data_store import _tidy_and_check_suffix with pytest.raises(ValueError, match="suffix is required"): _tidy_and_check_suffix(None) with pytest.raises(ValueError, match="suffix is required"): _tidy_and_check_suffix("*") with pytest.raises(ValueError, match="suffix is required"): _tidy_and_check_suffix(".**") def test_summary_not_completeds_with_bytes(): from unittest.mock import MagicMock members = [] for i in range(3): m = MagicMock() m.read.return_value = b"binary data" m.unique_id = f"item_{i}" members.append(m) result = summary_not_completeds(members) assert result == [] def test_summary_not_completeds_with_deserialise(write_dir): dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) from scinexus.composable import NotCompleted, NotCompletedType for i in range(3): nc = NotCompleted( NotCompletedType.ERROR, "myapp", f"error msg {i}", source=f"s{i}" ) dstore.write_not_completed(unique_id=f"deser_{i}.json", data=nc.to_json()) rows = summary_not_completeds(dstore.not_completed, deserialise=lambda x: x) assert len(rows) >= 1 def test_summary_not_completeds_long_sources(write_dir): dstore = DataStoreDirectory(write_dir, suffix="txt", mode=OVERWRITE) from scinexus.composable import NotCompleted, NotCompletedType for i in range(10): long_source = f"very_long_source_name_for_item_{i}_padding" nc = NotCompleted( NotCompletedType.ERROR, "myapp", f"error msg {i}", source=long_source, ) dstore.write_not_completed(unique_id=f"long_{i}.json", data=nc.to_json()) rows = summary_not_completeds(dstore.not_completed) assert len(rows) >= 1 for row in rows: if len(row["source"]) > 45: assert row["source"].endswith("...") def test_get_data_source_data_member(ro_dstore): member = ro_dstore[0] result = get_data_source(member) assert result == member.unique_id def test_zipped_not_completed_with_limit(tmp_path): from scinexus.composable import NotCompleted, NotCompletedType src = tmp_path / "src" src.mkdir() nc_dir = src / "not_completed" nc_dir.mkdir() (src / "a.txt").write_text("data") for i in range(5): nc = NotCompleted(NotCompletedType.ERROR, "test", f"msg {i}", source=f"s{i}") (nc_dir / f"nc_{i}.json").write_text(nc.to_json()) zpath = shutil.make_archive(str(src), "zip", root_dir=src.parent, base_dir=src.name) zstore = ReadOnlyDataStoreZipped(zpath, suffix="txt", limit=2) assert len(zstore.not_completed) == 2 def test_zipped_mode_property(zipped_basic): zstore = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") assert zstore.mode is READONLY def _make_minimal_ds(mode=OVERWRITE): from scinexus.data_store import DataStoreABC class MinimalDS(DataStoreABC): @property def source(self): return "test" @property def mode(self): return mode @property def limit(self): return None def read(self, unique_id): return "" def write(self, *, unique_id, data): super().write(unique_id=unique_id, data=data) def write_not_completed(self, *, unique_id, data): super().write_not_completed(unique_id=unique_id, data=data) def write_log(self, *, unique_id, data): super().write_log(unique_id=unique_id, data=data) @property def logs(self): return [] @property def completed(self): return [] @property def not_completed(self): return [] def drop_not_completed(self, *, unique_id=None): pass def md5(self, unique_id): return None return MinimalDS() def test_base_write_citations_warns(): ds = _make_minimal_ds() with pytest.warns(UserWarning, match="does not support saving citations"): ds.write_citations(data=(object(),)) def test_base_summary_citations_warns(): ds = _make_minimal_ds() with pytest.warns(UserWarning, match="does not support saving citations"): result = ds._summary_citations() assert result == [] def test_base_load_citations_returns_empty(): ds = _make_minimal_ds() assert ds._load_citations() == [] def test_base_write_not_completed_readonly(): ds = _make_minimal_ds(mode=READONLY) with pytest.raises(OSError, match="readonly"): ds.write_not_completed(unique_id="x", data="d") def test_base_write_log_readonly(): ds = _make_minimal_ds(mode=READONLY) with pytest.raises(OSError, match="readonly"): ds.write_log(unique_id="x", data="d") def test_base_write_citations_empty_data(): ds = _make_minimal_ds() ds.write_citations(data=()) @pytest.mark.mpi def test_source_check_create_not_master(tmp_path): from unittest.mock import patch from scinexus import data_store as ds_mod target = tmp_path / "should_not_exist" with patch.object(ds_mod, "is_master_process", return_value=False): dstore = DataStoreDirectory(target, suffix="txt", mode=OVERWRITE) assert not target.exists() assert dstore.source == target def test_write_read_not_completed(nc_dstore): nc_dstore.drop_not_completed() assert len(nc_dstore.not_completed) == 0 nc = NotCompleted("ERROR", "test", "for tracing", source="blah") writer = c3.get_app("write_seqs", data_store=nc_dstore) writer.main(nc, identifier="blah") assert len(nc_dstore.not_completed) == 1 got = nc_dstore.not_completed[0].read() assert nc.to_json() == got def test_summary_logs_missing_field(nc_dstore): log_path = Path(nc_dstore.source) / nc_dstore.logs[0].unique_id data = [ l for l in log_path.read_text().splitlines() if "composable function" not in l ] log_path.write_text("\n".join(data)) # doesn't fail because of a missing field in the log data assert isinstance(nc_dstore.summary_logs, list) @pytest.fixture def app_dstore_in(tmp_path): pytest.importorskip("cogent3") in_path = tmp_path / "in_data" in_path.mkdir(parents=True) fasta_content = ">seq\nACGT" with open(in_path / "one.fa", "w") as file: file.write(fasta_content) dstore_in = open_data_store(in_path, suffix=".fa", mode="r") dstore_out = open_data_store(tmp_path / "data_out", suffix="fa", mode="w") loader = c3.get_app("load_unaligned") writer = c3.get_app("write_seqs", dstore_out) pipe = loader + writer return pipe, dstore_in def test_write_multiple_times_apply_to(app_dstore_in): app, dstore_in = app_dstore_in app.apply_to(dstore_in) orig_length = len(app.data_store) app.apply_to(dstore_in) assert len(app.data_store) == orig_length def test_directory_data_store_write_compressed(tmp_path): out = open_data_store(base_path=tmp_path / "demo", suffix="fa.gz", mode="w") writer = c3.get_app("write_seqs", data_store=out) seqs = c3.make_aligned_seqs( {"s1": "CG--T", "s2": "CGTTT"}, moltype="dna", info={"source": "test"}, ) got = writer(seqs) # pylint: disable=not-callable assert got, got def test_apply_to_not_completed(nc_dstore, tmp_path): loader = c3.get_app("load_unaligned") num_seqs = c3.get_app("take_n_seqs", number=3, fixed_choice=False) out_dstore = open_data_store(tmp_path / "output", suffix="fa", mode="w") writer = c3.get_app("write_seqs", data_store=out_dstore, format_name="fasta") app = loader + num_seqs + writer fini = app.apply_to(nc_dstore) assert 0 < len(fini.completed) <= len(nc_dstore.completed) def test_summary_citations_directory(write_dir, sample_citations): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) cited = dstore.summary_citations assert isinstance(cited, list) assert len(cited) == 2 assert "app" in cited[0] assert "citation" in cited[0] def test_write_bib_tilde_path(write_dir, sample_citations, HOME_TMP_DIR): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) bib_path = f"~/{HOME_TMP_DIR.name}/refs.bib" dstore.write_bib(bib_path) expected = pathlib.Path(bib_path).expanduser() assert expected.exists() content = expected.read_text() assert "Tool One" in content assert "Tool Two" in content def test_summary_citations_zipped(write_dir, sample_citations): dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) dstore.write_citations(data=sample_citations) source = pathlib.Path(dstore.source) path = shutil.make_archive( base_name=str(source), format="zip", base_dir=source.name, root_dir=source.parent, ) zipped = ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") cited = zipped.summary_citations assert isinstance(cited, list) assert len(cited) == 2 def test_write_citations_zipped_raises(zipped_basic): zipped = ReadOnlyDataStoreZipped(zipped_basic, suffix="fasta") with pytest.raises(TypeError, match="read only"): zipped.write_citations(data=(None,)) def test_old_directory_store_without_citations(fasta_dir): """Opening a directory store created before citations were added works.""" # fasta_dir has .fasta files but no .citations file dstore = DataStoreDirectory(fasta_dir, suffix="fasta", mode=READONLY) assert dstore._load_citations() == [] cited = dstore.summary_citations assert isinstance(cited, list) assert len(cited) == 0 _dict_types = [dict] if UnionDict is not None: _dict_types.append(UnionDict) _types = tuple(product(_dict_types, (str, Path))) @pytest.mark.parametrize(("container_type", "source_stype"), _types) def test_get_data_source_dict(container_type, source_stype): """handles case where input is dict (sub)class instance with top level source key""" value = source_stype("some/path.txt") data = container_type(source=value) got = get_data_source(data) assert got == "path.txt" @pytest.mark.parametrize("klass", [str, Path]) def test_get_data_source_seqcoll(klass): """handles case where input is sequence collection object""" from cogent3 import make_unaligned_seqs value = klass("some/path.txt") obj = make_unaligned_seqs( {"seq1": "ACGG"}, moltype="dna", info={"random_key": 1234}, source=value, ) got = get_data_source(obj) assert got == "path.txt" _CONCURRENT_MEMBERS = 300 _CONCURRENT_READERS = 8 _CONCURRENT_ROUNDS = 20 _WRITE_ROUNDS = 40 _MOVED_RECORDS = 40 # generous: it only has to exceed a scan, and a hang here should fail not stall _SYNC_TIMEOUT = 30 @pytest.fixture def many_member_dir(tmp_dir): """a store directory holding enough members to widen the scan window""" source = Path(tmp_dir) / "many_members" (source / NOT_COMPLETED_TABLE).mkdir(parents=True, exist_ok=True) for i in range(_CONCURRENT_MEMBERS): (source / f"id_{i}.fasta").write_text(f">seq_{i}\nACGT\n") nc = NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"id_{i}" ) (source / NOT_COMPLETED_TABLE / f"id_{i}.json").write_text(nc.to_json()) yield source shutil.rmtree(source, ignore_errors=True) @pytest.fixture(params=["directory", "zipped"]) def many_member_store(request, many_member_dir): """the same members as a directory store and as a zipped store""" if request.param == "directory": return DataStoreDirectory(many_member_dir, suffix="fasta", mode=READONLY) path = shutil.make_archive( base_name=str(many_member_dir.parent / many_member_dir.name), format="zip", base_dir=many_member_dir.name, root_dir=many_member_dir.parent, ) return ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") def _count_members(dstore, attr, barrier, _): barrier.wait(timeout=_SYNC_TIMEOUT) return len(getattr(dstore, attr)) @pytest.mark.parametrize("attr", ["completed", "not_completed"]) def test_member_list_is_whole_for_concurrent_readers(many_member_store, attr): """concurrent readers each see every member, never a partial scan The scan must not publish its backing list until the list is complete. Filling the list in place instead makes it truthy from the first append onwards, so a second thread arriving mid-scan fails the emptiness guard, takes the early return, and is handed the list while it is still being filled. It then sees a short count and nothing raises. """ private = f"_{attr}" observed = set() old_interval = sys.getswitchinterval() sys.setswitchinterval(1e-6) try: for _ in range(_CONCURRENT_ROUNDS): setattr(many_member_store, private, []) barrier = threading.Barrier(_CONCURRENT_READERS, timeout=_SYNC_TIMEOUT) read = functools.partial(_count_members, many_member_store, attr, barrier) with ThreadPoolExecutor(max_workers=_CONCURRENT_READERS) as executor: observed.update(executor.map(read, range(_CONCURRENT_READERS))) finally: sys.setswitchinterval(old_interval) assert observed == {_CONCURRENT_MEMBERS} class _PausingWriteStore(DataStoreDirectory): """holds a writer between the file landing on disk and the cache append ``_write`` puts the file on disk and ``write`` records the member in the cached list afterwards. Pausing in that gap makes the window a test can drive deterministically rather than one it has to race for. """ def _write(self, **kwargs): member = super()._write(**kwargs) self.file_on_disk.set() self.reader_published.wait(timeout=_SYNC_TIMEOUT) return member def test_concurrent_write_is_recorded_once(write_dir): """a member written while a reader scans is recorded exactly once A fresh store's first write is the exposed case: the scan legitimately finds nothing, so the emptiness guard stays open. A reader entering after the file lands on disk but before the cache append therefore scans, finds the new file, and publishes it -- and the append then records the same member a second time. """ dstore = _PausingWriteStore(write_dir, suffix="fasta", mode=OVERWRITE) dstore.file_on_disk = threading.Event() dstore.reader_published = threading.Event() def read_once(): assert dstore.file_on_disk.wait(timeout=_SYNC_TIMEOUT) len(dstore.completed) dstore.reader_published.set() with ThreadPoolExecutor(max_workers=1) as executor: reader = executor.submit(read_once) dstore.write(unique_id="brand_new.fasta", data=">new\nACGT\n") reader.result(timeout=_SYNC_TIMEOUT) assert [m.unique_id for m in dstore.completed] == ["brand_new.fasta"] class _PausingNotCompletedStore(DataStoreDirectory): """holds a not-completed write between the mkdir and the file landing ``write_not_completed`` makes the directory and ``_write`` then opens the file in it. Pausing between the two makes the window a test can drive rather than one it has to race for. """ def _write(self, **kwargs): self.writer_paused.set() self.release_writer.wait(timeout=_SYNC_TIMEOUT) return super()._write(**kwargs) def test_drop_during_a_not_completed_write_leaves_it_somewhere_to_write(write_dir): """a drop running mid-write leaves the directory the write is opening in""" dstore = _PausingNotCompletedStore(write_dir, suffix="fasta", mode=OVERWRITE) dstore.writer_paused = threading.Event() dstore.release_writer = threading.Event() record = NotCompleted(NotCompletedType.ERROR, "location", "message", source="nc1") with ThreadPoolExecutor(max_workers=1) as executor: writer = executor.submit( dstore.write_not_completed, unique_id="nc1", data=record.to_json(), ) try: assert dstore.writer_paused.wait(timeout=_SYNC_TIMEOUT) # the whole drop runs in the window between the writer making # the directory and opening its file in it dstore.drop_not_completed() finally: dstore.release_writer.set() member = writer.result(timeout=_SYNC_TIMEOUT) assert member.unique_id == f"{NOT_COMPLETED_TABLE}/nc1.json" assert (write_dir / NOT_COMPLETED_TABLE / "nc1.json").exists() def test_drop_not_completed_twice(nc_dstore): """dropping an already emptied store is not an error""" nc_dstore.drop_not_completed() nc_dstore.drop_not_completed() assert nc_dstore.not_completed == [] def test_a_write_never_hides_the_record_it_moves(write_dir): """a record moving from not_completed to completed stays countable""" dstore = DataStoreDirectory(write_dir, suffix="fasta", mode=OVERWRITE) for i in range(_MOVED_RECORDS): record = NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"r{i}", ) dstore.write_not_completed(unique_id=f"r{i}", data=record.to_json()) observed = set() stop = threading.Event() def count_members(): while not stop.is_set(): observed.add(len(dstore)) old_interval = sys.getswitchinterval() sys.setswitchinterval(1e-6) try: with ThreadPoolExecutor(max_workers=_CONCURRENT_READERS + 1) as executor: readers = [ executor.submit(count_members) for _ in range(_CONCURRENT_READERS) ] for i in range(_MOVED_RECORDS): dstore.write(unique_id=f"r{i}.fasta", data=f">s{i}\nACGT\n") stop.set() for reader in readers: reader.result(timeout=_SYNC_TIMEOUT) finally: sys.setswitchinterval(old_interval) # each write moves one record between the halves, so the count is # invariant. only the low side is asserted: a count above it comes from # the completed file landing before the not-completed record is # removed, which is a state the directory itself passes through assert min(observed) == _MOVED_RECORDS def _scan_completed(dstore, barrier, _): barrier.wait(timeout=_SYNC_TIMEOUT) return len(dstore.completed) def test_concurrent_write_is_not_lost_from_the_cache(many_member_dir): """a member written while readers scan stays in the cached list A reader that begins its scan before the write lands builds a list from the old directory contents. Publishing that list after the writer has appended its member drops the member from the cache while leaving it on disk, so ``unique_id in self`` then reports False for a record that exists -- which is what stops ``_check_writable`` raising in APPEND mode. """ dstore = DataStoreDirectory(many_member_dir, suffix="fasta", mode=OVERWRITE) missing = [] old_interval = sys.getswitchinterval() sys.setswitchinterval(1e-6) try: for i in range(_WRITE_ROUNDS): unique_id = f"written_{i}.fasta" dstore._completed = [] barrier = threading.Barrier(_CONCURRENT_READERS + 1, timeout=_SYNC_TIMEOUT) scan = functools.partial(_scan_completed, dstore, barrier) with ThreadPoolExecutor(max_workers=_CONCURRENT_READERS + 1) as executor: readers = [executor.submit(scan, n) for n in range(_CONCURRENT_READERS)] barrier.wait(timeout=_SYNC_TIMEOUT) dstore.write(unique_id=unique_id, data=">new\nACGT\n") for reader in readers: reader.result(timeout=_SYNC_TIMEOUT) if unique_id not in {m.unique_id for m in dstore.completed}: missing.append(unique_id) finally: sys.setswitchinterval(old_interval) assert missing == [] cogent3-scinexus-b16bc06/tests/test_deserialise.py000066400000000000000000000053771525761402500224070ustar00rootroot00000000000000import json import pytest from scinexus.composable import NotCompleted from scinexus.deserialise import ( deserialise_object, get_class, register_deserialiser, str_to_version, ) def test_deserialise_python_builtins(): """any object that does not contain a type key is returned as is""" data = {"a": 123, "b": "text"} jdata = json.dumps(data) got = deserialise_object(jdata) assert got == data data = range(4) got = deserialise_object(data) assert got is data def test_custom_deserialiser(): """correctly registers a function to inflate a custom object""" @register_deserialiser("test_myfunkydata") def astuple(data): data.pop("type") return tuple(data["data"]) orig = {"type": "test_myfunkydata", "data": [1, 2, 3]} txt = json.dumps(orig) got = deserialise_object(txt) assert got == (1, 2, 3) assert isinstance(got, tuple) with pytest.raises(TypeError): @register_deserialiser(42) def bad(data): return data def test_register_deserialiser_duplicate(): """raises ValueError for duplicate type strings""" register_deserialiser("test_unique_type_xyz")(lambda d: d) with pytest.raises(ValueError, match="already in"): register_deserialiser("test_unique_type_xyz")(lambda d: d) def test_not_completed_deserialise(): """correctly reconstructs a NotCompleted object""" val = NotCompleted("ERROR", "nothing", "some error", source="here") expect = val.to_rich_dict() j = val.to_json() got = deserialise_object(j) assert got.to_rich_dict() == expect def test_deserialise_from_file(tmp_path): """correctly deserialises from a json file""" val = NotCompleted("ERROR", "nothing", "some error", source="here") j = val.to_json() outpath = tmp_path / "test.json" outpath.write_text(j) got = deserialise_object(outpath) assert got.to_rich_dict() == val.to_rich_dict() def test_deserialise_unknown_type(): """raises NotImplementedError for unknown type""" data = {"type": "completely.unknown.Type", "data": 42} with pytest.raises(NotImplementedError, match="completely.unknown.Type"): deserialise_object(data) def test_get_class(): """correctly imports a class from provenance string""" klass = get_class("scinexus.composable.NotCompleted") assert klass is NotCompleted def test_get_class_invalid(): """raises ValueError for invalid provenance string""" with pytest.raises(ValueError, match="invalid provenance"): get_class("nodotshere") def test_str_to_version(): """correctly parses version strings""" got = str_to_version("2024.5.8a9") assert isinstance(got, tuple) assert len(got) > 0 got = str_to_version("1.2.3") assert got == () cogent3-scinexus-b16bc06/tests/test_init.py000066400000000000000000000016261525761402500210520ustar00rootroot00000000000000import pytest import scinexus from scinexus.progress import NoProgress def test_get_progress(): result = scinexus.get_progress() assert isinstance(result, NoProgress) def test_set_default_progress(): scinexus.set_progress_backend(None) def test_lazy_import_open_data_store(): assert scinexus.open_data_store is not None def test_lazy_import_set_summary_display(): assert callable(scinexus.set_summary_display) def test_lazy_import_get_summary_display(): assert callable(scinexus.get_summary_display) def test_lazy_import_nonexistent(): with pytest.raises(AttributeError, match="no attribute"): scinexus.no_such_attribute # noqa: B018 @pytest.mark.parametrize( "attr", [ "open_", "open_data_store", "set_summary_display", "get_summary_display", ], ) def test_lazy_import(attr): assert callable(getattr(scinexus, attr)) cogent3-scinexus-b16bc06/tests/test_io.py000066400000000000000000000171531525761402500205200ustar00rootroot00000000000000import bz2 import gzip import json import pathlib import pickle import shutil import pytest import scinexus from scinexus.composable import NotCompleted from scinexus.data_store import DataStoreDirectory, ReadOnlyDataStoreZipped from scinexus.deserialise import deserialise_object from scinexus.io import ( DEFAULT_DESERIALISER, DEFAULT_SERIALISER, compress, decompress, from_json, from_primitive, open_data_store, pickle_it, to_json, to_primitive, ) from scinexus.sqlite_data_store import DataStoreSqlite @pytest.fixture def tmp_dir(tmp_path_factory): return tmp_path_factory.mktemp("io") @pytest.fixture(autouse=True) def workingdir(tmp_dir, monkeypatch): monkeypatch.chdir(tmp_dir) @pytest.fixture def fasta_dir(DATA_DIR, tmp_dir): tmp_dir = pathlib.Path(tmp_dir) filenames = DATA_DIR.glob("*.fasta") fasta_dir = tmp_dir / "fasta" fasta_dir.mkdir(parents=True, exist_ok=True) for fn in filenames: dest = fasta_dir / fn.name dest.write_text(fn.read_text()) return fasta_dir @pytest.fixture def zipped_full(fasta_dir): source = fasta_dir path = shutil.make_archive( base_name=str(source), format="zip", base_dir=source, root_dir=source.parent, ) return ReadOnlyDataStoreZipped(pathlib.Path(path), suffix="fasta") def test_define_data_store(fasta_dir): """returns an iterable data store""" found = open_data_store(fasta_dir, suffix=".fasta") assert len(found) > 1 found = open_data_store(fasta_dir, suffix=".fasta", limit=2) assert len(found) == 2 found = list(open_data_store(fasta_dir, suffix=".fasta*")) assert len(found) > 2 with pytest.raises(ValueError): open_data_store(fasta_dir, suffix="*") with pytest.raises(ValueError): open_data_store(fasta_dir) with pytest.raises(TypeError): open_data_store(fasta_dir, 1) @pytest.mark.parametrize( ("serialiser", "deserialiser"), [ (json.dumps, json.loads), (pickle.dumps, pickle.loads), (lambda x: x, deserialise_object), ], ) def test_deserialiser(serialiser, deserialiser): data = {"1": 1, "abc": [1, 2]} deserialised = from_primitive(deserialiser=deserialiser) assert deserialised(serialiser(data)) == data def test_pickle_unpickle_apps(): data = {"a": [1, 2, 3]} pkld = to_primitive() + to_json() upkld = from_json() + from_primitive() assert upkld(pkld(data)) == data def test_pickle_it_unpickleable(): def foo(): ... app = pickle_it() got = app(foo) assert isinstance(got, NotCompleted) @pytest.mark.parametrize( ("comp", "decomp"), [(bz2.compress, bz2.decompress), (gzip.compress, gzip.decompress)], ) def test_compress_decompress(comp, decomp): data = pickle.dumps({"1": 1, "abc": [1, 2]}) decompressor = decompress(decompressor=decomp) compressor = compress(compressor=comp) assert decompressor(compressor(data)) == data @pytest.mark.parametrize("data", [{"a": [0, 1]}]) def test_default_serialiser_deserialiser(data): s = DEFAULT_SERIALISER(data) ds = DEFAULT_DESERIALISER(s) assert ds == data def test_to_json(): to_j = to_json() data = {"a": [0, 1]} assert to_j(data) == json.dumps(data) def test_from_json(): from_j = from_json() assert from_j('{"a": [0, 1]}') == {"a": [0, 1]} def test_to_from_json(): to_j = to_json() from_j = from_json() app = to_j + from_j data = {"a": [0, 1]} assert app(data) == data assert app(data) is not data def test_open_suffix_dirname(tmp_dir): outpath = tmp_dir / "melsubgroup_aln_flydivas_v1.2" outpath.mkdir(exist_ok=True) dstore = open_data_store(outpath, suffix="txt") assert isinstance(dstore, DataStoreDirectory) def test_open_zipped(zipped_full): got = open_data_store(zipped_full.source, mode="r", suffix="fasta") assert len(got) == len(zipped_full) assert isinstance(got, type(zipped_full)) def test_open_data_store_sqlitedb(tmp_dir): path = tmp_dir / "test.sqlitedb" dstore = open_data_store(path, mode="w") assert isinstance(dstore, DataStoreSqlite) # a writable store holds the lock from the moment it is opened dstore.close() # Tests for top-level scinexus.open_data_store def test_toplevel_open_data_store(fasta_dir): """top-level open_data_store delegates to scinexus.io""" found = scinexus.open_data_store(fasta_dir, suffix="fasta") assert isinstance(found, DataStoreDirectory) assert len(found) > 1 def test_toplevel_open_data_store_sqlitedb(tmp_dir): path = tmp_dir / "test_toplevel.sqlitedb" dstore = scinexus.open_data_store(path, mode="w") assert isinstance(dstore, DataStoreSqlite) dstore.close() # Tests for set_summary_display / get_summary_display def test_get_summary_display_default(): """default summary display is None""" orig = scinexus.get_summary_display() try: scinexus.set_summary_display(None) assert scinexus.get_summary_display() is None finally: scinexus.set_summary_display(orig) def test_set_get_summary_display(): """set then get returns the same function""" orig = scinexus.get_summary_display() try: sentinel = lambda data, *, name: data # noqa: E731 scinexus.set_summary_display(sentinel) assert scinexus.get_summary_display() is sentinel finally: scinexus.set_summary_display(orig) def test_set_summary_display_none(): """setting None clears the display function""" orig = scinexus.get_summary_display() try: scinexus.set_summary_display(lambda data, *, name: data) scinexus.set_summary_display(None) assert scinexus.get_summary_display() is None finally: scinexus.set_summary_display(orig) def test_summary_display_applied(fasta_dir): """a registered display function is called by summary properties""" orig = scinexus.get_summary_display() calls = [] def track(data, *, name): calls.append(name) return data try: scinexus.set_summary_display(track) dstore = scinexus.open_data_store(fasta_dir, suffix="fasta") _ = dstore.describe assert "describe" in calls finally: scinexus.set_summary_display(orig) def test_register_datastore_reader_duplicate_none(): from scinexus.io import _datastore_reader_map, register_datastore_reader assert None in _datastore_reader_map with pytest.raises(ValueError, match="already in"): register_datastore_reader(None) def test_register_datastore_reader_non_string(): from scinexus.io import register_datastore_reader with pytest.raises(TypeError, match="is not a string"): register_datastore_reader(123) def test_register_datastore_reader_empty_string(): from scinexus.io import register_datastore_reader with pytest.raises(ValueError, match="white-space"): register_datastore_reader("") def test_register_datastore_reader_duplicate_suffix(): from scinexus.io import register_datastore_reader with pytest.raises(ValueError, match="already in"): register_datastore_reader("zip") def test_open_data_store_unknown_suffix(tmp_dir): path = tmp_dir / "test.xyz" path.write_text("data") with pytest.raises(KeyError): open_data_store(path, suffix="fasta") def test_open_data_store_no_suffix_write_mode(tmp_dir): outpath = tmp_dir / "newdir" with pytest.raises(ValueError, match="suffix is required"): open_data_store(outpath, mode="w") def test_open_data_store_memory_readonly(): with pytest.raises(NotImplementedError, match="readonly"): open_data_store(":memory:", mode="r") cogent3-scinexus-b16bc06/tests/test_io_util.py000066400000000000000000002153461525761402500215610ustar00rootroot00000000000000import bz2 import copy import email.message import gc import gzip import io import pathlib import typing import urllib.response import warnings import zipfile from urllib.parse import urlparse import pytest import typeguard import scinexus.io_util from scinexus.composable import NotCompleted from scinexus.io_util import ( _path_relative_to_zip_parent, atomic_write, get_format_suffixes, is_url, iter_line_blocks, iter_record_chunks, iter_splitlines, open_, open_url, open_zip, path_exists, ) @pytest.fixture def tmp_dir(tmp_path_factory): return tmp_path_factory.mktemp("test_io") @pytest.fixture def home_file(DATA_DIR, HOME_TMP_DIR): """makes a temporary directory with file""" fn = "sample.tsv" contents = (DATA_DIR / fn).read_text() (HOME_TMP_DIR / fn).write_text(contents) return str(HOME_TMP_DIR / fn) @pytest.mark.parametrize("transform", [str, pathlib.Path]) def test_open_home(DATA_DIR, home_file, transform): """expands tilde for opening / writing to home""" data_path = DATA_DIR / "sample.tsv" expect = data_path.read_text() with open_(transform(home_file)) as infile: got = infile.read() assert got == expect def test_does_not_write_if_exception(tmp_dir): """file does not exist if an exception raised before closing""" test_filepath = tmp_dir / "Atomic_write_test" with pytest.raises(AssertionError), atomic_write(test_filepath, mode="w") as f: f.write("abc") raise AssertionError assert not test_filepath.exists() @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz"]) def test_writes_compressed_formats(DATA_DIR, tmp_dir, suffix): """correctly writes / reads different compression formats""" fpath = DATA_DIR / "sample.tsv" expect = pathlib.Path(fpath).read_text() outpath = tmp_dir / f"{fpath.name}.{suffix}" with atomic_write(outpath, mode="wt") as f: f.write(expect) with open_(outpath) as infile: got = infile.read() assert got == expect, f"write failed for {suffix}" def test_atomic_invalid_parent_dir(): with pytest.raises(OSError), atomic_write("invalid_dir/test.txt") as out: out.write("will not work") def test_rename(tmp_dir): """Renames file as expected""" test_filepath = tmp_dir / "Atomic_write_test" open(test_filepath, "w").close() assert test_filepath.exists() with atomic_write(test_filepath, mode="w") as f: f.write("abc") def test_atomic_write_noncontext(tmp_dir): """atomic write works as more regular file object""" path = tmp_dir / "foo.txt" zip_path = path.parent / f"{path.name}.zip" aw = atomic_write(path, in_zip=zip_path, mode="w") aw.write("some data") aw.close() with open_(zip_path) as ifile: got = ifile.read() assert got == "some data" def test_open_handles_bom(tmp_dir): """handle files with a byte order mark""" text = "some text" textfile = tmp_dir / "sample.txt" textfile.write_text(text, encoding="utf-8-sig") gzip_file = tmp_dir / "sample.txt.gz" with gzip.open(gzip_file, "wt", encoding="utf-8-sig") as outfile: outfile.write(text) bzip_file = tmp_dir / "sample.txt.bz2" with bz2.open(bzip_file, "wt", encoding="utf-8-sig") as outfile: outfile.write(text) zip_file = tmp_dir / "sample.zip" with zipfile.ZipFile(zip_file, "w") as outfile: outfile.write(textfile, "sample.txt") for path in (bzip_file, gzip_file, textfile, zip_file): with open_(path) as infile: got = infile.read() assert got == text, f"failed reading {path}" @pytest.mark.parametrize("non", [None, ""]) def test_open_empty_raises(non): with pytest.raises(ValueError): open_(non) def test_aw_zip_from_path(tmp_dir): """supports inferring zip archive name from path""" path = tmp_dir / "foo.txt" zip_path = path.parent / f"{path.name}.zip" aw = atomic_write(zip_path, in_zip=True, mode="w") aw.write("some data") aw.close() with open_(zip_path) as ifile: got = ifile.read() assert got == "some data" path = tmp_dir / "foo2.txt" zip_path = path.parent / f"{path.name}.zip" aw = atomic_write(path, in_zip=zip_path, mode="w") aw.write("some data") aw.close() with open_(zip_path) as ifile: got = ifile.read() assert got == "some data" def test_expanduser(tmp_dir): """expands user correctly""" home = pathlib.Path("~").expanduser() test_filepath = tmp_dir / "Atomic_write_test" test_filepath = str(test_filepath).replace(str(home), "~") with atomic_write(test_filepath, mode="w") as f: f.write("abc") def test_path_relative_to_zip_parent(): """correctly generates member paths for a zip archive""" zip_path = pathlib.Path("some/path/to/a/data.zip") for member in ("data/member.txt", "member.txt", "a/b/c/member.txt"): got = _path_relative_to_zip_parent(zip_path, pathlib.Path(member)) assert got.parts[0] == "data" @pytest.mark.parametrize( ("name", "expect"), [ ("suffixes.GZ", (None, "gz")), ("suffixes.ABCD", ("abcd", None)), ("suffixes.ABCD.BZ2", ("abcd", "bz2")), ("suffixes.abcd.BZ2", ("abcd", "bz2")), ("suffixes.ABCD.bz2", ("abcd", "bz2")), ], ) def test_get_format_suffixes_returns_lower_case(name, expect): """should always return lower case""" assert get_format_suffixes(name) == expect @pytest.mark.parametrize( ("name", "expect"), [ ("no_suffixes", (None, None)), ("suffixes.gz", (None, "gz")), ("suffixes.abcd", ("abcd", None)), ("suffixes.abcd.bz2", ("abcd", "bz2")), ("suffixes.zip", (None, "zip")), ], ) def test_get_format_suffixes(name, expect): """correctly return suffixes for compressed etc.. formats""" assert get_format_suffixes(name) == expect @pytest.mark.parametrize( ("name", "expect"), [ ("no_suffixes", (None, None)), ("suffixes.gz", (None, "gz")), ("suffixes.abcd", ("abcd", None)), ("suffixes.abcd.bz2", ("abcd", "bz2")), ("suffixes.zip", (None, "zip")), ], ) def test_get_format_suffixes_pathlib(name, expect): """correctly return suffixes for compressed etc.. formats from pathlib""" assert get_format_suffixes(pathlib.Path(name)) == expect @pytest.mark.parametrize( ("val", "expect"), [ ({}, False), ("not an existing path", False), ("(a,b,(c,d))", False), ("(a:0.1,b:0.1,(c:0.1,d:0.1):0.1)", False), (__file__, True), (pathlib.Path(__file__), True), (NotCompleted("FAIL", "test", message="none", source="unknown"), False), ], ) def test_path_exists(val, expect): """robustly identifies whether an object is a valid path and exists""" assert path_exists(val) == expect def test_open_reads_zip(tmp_dir): """correctly reads a zip compressed file""" text_path = tmp_dir / "foo.txt" with open(text_path, "w") as f: f.write("any str") zip_path = tmp_dir / "foo.zip" with zipfile.ZipFile(zip_path, "w") as zf: zf.write(text_path) with open_(zip_path) as got: assert got.readline() == "any str" def test_open_writes_zip(tmp_dir): """correctly writes a zip compressed file""" zip_path = tmp_dir / "foo.txt.zip" with open_(zip_path, "w") as f: f.write("any str") with zipfile.ZipFile(zip_path, "r") as zf: name = zf.namelist()[0] got = zf.open(name).read() assert got == b"any str" def test_open_writes_zip_binary(tmp_dir): """a wb mode writes bytes to a zip unchanged The payload is not valid utf-8 and carries a carriage return, so a member that had been through a text encode or a newline translation would not match. Reading the member back with zipfile rather than with open_ keeps this independent of the read path. """ zip_path = tmp_dir / "foo.txt.zip" payload = b"\xff\xfe\x00\x80 raw \r\n bytes" with open_(zip_path, "wb") as f: f.write(payload) with zipfile.ZipFile(zip_path, "r") as zf: name = zf.namelist()[0] assert zf.open(name).read() == payload def test_open_zip_multi(tmp_dir): """zip with multiple records cannot be opened using open_""" text_path1 = tmp_dir / "foo.txt" with open(text_path1, "w") as f: f.write("any str") text_path2 = tmp_dir / "bar.txt" with open(text_path2, "w") as f: f.write("any str") zip_path = tmp_dir / "foo.zip" with zipfile.ZipFile(zip_path, "w") as zf: zf.write(text_path1) zf.write(text_path2) with pytest.raises(ValueError): open_(zip_path) @pytest.mark.parametrize("newline", ["\n", "\r\n"]) @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_write_uses_given_newline(tmp_path, suffix, newline): """a newline argument controls the line endings that are written Without the argument a bare newline is translated to os.linesep, so whichever of the two below matches the platform cannot tell a forwarded argument from a dropped one. The other one can, so the pair covers any platform. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wt", newline=newline) as outfile: outfile.write("first\nsecond\n") with open_(outpath, mode="rb") as infile: assert infile.read() == f"first{newline}second{newline}".encode() @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_read_uses_given_newline(tmp_path, suffix): """a newline argument turns off the translation of line endings""" outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wb") as outfile: outfile.write(b"first\r\nsecond\r\n") with open_(outpath, mode="rt", encoding="utf-8", newline="") as infile: assert infile.read() == "first\r\nsecond\r\n" @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_read_uses_given_errors(tmp_path, suffix): """an errors argument reaches the decoder""" outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wb") as outfile: outfile.write(b"caf\xe9") with open_(outpath, mode="rt", encoding="utf-8", errors="replace") as infile: assert infile.read() == "caf\N{REPLACEMENT CHARACTER}" @pytest.mark.parametrize( "kwargs", [{"encoding": "utf-8"}, {"errors": "x"}, {"newline": ""}] ) @pytest.mark.parametrize("mode", ["rb", "wb"]) @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_binary_rejects_decoding_arguments(tmp_path, suffix, mode, kwargs): """a decoding argument with a binary mode raises for every suffix zip was the odd one out: it dropped the encoding on the floor and let ZipFile.open report errors and newline as an unexpected keyword, so a caller catching ValueError caught five suffixes and missed the sixth. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wb") as outfile: outfile.write(b"data\n") with pytest.raises(ValueError, match=r"not supported|doesn't take|does not take"): open_(outpath, mode=mode, **kwargs) @pytest.mark.parametrize( "kwargs", [{"encoding": None}, {"errors": None}, {"newline": None}], ) @pytest.mark.parametrize("mode", ["rb", "wb"]) @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_binary_allows_none_decoding_arguments(tmp_path, suffix, mode, kwargs): """a decoding argument of None is not an argument builtin open, gzip, bz2 and lzma all test the value rather than ask whether the name was passed, so that a caller relaying an unset argument of its own is not rejected for it. A guard that checked for the name would make zip and a url the only two that broke such a caller. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wb") as outfile: outfile.write(b"data\n") with open_(outpath, mode=mode, **kwargs) as handle: assert handle is not None def test_open_zip_write_does_not_take_atomic_write_parameters(tmp_path): """a caller argument cannot bind to a parameter of atomic_write tmpdir is the dangerous one. Bound as a parameter it names the directory that _close_rename_zip removes with rmtree once the write succeeds, so forwarding it would delete a directory of the caller's. """ keep = tmp_path / "keep" keep.mkdir() (keep / "precious.txt").write_text("do not delete me") handle = open_(tmp_path / "out.tsv.zip", mode="wt", tmpdir=keep) # the file is not opened until the atomic_write is entered, so that # is where an argument the open cannot take is found with pytest.raises(TypeError, match="tmpdir"): handle.__enter__() assert (keep / "precious.txt").exists() # the rejection happens after __init__ has made somewhere to write, # so the failure has to take that directory away again assert sorted(p.name for p in tmp_path.iterdir()) == ["keep"] @pytest.mark.parametrize( ("kwargs", "expect"), [ ({"encoding": "not-a-real-codec"}, LookupError), ({"encoding": "utf-8", "newline": "X"}, ValueError), ], ) def test_open_zip_read_closes_member_on_a_bad_argument(tmp_path, kwargs, expect): """a rejected decoding argument does not leave the member open The codec is looked up and the newline validated by the wrapper, so the two arrive as different exception types and a handler narrowed to one of them would leak on the other. """ outpath = tmp_path / "sample.tsv.zip" with open_(outpath, mode="wt") as outfile: outfile.write("data\n") opened = [] real_open = zipfile.ZipFile.open def spy(self, name, mode="r", pwd=None, **kwargs): member = real_open(self, name, mode, pwd, **kwargs) opened.append(member) return member with pytest.MonkeyPatch.context() as mp: mp.setattr(zipfile.ZipFile, "open", spy) with pytest.raises(expect): open_(outpath, mode="rt", **kwargs) assert [member.closed for member in opened] == [True] class _NotSeekable(io.BufferedIOBase): """a readable stream with no seek, as an http response is http.client.HTTPResponse is a BufferedIOBase whose seekable comes straight from io.IOBase and answers False. """ def __init__(self, data): self._buffer = io.BytesIO(data) def read(self, size=-1): return self._buffer.read(size) def readable(self): return True def seekable(self): return False @pytest.mark.parametrize("mode", ["rt", "rb"]) def test_open_zip_reads_a_stream_that_cannot_seek(tmp_path, mode): """a zip read buffers a stream ZipFile could not otherwise use ZipFile finds the central directory by seeking to the end of the archive, so a stream without seek is reported as not being a zip at all rather than as something that cannot be read this way. """ outpath = tmp_path / "sample.tsv.zip" # newline is named so the member holds the bytes asserted below on # every platform. Without it a bare "\n" is written as os.linesep, # so the archive holds "\r\n" on Windows and a binary read of it # does not match with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\n") with open_zip(_NotSeekable(outpath.read_bytes()), mode=mode) as infile: got = infile.read() assert got == (b"id\tname\n" if "b" in mode else "id\tname\n") def test_open_zip_closes_a_stream_it_had_to_buffer(tmp_path): """the drained stream is closed, since nothing else holds it open_url hands the response over and returns the member, so if the buffering did not close it the connection behind it would stay open with no handle anywhere to close it with. """ outpath = tmp_path / "sample.tsv.zip" with open_(outpath, mode="wt") as outfile: outfile.write("id\tname\n") stream = _NotSeekable(outpath.read_bytes()) with open_zip(stream) as infile: infile.read() assert stream.closed def test_open_zip_closes_a_stream_whose_read_fails(): """a stream that fails part way through is still closed Buffering it is what takes ownership, so the failure has to release it. A network stream that drops mid-transfer is the case: the read raises, and without this the response stays open with nothing holding it while the exception goes past. """ class _FailsPartWay(_NotSeekable): def read(self, _size=-1): msg = "connection dropped" raise OSError(msg) stream = _FailsPartWay(b"") with pytest.raises(OSError, match="connection dropped"): open_zip(stream) assert stream.closed def test_open_zip_leaves_a_seekable_stream_alone(tmp_path): """a stream that can seek is used directly, not copied ZipFile reads only the parts of the archive it needs from a seekable source, so buffering one would be a whole extra copy of it in memory for nothing. """ outpath = tmp_path / "sample.tsv.zip" with open_(outpath, mode="wt") as outfile: outfile.write("id\tname\n") stream = io.BytesIO(outpath.read_bytes()) with open_zip(stream) as infile: assert infile.read() == "id\tname\n" assert not stream.closed @pytest.mark.parametrize("mode", ["wt", "wb"]) def test_open_zip_write_rejects_a_stream(mode): """a write names an archive to create, so it needs a path""" with pytest.raises(TypeError, match="not an open stream"): open_zip(io.BytesIO(), mode=mode) @pytest.mark.parametrize("mode", ["rt", "rb"]) def test_open_url_reads_a_zip_over_a_stream_that_cannot_seek( tmp_path, monkeypatch, mode ): """a zip url works where the response cannot seek http.client.HTTPResponse has no seek, so this is what a real zip over http does. A file:// response wraps a real file and can seek, which is why the existing url tests did not catch it. """ outpath = tmp_path / "sample.tsv.zip" # newline is named so the member holds the bytes asserted below on # every platform. Without it a bare "\n" is written as os.linesep, # so the archive holds "\r\n" on Windows and a binary read of it # does not match with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\n") def fake_urlopen(url, timeout=None): # noqa: ARG001 return urllib.response.addinfourl( _NotSeekable(outpath.read_bytes()), email.message.Message(), url, ) monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with open_url(outpath.as_uri(), mode=mode) as infile: got = infile.read() assert got == (b"id\tname\n" if "b" in mode else "id\tname\n") def test_open_zip_write_twice_replaces_the_member(tmp_path): """a second write to the same path replaces the first Appending instead left two entries under one name, which zipfile warns about and open_ then refuses to read at all, so the second write destroyed the archive rather than updating it. """ outpath = tmp_path / "sample.tsv.zip" for text in ("first\n", "second\n"): with open_(outpath, mode="wt") as outfile: outfile.write(text) with zipfile.ZipFile(outpath) as zf: assert zf.namelist() == ["sample.tsv"] with open_(outpath) as infile: assert infile.read() == "second\n" def test_open_zip_write_keeps_the_other_members(tmp_path): """replacing one member leaves the rest of the archive alone atomic_write can be aimed at a named member of a multi-member archive, so replacing one must not rewrite the archive down to just that one. """ outpath = tmp_path / "sample.zip" with zipfile.ZipFile(outpath, "w") as zf: zf.writestr("sample/first.tsv", "one\n") zf.writestr("sample/second.tsv", "two\n") # as above, the newline keeps the bytes the same on every platform aw = atomic_write( pathlib.Path("sample/second.tsv"), in_zip=outpath, mode="wt", open_kwargs={"newline": "\n"}, ) aw.write("replaced\n") aw.close() with zipfile.ZipFile(outpath) as zf: assert zf.namelist() == ["sample/first.tsv", "sample/second.tsv"] assert zf.read("sample/first.tsv") == b"one\n" assert zf.read("sample/second.tsv") == b"replaced\n" @pytest.mark.parametrize( "member", ["sample/sub/../target.tsv", "/sample/target.tsv", "sample/target.tsv"], ) def test_open_zip_write_twice_under_a_normalised_name(tmp_path, member): """the name compared is the name the archive will store ZipInfo.from_file collapses "..", drops a leading separator and a drive, so a member path is not in general the name that comes back from namelist(). Comparing the path instead appends a duplicate under the stored name and the archive stops being readable. """ outpath = tmp_path / "sample.zip" for text in ("first\n", "second\n"): aw = atomic_write(pathlib.Path(member), in_zip=outpath, mode="wt") aw.write(text) aw.close() with zipfile.ZipFile(outpath) as zf: assert len(zf.namelist()) == 1 with open_(outpath) as infile: assert infile.read() == "second\n" def test_open_zip_write_into_a_zero_byte_file(tmp_path): """a 0-byte archive file is written to, not refused That file is what an interrupted earlier write leaves behind. A read-only open of one raises BadZipFile, so testing for the member that way would turn a recoverable state into a permanent one. """ outpath = tmp_path / "sample.tsv.zip" outpath.write_bytes(b"") with open_(outpath, mode="wt") as outfile: outfile.write("data\n") with open_(outpath) as infile: assert infile.read() == "data\n" def test_open_zip_write_keeps_member_compression_and_comments(tmp_path): """a member copied across keeps how it was stored Copying by name rather than through the ZipInfo would re-store every other member with the default method and lose the comments. """ outpath = tmp_path / "sample.zip" with zipfile.ZipFile(outpath, "w") as zf: info = zipfile.ZipInfo("sample/first.tsv") info.compress_type = zipfile.ZIP_DEFLATED info.comment = b"a member comment" zf.writestr(info, "one\n" * 100) zf.writestr("sample/second.tsv", "two\n") zf.comment = b"an archive comment" aw = atomic_write(pathlib.Path("sample/second.tsv"), in_zip=outpath, mode="wt") aw.write("replaced\n") aw.close() with zipfile.ZipFile(outpath) as zf: assert zf.comment == b"an archive comment" kept = zf.getinfo("sample/first.tsv") assert kept.compress_type == zipfile.ZIP_DEFLATED assert kept.comment == b"a member comment" def test_open_zip_write_keeps_the_mode_of_the_archive(tmp_path): """the rewritten archive is not widened to the umask default What is asserted is that the mode survives the rewrite, not that it is any particular value. Windows models only the read-only bit, so a chmod to 0o600 there leaves the file at 0o666 and the check is weak rather than wrong: it holds, but a new file would have had that mode anyway. On a platform with real permission bits the rewritten file would come back 0o644 from the umask without the mode being carried across. """ outpath = tmp_path / "sample.tsv.zip" with open_(outpath, mode="wt") as outfile: outfile.write("first\n") outpath.chmod(0o600) before = outpath.stat().st_mode & 0o777 with open_(outpath, mode="wt") as outfile: outfile.write("second\n") assert outpath.stat().st_mode & 0o777 == before def test_open_zip_write_cleans_up_when_the_rewrite_fails(tmp_path, monkeypatch): """a failed rewrite leaves the archive and the directory as they were""" outpath = tmp_path / "sample.tsv.zip" with open_(outpath, mode="wt") as outfile: outfile.write("first\n") before = outpath.read_bytes() def boom(*_args, **_kwargs): msg = "copy failed" raise OSError(msg) monkeypatch.setattr(scinexus.io_util.shutil, "copyfileobj", boom) with ( pytest.raises(OSError, match="copy failed"), open_(outpath, mode="wt") as outfile, ): outfile.write("second\n") assert outpath.read_bytes() == before assert [p.name for p in tmp_path.iterdir()] == ["sample.tsv.zip"] def test_open_zip_write_repairs_an_already_duplicated_member(tmp_path): """an archive the old code wrecked is made readable again Two entries under one name is what appending produced, so a write to such an archive has to leave one member rather than three. """ outpath = tmp_path / "sample.zip" with zipfile.ZipFile(outpath, "w") as zf: zf.writestr("sample/target.tsv", "first\n") with warnings.catch_warnings(): # zipfile warns on the duplicate name, which is the corruption this # test creates on purpose so it can check atomic_write repairs it warnings.simplefilter("ignore", UserWarning) with zipfile.ZipFile(outpath, "a") as zf: zf.writestr("sample/target.tsv", "second\n") aw = atomic_write(pathlib.Path("sample/target.tsv"), in_zip=outpath, mode="wt") aw.write("third\n") aw.close() with zipfile.ZipFile(outpath) as zf: assert zf.namelist() == ["sample/target.tsv"] with open_(outpath) as infile: assert infile.read() == "third\n" def test_open_url_write_exceptions(): """Test 'w' mode (should raise Exception)""" with pytest.raises(Exception): open_url("http://example.com/test.txt", mode="w") def test_open_url_exceptions(): """non-http(s) address for url (should raise Exception)""" with pytest.raises(Exception): open_url("ftp://example.com/test.txt") def test_open_url_uses_given_encoding(tmp_path): """an encoding argument is used where the headers name no charset A file:// response carries no charset, so without the argument the text is decoded with the locale default. The euro sign is the byte to test with: 0x80 is the euro in cp1252, a control character in latin-1 and not valid utf-8, so no locale decodes it to the expected string by accident. """ path = tmp_path / "sample.txt" text = "price 5\N{EURO SIGN}" path.write_bytes(text.encode("cp1252")) with open_url(path.as_uri(), encoding="cp1252") as infile: assert infile.read() == text def test_open_url_encoding_beats_header_charset(tmp_path, monkeypatch): """an encoding argument overrides the charset the headers name A file:// response names no charset, so a response with one has to be built here for the override to be exercised at all. """ path = tmp_path / "sample.txt" text = "price 5\N{EURO SIGN}" path.write_bytes(text.encode("cp1252")) headers = email.message.Message() headers["Content-Type"] = "text/plain; charset=utf-8" def fake_urlopen(url, timeout=None): # noqa: ARG001 return urllib.response.addinfourl(path.open("rb"), headers, url) monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) # utf-8 from the header would raise on the 0x80 byte with open_url(path.as_uri(), encoding="cp1252") as infile: assert infile.read() == text @pytest.mark.parametrize( ("kwargs", "expect"), [ ({"encoding": "not-a-real-codec"}, LookupError), ({"newline": "X"}, ValueError), ({"not_an_argument": 1}, TypeError), ], ) def test_open_url_closes_response_on_a_bad_argument( tmp_path, monkeypatch, kwargs, expect, ): """a rejected argument does not leave the connection open The wrapper is built after the request has been made, so an unknown codec or an illegal newline has to release the response rather than hand the caller an exception and no handle to close with. """ path = tmp_path / "sample.txt" path.write_bytes(b"data") responses = [] def fake_urlopen(url, timeout=None): # noqa: ARG001 response = urllib.response.addinfourl( path.open("rb"), email.message.Message(), url, ) responses.append(response) return response monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with pytest.raises(expect): open_url(path.as_uri(), **kwargs) assert [r.closed for r in responses] == [True] def test_open_url_uses_given_errors(tmp_path): """an errors argument reaches the decoder""" path = tmp_path / "sample.txt" path.write_bytes(b"caf\xe9") with open_url(path.as_uri(), encoding="utf-8", errors="replace") as infile: assert infile.read() == "caf\N{REPLACEMENT CHARACTER}" def test_open_url_uses_given_newline(tmp_path): """a newline argument turns off the translation of line endings""" path = tmp_path / "sample.txt" path.write_bytes(b"first\r\nsecond\r\n") with open_url(path.as_uri(), newline="") as infile: assert infile.read() == "first\r\nsecond\r\n" @pytest.mark.parametrize( "kwargs", [{"encoding": "utf-8"}, {"errors": "replace"}, {"newline": ""}], ) def test_open_url_binary_rejects_text_arguments(tmp_path, kwargs): """text-only arguments in a binary mode are a caller error builtin open raises for the same combination, rather than accepting an argument it will not use. """ path = tmp_path / "sample.txt" path.write_bytes(b"data") with pytest.raises(ValueError, match="binary mode does not take"): open_url(path.as_uri(), mode="rb", **kwargs) @pytest.mark.parametrize("mode", ["rt", "rb"]) def test_open_url_closes_response_on_a_corrupt_archive(tmp_path, monkeypatch, mode): """a decompression that fails also releases the response The archive is opened after the request has been made, so this is the same problem as a rejected argument and not a different one. The error is BadZipFile rather than anything the wrapper raises, which is why the release cannot hang off a list of argument errors. """ path = tmp_path / "sample.zip" path.write_bytes(b"not a zip at all") responses = [] def fake_urlopen(url, timeout=None): # noqa: ARG001 response = urllib.response.addinfourl( path.open("rb"), email.message.Message(), url, ) responses.append(response) return response monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with pytest.raises(zipfile.BadZipFile): open_url(path.as_uri(), mode=mode) assert [r.closed for r in responses] == [True] def test_open_url_closes_response_on_a_keyboard_interrupt(tmp_path, monkeypatch): """an interrupt mid-construction releases the response too This is the whole of what registering the cleanup buys over catching Exception and re-raising: a KeyboardInterrupt does not derive from Exception, so it used to go past and leave the socket open with no handle on it anywhere. """ path = tmp_path / "sample.txt" path.write_bytes(b"data") responses = [] def fake_urlopen(url, timeout=None): # noqa: ARG001 response = urllib.response.addinfourl( path.open("rb"), email.message.Message(), url, ) responses.append(response) return response def interrupt(*_args, **_kwargs): raise KeyboardInterrupt monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) monkeypatch.setattr(scinexus.io_util, "TextIOWrapper", interrupt) with pytest.raises(KeyboardInterrupt): open_url(path.as_uri()) assert [r.closed for r in responses] == [True] @pytest.mark.parametrize("suffix", ["gz", "bz2", "xz", "lzma", "zip", "tsv"]) @pytest.mark.parametrize("mode", ["rt", "rb"]) def test_open_url_reader_closes_the_response(tmp_path, monkeypatch, suffix, mode): """closing what open_url returned closes the response behind it gzip, bz2 and lzma leave a file object they were handed open when the reader over them is closed, and so does a zip member, so for a compressed url the response was left open with no handle on it anywhere once open_url had returned. Reading such a url in a loop leaks one connection per call. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\n") responses = [] def fake_urlopen(url, timeout=None): # noqa: ARG001 response = urllib.response.addinfourl( outpath.open("rb"), email.message.Message(), url, ) responses.append(response) return response monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with open_url(outpath.as_uri(), mode=mode) as infile: assert infile.read() in (b"id\tname\n", "id\tname\n") assert [r.closed for r in responses] == [True] @pytest.mark.parametrize("suffix", ["gz", "bz2", "xz", "lzma", "zip", "tsv"]) def test_open_url_text_returns_an_io_object(tmp_path, monkeypatch, suffix): """what comes back is still an I/O object to anything that asks typeguard tests IO[str] with isinstance against io.TextIOBase, so an object that merely forwards every call to a reader is rejected. It is a runtime dependency here, and an app whose signature names a handle would have started refusing compressed urls, and only compressed urls. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\n") def fake_urlopen(url, timeout=None): # noqa: ARG001 return urllib.response.addinfourl( outpath.open("rb"), email.message.Message(), url, ) monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with open_url(outpath.as_uri()) as infile: typeguard.check_type(infile, typing.IO[str]) @pytest.mark.parametrize("hint", [typing.IO[bytes], typing.IO[typing.Any]]) @pytest.mark.parametrize("suffix", ["gz", "bz2", "xz", "lzma", "zip"]) def test_open_url_binary_returns_an_io_object(tmp_path, monkeypatch, suffix, hint): """as above for a binary read of a compressed url The uncompressed suffixes are not here. A binary read of one hands back the response itself, so whether it is an I/O object is the transport's business: http.client.HTTPResponse is a BufferedIOBase and the addinfourl wrapping a file:// read is not. That is not something this changes either way. """ outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\n") def fake_urlopen(url, timeout=None): # noqa: ARG001 return urllib.response.addinfourl( outpath.open("rb"), email.message.Message(), url, ) monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with open_url(outpath.as_uri(), mode="rb") as infile: typeguard.check_type(infile, hint) @pytest.mark.parametrize("suffix", ["gz", "bz2", "xz", "lzma", "zip"]) def test_open_url_binary_reader_reads_like_the_stream_under_it( tmp_path, monkeypatch, suffix, ): """the owning reader serves reads, seeks and iteration unchanged""" outpath = tmp_path / f"sample.tsv.{suffix}" with open_(outpath, mode="wt", newline="\n") as outfile: outfile.write("id\tname\nrow\tone\n") def fake_urlopen(url, timeout=None): # noqa: ARG001 return urllib.response.addinfourl( outpath.open("rb"), email.message.Message(), url, ) monkeypatch.setattr(scinexus.io_util, "urlopen", fake_urlopen) with open_url(outpath.as_uri(), mode="rb") as infile: assert infile.readable() assert infile.read(3) == b"id\t" assert list(infile) == [b"name\n", b"row\tone\n"] if infile.seekable(): assert infile.seek(0) == 0 assert infile.tell() == 0 assert infile.read() == b"id\tname\nrow\tone\n" assert infile.closed def test_closing_reader_read1_serves_the_reader(tmp_path): """read1 is served like read, not left to the base class io.BufferedIOBase leaves read1 raising, and TextIOWrapper prefers it over read when the buffer offers one. """ outpath = tmp_path / "sample.bin" outpath.write_bytes(b"id\tname\n") stream = outpath.open("rb") reader = scinexus.io_util._ClosingReader(io.BytesIO(b"id\tname\n"), stream) with reader: assert reader.read1(3) == b"id\t" assert reader.read1() == b"name\n" assert stream.closed def test_closing_reader_without_a_reader_does_not_recurse(): """an instance made without __init__ raises rather than recursing A proxy that looks its own attribute up through __getattr__ calls itself forever when that attribute is missing, which is the state copy.copy builds before it probes for __setstate__. """ bare = scinexus.io_util._ClosingReader.__new__(scinexus.io_util._ClosingReader) # name is not defined on the class, so it is the delegation that # answers for it, where read and close are the class's own with pytest.raises(AttributeError): _ = bare.name assert not hasattr(bare, "__setstate__") assert copy.copy(scinexus.io_util._ClosingReader(io.BytesIO(b""), io.BytesIO(b""))) def test_closing_reader_without_a_reader_closes_quietly(recwarn): """such an instance can be closed, and so can be finalised io.IOBase calls close from __del__, and an exception there cannot be raised, so it is reported as an unraisable and turns into a warning under pytest. A copy.copy of one of these is finalised exactly this way. """ bare = scinexus.io_util._ClosingReader.__new__(scinexus.io_util._ClosingReader) bare.close() assert bare.closed del bare gc.collect() unraisable = [w for w in recwarn if "Unraisable" in type(w.message).__name__] assert unraisable == [] def test_open_url_binary_rejects_unknown_argument(tmp_path): """an unknown argument is a TypeError in either mode A text mode gets this from TextIOWrapper. A binary mode has nothing to hand the argument to, so it has to raise the error itself rather than report the name as a text-only argument or ignore it. """ path = tmp_path / "sample.txt" path.write_bytes(b"data") with pytest.raises(TypeError, match="not_an_argument"): open_url(path.as_uri(), mode="rb", not_an_argument=1) def test_open_url_binary_allows_encoding_none(tmp_path): """an explicit encoding of None is not an argument, as for open open_ hands its kwargs to open_url before it pops the encoding, so a None reaching a binary mode this way must not be rejected. """ path = tmp_path / "sample.txt" path.write_bytes(b"data") with open_url(path.as_uri(), mode="rb", encoding=None) as infile: assert infile.read() == b"data" def test_iter_splitlines_one(tmp_path): path = tmp_path / "one-line.txt" value = "We have text on one line." path.write_text(value) got = list(iter_splitlines(path)) assert got == [value] @pytest.mark.parametrize("newline", ["\n", "\r\n"]) def test_iter_splitlines_line_diff_newline(tmp_path, newline): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] with open_(path, mode="w", newline=newline) as out: out.write("\n".join(value)) got = list(iter_splitlines(path, chunk_size=5)) assert got == value @pytest.mark.parametrize("newline", ["\n", "\r\n"]) def test_iter_splitlines_file_endswith_newline(tmp_path, newline): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] with open_(path, mode="w", newline=newline) as out: out.write("\n".join(value) + "\n") got = list(iter_splitlines(path, chunk_size=5)) assert got == value def test_iter_splitlines_chunk_size_exceeds_file_size(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value)) got = list(iter_splitlines(path, chunk_size=5_000_000)) assert got == value @pytest.mark.parametrize( "value", [ "With text\nending on a\nended in newline.", "With text\nending\non a\nended in newline.", ], ) def test_iter_splitlines_chunk_endswith_newline(tmp_path, value): path = tmp_path / "multi-line.txt" value = value.splitlines() path.write_text("\n".join(value)) got = list(iter_splitlines(path, chunk_size=11)) assert got == value def test_iter_splitlines_line_spans_multiple_chunks(tmp_path): path = tmp_path / "long-lines.txt" value = ["a" * 12, "b" * 12] path.write_text("\n".join(value)) # each line is spread over 3 chunks got = list(iter_splitlines(path, chunk_size=5)) assert got == value @pytest.mark.parametrize("chunk_size", [5, None]) def test_iter_splitlines_trailing_carriage_return(tmp_path, chunk_size): path = tmp_path / "trailing-cr.txt" value = "We have some\n\r" path.write_text(value, newline="") got = list(iter_splitlines(path, chunk_size=chunk_size)) # the file ends on a line terminator, splitlines() would report a # trailing empty line here assert got == ["We have some"] LINE_BOUNDARIES = [ "\n", "\r", "\r\n", "\v", "\f", "\x1c", "\x1d", "\x1e", "\x85", "\N{LINE SEPARATOR}", "\N{PARAGRAPH SEPARATOR}", ] @pytest.mark.parametrize("chunk_size", [3, 4, 5, 6, 7, None]) @pytest.mark.parametrize("boundary", LINE_BOUNDARIES) def test_iter_splitlines_line_boundaries(tmp_path, boundary, chunk_size): # str.splitlines() breaks on more than "\n", and the result must not # depend on where the chunk boundary falls path = tmp_path / "boundary.txt" content = f"abcd{boundary}efgh" path.write_text(content, newline="", encoding="utf8") got = list(iter_splitlines(path, chunk_size=chunk_size)) assert got == ["abcd", "efgh"] @pytest.mark.parametrize("chunk_size", [3, 4, 5, 6, 7, None]) @pytest.mark.parametrize("boundary", [b"\v", b"\f", b"\x1c", b"\x1d", b"\x1e"]) def test_iter_splitlines_as_bytes_not_line_boundaries(tmp_path, boundary, chunk_size): # bytes.splitlines() breaks only on "\n", "\r" and "\r\n" path = tmp_path / "boundary.dat" content = b"abcd" + boundary + b"efgh" path.write_bytes(content) got = list(iter_splitlines(path, chunk_size=chunk_size, as_bytes=True)) assert got == [content] @pytest.mark.parametrize("chunk_size", [1, 2, 3, None]) @pytest.mark.parametrize( ("content", "expect"), [ ("abcd\n", ["abcd"]), ("abcd\n\n", ["abcd"]), ("abcd\n\n\n", ["abcd", ""]), ("abcd\n\x0c", ["abcd"]), ("abcd\nefgh\n\n", ["abcd", "efgh"]), ("abcd\n\nefgh", ["abcd", "", "efgh"]), ("\n", []), ("\n\n", [""]), ], ) def test_iter_splitlines_no_trailing_empty_line(tmp_path, content, expect, chunk_size): path = tmp_path / "trailing.txt" path.write_text(content, newline="", encoding="utf8") got = list(iter_splitlines(path, chunk_size=chunk_size)) assert got == expect @pytest.mark.parametrize("chunk_size", [1, 2, 3, None]) @pytest.mark.parametrize( ("content", "expect"), [ (b"abcd\n", [b"abcd"]), (b"abcd\n\n", [b"abcd"]), (b"abcd\n\r", [b"abcd"]), (b"abcd\r\n\r\n", [b"abcd"]), (b"abcd\n\n\n", [b"abcd", b""]), (b"\n", []), ], ) def test_iter_splitlines_as_bytes_no_trailing_empty_line( tmp_path, content, expect, chunk_size, ): path = tmp_path / "trailing.dat" path.write_bytes(content) got = list(iter_splitlines(path, chunk_size=chunk_size, as_bytes=True)) assert got == expect def test_iter_splitlines_chunk_empty_file(tmp_path): path = tmp_path / "zero.txt" path.write_text("") got = list(iter_splitlines(path)) assert not got def test_iter_splitlines_as_bytes_one(tmp_path): path = tmp_path / "one-line.txt" value = "We have text on one line." path.write_text(value) got = list(iter_splitlines(path, as_bytes=True)) assert got == [value.encode("utf8")] @pytest.mark.parametrize("newline", ["\n", "\r\n"]) @pytest.mark.parametrize("chunk_size", [5, None]) def test_iter_splitlines_as_bytes_multi_line(tmp_path, newline, chunk_size): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] with open_(path, mode="w", newline=newline) as out: out.write("\n".join(value)) got = list(iter_splitlines(path, chunk_size=chunk_size, as_bytes=True)) assert got == [line.encode("utf8") for line in value] def test_iter_splitlines_as_bytes_file_endswith_newline(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value) + "\n") got = list(iter_splitlines(path, chunk_size=5, as_bytes=True)) assert got == [line.encode("utf8") for line in value] def test_iter_splitlines_as_bytes_empty_file(tmp_path): path = tmp_path / "zero.txt" path.write_text("") got = list(iter_splitlines(path, as_bytes=True)) assert not got def test_iter_splitlines_as_bytes_not_decodable(tmp_path): # bytes that are not valid utf8 are returned unchanged path = tmp_path / "binary.dat" value = [b"\xff\xfe some", b"\x00 bytes"] path.write_bytes(b"\n".join(value)) got = list(iter_splitlines(path, chunk_size=4, as_bytes=True)) assert got == value @pytest.mark.parametrize("chunk_size", [1, 2, 3, 4, None]) @pytest.mark.parametrize( "content", [ b"ab\rcd", b"ab\r\rcd", b"ab\r\ncd", b"ab\r\n\r\ncd", b"ab\r", b"ab\r\n", b"\rab", b"\nab", ], ) def test_iter_splitlines_as_bytes_carriage_return(tmp_path, content, chunk_size): # binary mode does no newline translation, so a chunk boundary can # fall between the "\r" and the "\n" of a "\r\n" path = tmp_path / "cr.dat" path.write_bytes(content) got = list(iter_splitlines(path, chunk_size=chunk_size, as_bytes=True)) assert got == content.splitlines() def test_iter_splitlines_as_bytes_compressed(tmp_path): path = tmp_path / "multi-line.txt.gz" value = ["We have some", "text on different lines", "which load"] with open_(path, mode="wt") as out: out.write("\n".join(value)) got = list(iter_splitlines(path, chunk_size=5, as_bytes=True)) assert got == [line.encode("utf8") for line in value] @pytest.mark.parametrize("transform", [str, pathlib.Path]) def test_iter_splitlines_tilde(home_file, transform): expect = pathlib.Path(home_file).expanduser().read_text().splitlines() got = list(iter_splitlines(transform(home_file))) assert len(got) == len(expect) def test_iter_line_blocks_correct_size(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value)) got = list(iter_line_blocks(path, num_lines=2, chunk_size=5)) expect = [value[:2], value[-1:]] assert got == expect def test_iter_line_blocks_empty(tmp_path): path = tmp_path / "zero.txt" path.write_text("") got = list(iter_line_blocks(path, num_lines=2)) assert not got def test_iter_line_blocks_one(tmp_path): path = tmp_path / "one-line.txt" value = "We have text on one line." path.write_text(value) got = list(iter_line_blocks(path, num_lines=2)) assert got == [[value]] def test_iter_line_blocks_as_bytes_correct_size(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value)) got = list(iter_line_blocks(path, num_lines=2, chunk_size=5, as_bytes=True)) expect = [line.encode("utf8") for line in value] assert got == [expect[:2], expect[-1:]] def test_iter_line_blocks_as_bytes_empty(tmp_path): path = tmp_path / "zero.txt" path.write_text("") got = list(iter_line_blocks(path, num_lines=2, as_bytes=True)) assert not got def test_iter_line_blocks_as_bytes_none_num_lines(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value)) got = list(iter_line_blocks(path, num_lines=None, as_bytes=True)) assert got == [[line.encode("utf8") for line in value]] def test_iter_line_blocks_none_num_lines(tmp_path): path = tmp_path / "multi-line.txt" value = ["We have some", "text on different lines", "which load"] path.write_text("\n".join(value)) got = list(iter_line_blocks(path, num_lines=None)) expect = [value] assert got == expect class _RecordingReader: """wraps a reader and records the bytes of the file each read took A text read reports what the characters it returned cost in the file, which is the budget chunk_size is meant to be setting. """ def __init__(self, handle, taken): self._handle = handle self._taken = taken def read(self, size=-1): data = self._handle.read(size) # a text handle reports its own encoding, which is what turns # the characters it returned back into a count of file bytes taken = ( len(data) if isinstance(data, bytes) else len(data.encode(self.encoding)) ) self._taken.append(taken) return data @property def encoding(self): return self._handle.encoding def __enter__(self): return self def __exit__(self, *args): self._handle.close() @pytest.fixture def bytes_taken(monkeypatch): """records the bytes of the file each read consumed""" taken = [] real_open = scinexus.io_util.open_ def recording_open(filename, mode="rt", **kwargs): return _RecordingReader(real_open(filename, mode, **kwargs), taken) monkeypatch.setattr(scinexus.io_util, "open_", recording_open) return taken # ascii mixed with CJK, at 1.8 bytes per character. Plain CJK would be # 3 bytes per character, but charset_normalizer reads the first 100 # bytes of it as cp874, a single byte encoding, and then a read of n # characters costs n bytes and there is no overshoot left to measure MIXED_LINE = "id\t\N{CJK UNIFIED IDEOGRAPH-6F22}\N{CJK UNIFIED IDEOGRAPH-5B57}" * 30 def test_iter_splitlines_chunk_size_is_bytes_in_text_mode(tmp_path, bytes_taken): """a text read spends chunk_size on bytes, not on characters read(n) on a text handle returns n characters, so a utf-8 file of mixed ascii and CJK takes about 1.8 times chunk_size bytes of the file per chunk, which is that much more memory than the caller budgeted for. Plain CJK is 3 times. """ path = tmp_path / "mixed.txt" path.write_text("\n".join([MIXED_LINE] * 40), encoding="utf-8") chunk_size = 3000 lines = list(iter_splitlines(path, chunk_size=chunk_size)) assert lines == [MIXED_LINE] * 40 assert max(bytes_taken) <= chunk_size def test_iter_splitlines_chunk_size_survives_compression(tmp_path, bytes_taken): """a compressed file is still read in chunks st_size of a compressed file is the compressed size, so comparing it against chunk_size switched chunking off for anything that compressed below the budget, however large it was uncompressed. """ path = tmp_path / "repetitive.txt.gz" with open_(path, mode="wt") as outfile: outfile.write("\n".join(["x" * 99] * 20_000)) chunk_size = 100_000 assert path.stat().st_size < chunk_size lines = list(iter_splitlines(path, chunk_size=chunk_size)) assert lines == ["x" * 99] * 20_000 assert max(bytes_taken) <= chunk_size def test_iter_record_chunks_chunk_size_survives_compression(tmp_path, bytes_taken): """the same st_size shortcut is in iter_record_chunks""" path = tmp_path / "repetitive.bin.gz" with open_(path, mode="wb") as outfile: outfile.write(b">record\n" + b"A" * 99 + b">record\n" + b"A" * 400_000) chunk_size = 50_000 assert path.stat().st_size < chunk_size got = list(iter_record_chunks(path=path, delimiter=b">", chunk_size=chunk_size)) assert got == [b"", b"record\n" + b"A" * 99, b"record\n" + b"A" * 400_000] assert max(bytes_taken) <= chunk_size @pytest.mark.parametrize("chunk_size", [1, 2, 3, 4, 5, 7]) def test_iter_splitlines_multi_byte_across_a_chunk_boundary(tmp_path, chunk_size): """a character split by a chunk boundary is not lost or doubled A chunk ending part way through a three-byte character decodes to the empty string, which a reader must not take for the end of the file. """ path = tmp_path / "cjk.txt" expect = ["\N{CJK UNIFIED IDEOGRAPH-6F22}\N{CJK UNIFIED IDEOGRAPH-5B57}", "ab"] path.write_text("\n".join(expect), encoding="utf-8") assert list(iter_splitlines(path, chunk_size=chunk_size)) == expect def test_iter_splitlines_strips_a_byte_order_mark(tmp_path): """a BOM is consumed by the codec, not yielded as part of the line charset_normalizer answers UTF-8-SIG for a file with one, and it is that codec rather than plain utf-8 that takes the mark off. """ path = tmp_path / "bom.txt" path.write_text("first\nsecond", encoding="utf-8-sig") assert list(iter_splitlines(path, chunk_size=4)) == ["first", "second"] def _outcome(read): """what a read returned, or the name of the decoding error it raised Only a decoding failure becomes a value to compare, since that is the one outcome the two readers are allowed to differ on. Anything else, from either of them, goes on and fails the test rather than being turned into a value that the other side might match. """ try: return ("read", read()) except UnicodeDecodeError as e: return ("raised", type(e).__name__) def _agrees_with_open(path, **kwargs): """whether iter_splitlines and open_ give the same lines or error""" def via_open(): with open_(path) as infile: return infile.read().splitlines() return _outcome(lambda: list(iter_splitlines(path, **kwargs))) == _outcome(via_open) @pytest.mark.parametrize("suffix", ["bin", "gz", "bz2", "xz", "lzma", "zip"]) def test_iter_splitlines_unsniffable_file_behaves_like_open(tmp_path, suffix): """bytes no sniff can name are handled the way open_ handles them charset_normalizer answers None for these, and a None handed to codecs.getincrementaldecoder is a TypeError, where open_ passes it on to the opener. Which fallback that means depends on the suffix: every opener but one takes None as the locale default, and open_zip substitutes latin-1, which decodes any byte. So a zip of these bytes reads as text where the others raise, and this has to follow it. What is asserted is that the two agree, not which of the two outcomes happens, so a locale that decodes these bytes rather than raising on them still exercises the same thing. """ path = tmp_path / f"raw.{suffix}" with open_(path, mode="wb") as outfile: outfile.write(b"\xff\xfe\x00\x80 raw \r\n bytes") assert _agrees_with_open(path) @pytest.mark.parametrize("chunk_size", [8, 64, None]) def test_iter_splitlines_truncated_character_behaves_like_open(tmp_path, chunk_size): """a file ending part way through a character is not quietly cut The decoder is told the last chunk is the last one, and it reports the held-back bytes it can no longer complete. Without that it would return what it had and the truncated tail would vanish, which is a wrong answer rather than an error. """ path = tmp_path / "truncated.txt" body = "\n".join([MIXED_LINE] * 5).encode("utf-8") path.write_bytes(body[:-1]) assert _agrees_with_open(path, chunk_size=chunk_size) @pytest.mark.parametrize("suffix", ["gz", "bz2", "xz", "lzma", "zip", "tsv"]) def test_iter_splitlines_text_agrees_with_open_for_every_suffix(tmp_path, suffix): """the lines are what reading the whole thing and splitting gives""" path = tmp_path / f"sample.tsv.{suffix}" # the encoding is named because the content is not ascii and a text # write that is given none uses the locale's, which on Windows is # cp1252 and cannot encode CJK at all with open_(path, mode="wt", encoding="utf-8") as outfile: outfile.write("\n".join([MIXED_LINE] * 20)) assert _agrees_with_open(path, chunk_size=64) @pytest.mark.parametrize("num_lines", [0, -1, 1.5]) def test_iter_line_blocks_unusable_num_lines(tmp_path, num_lines): """a block size that cannot be met is an error, not the whole file len(lines) == num_lines never holds for any of these, so the whole file came back as one block, which is what num_lines=None asks for and the opposite of what a small number asks for. A fraction is in the list because nothing downstream of num_lines would catch one. """ path = tmp_path / "multi-line.txt" path.write_text("a\nb\nc\n") with pytest.raises(ValueError, match="num_lines"): list(iter_line_blocks(path, num_lines=num_lines)) def test_iter_line_blocks_num_lines_one(tmp_path): """the smallest usable block size still works The guard above has to stop below one without taking one with it. """ path = tmp_path / "multi-line.txt" path.write_text("a\nb\nc\n") assert list(iter_line_blocks(path, num_lines=1)) == [["a"], ["b"], ["c"]] @pytest.mark.parametrize("chunk_size", [0, -1]) @pytest.mark.parametrize("as_bytes", [False, True]) def test_iter_splitlines_non_positive_chunk_size(tmp_path, chunk_size, as_bytes): """zero and a negative fail in opposite directions, neither loudly read(0) returns an empty string, which the loop takes for the end of the file, so nothing at all was yielded for a file that plainly has lines. read(-1) reads to the end, so the whole file arrived in one chunk, which is what None already means and the opposite of the bounded memory the argument asks for. """ path = tmp_path / "multi-line.txt" path.write_text("a\nb\nc\n") with pytest.raises(ValueError, match="chunk_size"): list(iter_splitlines(path, chunk_size=chunk_size, as_bytes=as_bytes)) @pytest.mark.parametrize("chunk_size", [0, -1]) def test_iter_record_chunks_non_positive_chunk_size(tmp_path, chunk_size): """zero yields nothing and a negative reads the lot, neither loudly""" path = tmp_path / "records.bin" path.write_bytes(b">a\nAAA>b\nBBB") with pytest.raises(ValueError, match="chunk_size"): list(iter_record_chunks(path=path, delimiter=b">", chunk_size=chunk_size)) @pytest.mark.parametrize("chunk_size", [0, -1]) def test_iter_line_blocks_non_positive_chunk_size(tmp_path, chunk_size): """the chunk_size check reaches through iter_line_blocks It delegates the reading, so it needs no check of its own for this. """ path = tmp_path / "multi-line.txt" path.write_text("a\nb\nc\n") with pytest.raises(ValueError, match="chunk_size"): list(iter_line_blocks(path, chunk_size=chunk_size)) def test_iter_splitlines_chunk_size_checked_before_the_path(tmp_path): """a bad chunk_size is reported as one, not as a missing file The check sits above the stat, so the argument the caller got wrong is what they are told about rather than the path they would have got wrong next. """ missing = tmp_path / "does-not-exist.txt" with pytest.raises(ValueError, match="chunk_size"): list(iter_splitlines(missing, chunk_size=0)) with pytest.raises(ValueError, match="chunk_size"): list(iter_record_chunks(path=missing, delimiter=b">", chunk_size=0)) @pytest.mark.parametrize( "url", [ "http://example.com", b"file://example.txt", pathlib.Path("example.txt").absolute().as_uri(), ], ) def test_is_url(url): assert is_url(url) @pytest.mark.parametrize( "url", [ "example.txt", pathlib.Path("example.txt"), b"example.txt", r"D:\foo\example.txt", ], ) def test_not_is_url(url): assert not is_url(url) def test_open_url_local(DATA_DIR, tmp_path): """using file:///""" file_name = "sample.tsv" local_path = DATA_DIR / file_name with open_(local_path) as infile: local_data = infile.read() with open_url(local_path.absolute().as_uri()) as infile: remote_data = infile.read() assert remote_data.splitlines() == local_data.splitlines() COMPRESSED_SAMPLE = "id\tname\n1\talpha\n2\tbeta\n3\tgamma\n" @pytest.fixture(params=["gz", "bz2", "zip", "lzma", "xz"]) def compressed_path(tmp_path, request): """path to text written under each compressed suffix The content is ascii deliberately. open_ sniffs the encoding of a local file while open_url takes it from the response headers, which a file:// url does not set, so the two sides only agree on content that decodes the same way under any locale. """ outpath = tmp_path / f"sample.tsv.{request.param}" with open_(outpath, "wt") as outfile: outfile.write(COMPRESSED_SAMPLE) return outpath @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_uses_given_encoding(tmp_path, suffix): """an encoding argument is used instead of sniffing the content The text is written as cp1252, which is not valid utf-8, so reading it back correctly is only possible with the encoding the caller names. """ outpath = tmp_path / f"sample.tsv.{suffix}" text = "temperature 20\N{DEGREE SIGN}C \N{EURO SIGN}5" with open_(outpath, mode="wt", encoding="cp1252") as outfile: outfile.write(text) # the bytes are checked as well as the round trip, since a round # trip alone cannot tell an encoding that was honoured from one that # was ignored the same way on both sides with open_(outpath, mode="rb") as infile: assert infile.read() == text.encode("cp1252") with open_(outpath, mode="rt", encoding="cp1252") as infile: assert infile.read() == text @pytest.mark.parametrize("mode", ["w", "wt", "wb"]) @pytest.mark.parametrize("suffix", ["gz", "bz2", "zip", "lzma", "xz", "tsv"]) def test_open_write_modes(tmp_path, suffix, mode): """a bare w writes text, as it does for an uncompressed file Writing a str to a handle that turned out to be binary raises TypeError, so a round trip with a payload chosen to match the mode is what pins text against binary. """ outpath = tmp_path / f"sample.tsv.{suffix}" binary = "b" in mode payload = COMPRESSED_SAMPLE.encode("utf-8") if binary else COMPRESSED_SAMPLE with open_(outpath, mode=mode) as outfile: outfile.write(payload) with open_(outpath, mode="rb" if binary else "rt") as infile: assert infile.read() == payload def test_open_compressed_bare_read_mode(compressed_path): """a bare r reads text, as it does for an uncompressed file gzip, bz2 and lzma read a bare r as binary where builtin open and open_zip read it as text, so open_ has to say which it means. """ with open_(compressed_path, mode="r") as infile: got = infile.read() assert got == COMPRESSED_SAMPLE @pytest.mark.parametrize("mode", ["r", "rb", "rt"]) def test_open_url_compressed_local(compressed_path, mode): """a compressed file:// url reads the same as the local path zip is the regression guarded here, it was the only suffix whose handler defaulted to text rather than bytes. """ # open_url gives bytes only when "b" is in the mode, so "r" reads as # text. The local read spells that out because open_(path, "r") is # separately broken for gz, bz2, xz and lzma. with open_(compressed_path, mode="rb" if "b" in mode else "rt") as infile: expect = infile.read() with open_url(compressed_path.as_uri(), mode=mode) as infile: got = infile.read() assert got == expect @pytest.mark.parametrize("compressed_path", ["zip"], indirect=True) @pytest.mark.parametrize("mode", ["rb", "rt"]) def test_open_compressed_url(compressed_path, mode): """open_ hands a compressed url to open_url Only zip is worth running here. open_ delegates any url whole, so the suffix fan-out belongs to test_open_url_compressed_local and repeating it would add cases that cannot fail. """ with open_(compressed_path, mode=mode) as infile: expect = infile.read() with open_(compressed_path.as_uri(), mode=mode) as infile: got = infile.read() assert got == expect @pytest.mark.parametrize("compressed_path", ["zip"], indirect=True) @pytest.mark.parametrize("as_bytes", [False, True]) def test_iter_splitlines_compressed_url(compressed_path, as_bytes): """a consumer of open_ reaches compressed urls too""" expect = list(iter_splitlines(compressed_path, as_bytes=as_bytes)) got = list(iter_splitlines(compressed_path.as_uri(), as_bytes=as_bytes)) assert got == expect @pytest.mark.slow @pytest.mark.parametrize( "mode", ["r", "rb", "rt", None], ) @pytest.mark.internet def test_open_url(DATA_DIR, mode): """different open mode's all work""" file_name = "formattest.fasta" remote_root = "https://github.com/user-attachments/files/20321056/{}.gz" with open_(DATA_DIR / file_name, mode=mode) as infile: local_data = infile.read() with open_url(remote_root.format(file_name), mode=mode) as infile: remote_data = infile.read() assert remote_data.splitlines() == local_data.splitlines() # Test using a ParseResult for url with open_url(urlparse(remote_root.format(file_name)), mode=mode) as infile: remote_data = infile.read() assert remote_data.splitlines() == local_data.splitlines() @pytest.mark.slow @pytest.mark.internet def test_open_url_compressed(DATA_DIR): """comparing compressed file handling""" file_name = "formattest.fasta.gz" remote_root = "https://github.com/user-attachments/files/20321056/{}" with open_(DATA_DIR / file_name) as infile: local_data = infile.read() with open_url(remote_root.format(file_name), mode="rt") as infile: remote_data = infile.read() assert remote_data.splitlines() == local_data.splitlines() def test_get_compression_open_no_args(): from scinexus.io_util import _get_compression_open with pytest.raises(ValueError, match="either path or compression"): _get_compression_open() def test_open_via_url(DATA_DIR): uri = (DATA_DIR / "sample.tsv").absolute().as_uri() with open_(uri) as infile: got = infile.read() assert len(got) > 0 def test_atomic_write_tmpdir_not_exist(tmp_path): from scinexus.io_util import atomic_write bad_tmpdir = tmp_path / "nonexistent_tmpdir" with pytest.raises(FileNotFoundError, match="does not exist"): atomic_write(tmp_path / "test.txt", tmpdir=bad_tmpdir, mode="w") def test_close_rename_zip_in_zip_none(tmp_path): path = tmp_path / "test.txt" zip_path = tmp_path / "test.zip" aw = atomic_write(path, in_zip=zip_path, mode="w") aw._in_zip = None with pytest.raises(RuntimeError, match="in_zip path is unexpectedly None"): aw._close_rename_zip(aw._tmppath) def test_atomic_write_exit_without_enter(tmp_path): aw = atomic_write(tmp_path / "test.txt", mode="w") with pytest.raises(ValueError, match="file object is unexpectedly None"): aw.__exit__(None, None, None) def test_iter_splitlines_url(DATA_DIR): uri = (DATA_DIR / "sample.tsv").absolute().as_uri() got = list(iter_splitlines(uri)) assert len(got) > 0 def test_iter_splitlines_url_as_bytes(DATA_DIR): path = (DATA_DIR / "sample.tsv").absolute() uri = path.as_uri() got = list(iter_splitlines(uri, as_bytes=True)) assert got == [line.encode("utf8") for line in iter_splitlines(path)] @pytest.mark.parametrize("chunk_size", [1, 16, 64, 1024, 5_000_000]) def test_iter_record_chunks_chunk_size_independence(tmp_path, chunk_size): delim = b"\n//" data = b"record1\n//record2 is longer\n//record3" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=delim, chunk_size=chunk_size)) assert got == [b"record1", b"record2 is longer", b"record3"] def test_iter_record_chunks_delimiter_spans_chunk_boundary(tmp_path): delim = b"\n//" data = b"AAAAA" + delim + b"BBBBB" + delim + b"CCCCC" path = tmp_path / "records.bin" path.write_bytes(data) chunk_size = data.index(delim) + 1 got = list(iter_record_chunks(path=path, delimiter=delim, chunk_size=chunk_size)) assert got == [b"AAAAA", b"BBBBB", b"CCCCC"] def test_iter_record_chunks_record_larger_than_chunk(tmp_path): delim = b">" record = b"x" * 1000 data = delim + record + delim + b"short" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=delim, chunk_size=16)) assert got == [b"", record, b"short"] def test_iter_record_chunks_chunk_size_one(tmp_path): delim = b">" data = b">a>b>c" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=delim, chunk_size=1)) assert got == [b"", b"a", b"b", b"c"] def test_iter_record_chunks_ends_on_delimiter(tmp_path): delim = b"\n//" data = b"record1\n//record2\n//" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=delim, chunk_size=8)) assert got == [b"record1", b"record2"] def test_iter_record_chunks_no_delimiter(tmp_path): data = b"no delimiter present at all" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=b">", chunk_size=8)) assert got == [data] def test_iter_record_chunks_empty_file(tmp_path): path = tmp_path / "empty.bin" path.write_bytes(b"") got = list(iter_record_chunks(path=path, delimiter=b">")) assert got == [] def test_iter_record_chunks_empty_delimiter_raises(tmp_path): path = tmp_path / "records.bin" path.write_bytes(b"anything") with pytest.raises(ValueError, match="delimiter must be non-empty"): list(iter_record_chunks(path=path, delimiter=b"")) @pytest.mark.parametrize("compression", ["gz", "bz2"]) def test_iter_record_chunks_compressed(tmp_path, compression): data = b">a\nAAA>b\nBBB>c\nCCC" path = tmp_path / f"records.bin.{compression}" with open_(path, mode="wb") as f: f.write(data) got = list(iter_record_chunks(path=path, delimiter=b">", chunk_size=4)) assert got == [b"", b"a\nAAA", b"b\nBBB", b"c\nCCC"] @pytest.mark.parametrize("chunk_size", [None, 5_000_000]) def test_iter_record_chunks_read_all(tmp_path, chunk_size): data = b">a>b>c" path = tmp_path / "records.bin" path.write_bytes(data) got = list(iter_record_chunks(path=path, delimiter=b">", chunk_size=chunk_size)) assert got == [b"", b"a", b"b", b"c"] def test_iter_record_chunks_url(tmp_path): data = b">a\nAAA>b\nBBB>c\nCCC" src = tmp_path / "records.bin" src.write_bytes(data) uri = src.absolute().as_uri() got = list(iter_record_chunks(path=uri, delimiter=b">", chunk_size=4)) assert got == [b"", b"a\nAAA", b"b\nBBB", b"c\nCCC"] cogent3-scinexus-b16bc06/tests/test_misc.py000066400000000000000000000067631525761402500210510ustar00rootroot00000000000000from gzip import GzipFile, compress import pytest import scinexus.misc as misc_module from scinexus.misc import ( docstring_to_summary_rest, extend_docstring_from, get_object_provenance, in_jupyter, ) def test_not_in_jupyter(): assert not in_jupyter() def test_is_in_jupyter(): misc_module.get_ipython = lambda: None try: assert in_jupyter() finally: del misc_module.get_ipython def test_get_object_provenance_builtin_instance(): assert get_object_provenance("abc") == "str" assert get_object_provenance({"a": 1}) == "dict" def test_get_object_provenance_builtin_type(): assert get_object_provenance(dict) == "dict" assert get_object_provenance(str) == "str" def test_get_object_provenance_function(): assert get_object_provenance(compress) == "gzip.compress" def test_get_object_provenance_class(): assert get_object_provenance(GzipFile) == "gzip.GzipFile" def _source(): """This is a source docstring.""" def test_extend_docstring_from_append(): @extend_docstring_from(_source) def target(): """I am target.""" assert target.__doc__ == "This is a source docstring.\nI am target." def test_extend_docstring_from_prepend(): @extend_docstring_from(_source, pre=True) def target(): """I am target.""" assert target.__doc__ == "I am target.\nThis is a source docstring." def test_extend_docstring_from_no_dest_doc(): @extend_docstring_from(_source) def target(): pass assert target.__doc__ == "This is a source docstring.\n" def test_extend_docstring_from_method_append(): class C: @extend_docstring_from(_source) def target(self): """I am target.""" assert C.target.__doc__ == "This is a source docstring.\nI am target." def test_extend_docstring_from_method_prepend(): class C: @extend_docstring_from(_source, pre=True) def target(self): """I am target.""" assert C.target.__doc__ == "I am target.\nThis is a source docstring." def test_extend_docstring_from_method_no_dest_doc(): class C: @extend_docstring_from(_source) def target(self): pass assert C.target.__doc__ == "This is a source docstring.\n" class _SourceClass: """This is a class docstring.""" def test_extend_docstring_from_class_append(): @extend_docstring_from(_SourceClass) class Target: """I am target.""" assert Target.__doc__ == "This is a class docstring.\nI am target." def test_extend_docstring_from_class_prepend(): @extend_docstring_from(_SourceClass, pre=True) class Target: """I am target.""" assert Target.__doc__ == "I am target.\nThis is a class docstring." def test_extend_docstring_from_class_no_dest_doc(): @extend_docstring_from(_SourceClass) class Target: pass assert Target.__doc__ == "This is a class docstring.\n" def _foo1(): """some text""" def _foo2(): """some text Notes ----- body """ def _foo3(): """ Notes ----- body """ def _foo4(): ... @pytest.mark.parametrize( ("func", "sum_exp", "body_exp"), [ (_foo1, "some text", []), (_foo2, "some text", ["Notes", "-----", "body"]), (_foo3, "", ["Notes", "-----", "body"]), (_foo4, "", []), ], ) def test_docstring_to_summary_rest(func, sum_exp, body_exp): summary, body = docstring_to_summary_rest(func.__doc__) assert summary == sum_exp assert body.split() == body_exp cogent3-scinexus-b16bc06/tests/test_mypy_typing.py000066400000000000000000000101441525761402500224720ustar00rootroot00000000000000"""Tests that type-checker support works for both inheritance and decorator paths.""" import subprocess import sys import textwrap import pytest from scinexus import ComposableApp, NonComposableApp from scinexus.composable import ( NON_COMPOSABLE, NotCompleted, NotCompletedType, define_app, ) class IntToStr(ComposableApp[int, str]): def main(self, val: int) -> str: return str(val) class StrToInt(ComposableApp[str, int]): def main(self, val: str) -> int: return int(val) class NonComp(NonComposableApp[int, int]): def main(self, val: int) -> int: return val * 2 def test_inheritance_basic_call(): app = IntToStr() result = app(42) assert result == "42" def test_inheritance_composition(): app = IntToStr() + StrToInt() result = app(42) assert result == 42 def test_inheritance_not_completed_propagation(): app = IntToStr() nc = NotCompleted(NotCompletedType.ERROR, "test", "msg") result = app(nc) assert isinstance(result, NotCompleted) def test_inheritance_non_composable(): app = NonComp() assert app(5) == 10 assert not hasattr(app, "__add__") or app.app_type is NON_COMPOSABLE def test_inheritance_repr(): app = IntToStr() assert "IntToStr()" in repr(app) def test_inheritance_type_validation(): app = IntToStr() result = app("wrong type") assert isinstance(result, NotCompleted) def test_inheritance_init_vals(): class WithInit(ComposableApp[int, int]): def __init__(self, factor: int = 1): self.factor = factor def main(self, val: int) -> int: return val * self.factor app = WithInit(factor=3) assert app._init_vals == {"factor": 3} assert app(5) == 15 def test_inheritance_citations(): from citeable import Software cite = Software( author=["Doe, J"], title="test", year=2024, url="https://example.com", version="1.0", license="MIT", doi="10.0/test", publisher="test", ) class Cited(ComposableApp[int, int], cite=cite): def main(self, val: int) -> int: return val app = Cited() assert app.citations == (cite,) assert cite.app == "Cited" def test_inheritance_pickle(): import pickle app = IntToStr() data = pickle.dumps(app) restored = pickle.loads(data) assert restored(42) == "42" def test_decorator_still_works(): """define_app decorator continues to produce working apps.""" @define_app class dec_app: def main(self, val: int) -> int: return val + 1 app = dec_app() assert app(1) == 2 def test_decorator_composable_with_inheritance(): """Decorator-created and inheritance-created apps compose together.""" @define_app class adder: def main(self, val: int) -> int: return val + 1 composed = adder() + IntToStr() assert composed(1) == "2" def _run_mypy(code: str, tmp_path) -> tuple[int, str]: src = tmp_path / "check.py" src.write_text(code) result = subprocess.run( [sys.executable, "-m", "mypy", "--no-error-summary", str(src)], capture_output=True, text=True, check=False, ) return result.returncode, result.stdout + result.stderr @pytest.mark.slow def test_mypy_decorator_reveal_type(tmp_path): code = textwrap.dedent("""\ from scinexus.composable import define_app @define_app class MyApp: def main(self, val: int) -> str: return str(val) app = MyApp() reveal_type(app(42)) """) _, output = _run_mypy(code, tmp_path) assert "str" in output or "Union" in output def test_mypy_inheritance_reveal_type(tmp_path): code = textwrap.dedent("""\ from scinexus import ComposableApp class MyApp(ComposableApp[int, str]): def main(self, val: int) -> str: return str(val) app = MyApp() reveal_type(app(42)) """) _, output = _run_mypy(code, tmp_path) # Should see str (or Union[str, NotCompleted]) — not Any assert "str" in output cogent3-scinexus-b16bc06/tests/test_parallel.py000066400000000000000000001062661525761402500217110ustar00rootroot00000000000000import multiprocessing import os import sys import threading import time from collections.abc import Generator from unittest.mock import patch import numpy import pytest from scinexus import parallel from scinexus.parallel import ( BACKEND_TYPES, LokyBackend, MPIBackend, MultiprocessBackend, Parallel, PicklableAndCallable, ThreadBackend, _clamp_max_workers_local, _effective_backend, _get_rank_thread, _gil_enabled, _resolve_chunksize, _resolve_max_workers_local, _resolve_max_workers_mpi, _worker_budget, as_completed, get_default_chunksize, get_parallel_backend, get_size, set_parallel_backend, ) @pytest.fixture(autouse=True) def _reset_backend(): """Reset the module-level default after each test.""" yield set_parallel_backend(None) def get_process_value(n): # Sleep to accommodate Windows process creation overhead time.sleep(1) return (parallel.get_rank(), n) def get_ranint(n): numpy.random.seed(n) return numpy.random.randint(1, 10) def check_is_master_process(n): return parallel.is_master_process() def _double(x): return x * 2 def _getpid(_): return os.getpid() def test_parallel_backend_abc_cannot_instantiate(): """Parallel cannot be instantiated directly""" with pytest.raises(TypeError): Parallel() def test_parallel_backend_abc_missing_methods(): """incomplete subclass raises TypeError""" class Incomplete(Parallel): def imap(self, f, s, max_workers=None, **kwargs): yield from () with pytest.raises(TypeError): Incomplete() def test_set_parallel_backend_multiprocess(): """setting 'multiprocess' returns MultiprocessBackend""" set_parallel_backend("multiprocess") assert isinstance(get_parallel_backend(), MultiprocessBackend) def test_set_parallel_backend_loky(): """setting 'loky' returns LokyBackend""" set_parallel_backend("loky") assert isinstance(get_parallel_backend(), LokyBackend) def test_set_parallel_backend_none_resets(monkeypatch): """None resets to default The probe is pinned so this tests the reset rather than which build is running the suite. """ monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) set_parallel_backend("loky") set_parallel_backend(None) assert isinstance(get_parallel_backend(), MultiprocessBackend) def test_set_parallel_backend_custom_instance(): """accepts a Parallel instance""" class Custom(Parallel): def imap(self, f, s, max_workers=None, **kwargs): yield from () def as_completed(self, f, s, max_workers=None, **kwargs): yield from () def is_master_process(self): return True def get_rank(self): return 0 def get_size(self): return 1 custom = Custom() set_parallel_backend(custom) assert get_parallel_backend() is custom def test_set_parallel_backend_invalid_string(): """invalid string raises ValueError""" with pytest.raises(ValueError, match="unknown backend"): set_parallel_backend("invalid") # type: ignore def test_set_parallel_backend_loky_not_installed(): """set_parallel_backend('loky') raises ImportError when loky is missing""" with patch.dict("sys.modules", {"loky": None}), pytest.raises(ImportError): set_parallel_backend("loky") def test_set_parallel_backend_mpi_not_available(): """set_parallel_backend('mpi') raises ImportError when mpi4py is missing""" with patch.object(parallel, "MPI", None), pytest.raises(ImportError): set_parallel_backend("mpi") def test_get_parallel_backend_default(monkeypatch): """returns MultiprocessBackend when nothing set and the GIL is in force""" monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) set_parallel_backend(None) assert isinstance(get_parallel_backend(), MultiprocessBackend) def test_get_parallel_backend_caches(): """get_parallel_backend caches the default instance""" set_parallel_backend(None) b1 = get_parallel_backend() b2 = get_parallel_backend() assert b1 is b2 def test_get_parallel_backend_with_backend_multiprocess(): """returns a MultiprocessBackend when backend='multiprocess'""" assert isinstance(get_parallel_backend(backend="multiprocess"), MultiprocessBackend) def test_get_parallel_backend_with_backend_loky(): """returns a LokyBackend when backend='loky'""" assert isinstance(get_parallel_backend(backend="loky"), LokyBackend) def test_get_parallel_backend_with_backend_does_not_modify_default(): """passing backend does not change the global default""" set_parallel_backend("multiprocess") default_before = get_parallel_backend() get_parallel_backend(backend="loky") default_after = get_parallel_backend() assert default_before is default_after assert isinstance(default_after, MultiprocessBackend) def test_get_parallel_backend_with_backend_none(): """backend=None returns the current default""" set_parallel_backend("loky") assert isinstance(get_parallel_backend(backend=None), LokyBackend) def test_effective_backend_caches_mpi(): """_effective_backend caches the MPIBackend instance""" mock_mpi = type("FakeMPI", (), {"COMM_WORLD": None}) with ( patch.object(parallel, "USING_MPI", True), patch.object(parallel, "MPI", mock_mpi), patch.object(parallel, "_mpi_backend", None), patch.object(MPIBackend, "__init__", lambda self: None), ): b1 = _effective_backend() b2 = _effective_backend() assert b1 is b2 assert isinstance(b1, MPIBackend) def test_multiprocess_imap(): """MultiprocessBackend.imap returns ordered results""" backend = MultiprocessBackend() data = list(range(10)) result = list(backend.imap(_double, data, max_workers=1)) assert result == [x * 2 for x in data] def test_multiprocess_as_completed(): """MultiprocessBackend.as_completed returns all results""" backend = MultiprocessBackend() data = list(range(10)) result = sorted(backend.as_completed(_double, data)) assert result == sorted(x * 2 for x in data) def test_multiprocess_is_master_process(): """MultiprocessBackend.is_master_process returns True in main""" backend = MultiprocessBackend() assert backend.is_master_process() def test_multiprocess_get_rank(): """MultiprocessBackend.get_rank returns 0 in main process""" backend = MultiprocessBackend() assert backend.get_rank() == 0 def test_multiprocess_get_size(): """MultiprocessBackend.get_size returns cpu_count""" backend = MultiprocessBackend() assert backend.get_size() == multiprocessing.cpu_count() def test_multiprocess_max_workers_too_large(): """max_workers > cpu_count raises ValueError""" backend = MultiprocessBackend() n = multiprocessing.cpu_count() + 1 with pytest.raises(ValueError, match="max_workers"): list(backend.imap(_double, [1], max_workers=n)) def test_multiprocess_non_sized_iterable(): """imap with a generator defaults chunksize to 1""" backend = MultiprocessBackend() def gen(): yield from range(4) result = list(backend.imap(_double, gen(), max_workers=1)) assert sorted(result) == [0, 2, 4, 6] def test_multiprocess_as_completed_max_workers_clamped(): """large max_workers gets clamped""" backend = MultiprocessBackend() data = list(range(4)) result = sorted(backend.as_completed(_double, data, max_workers=9999)) assert result == sorted(x * 2 for x in data) def test_clamp_max_workers_local_valid(): """valid max_workers is returned unchanged""" result = _clamp_max_workers_local(1) assert result == 1 def test_clamp_max_workers_local_too_large(): """max_workers exceeding cpu_count is clamped to cpu_count""" cpu = multiprocessing.cpu_count() result = _clamp_max_workers_local(cpu + 1) assert result == cpu @pytest.mark.parametrize( "resolve", [_resolve_max_workers_local, _clamp_max_workers_local] ) @pytest.mark.parametrize("max_workers", [-1, 0]) def test_max_workers_below_one_refused(resolve, max_workers): """a worker count below one is named in the message we raise""" with pytest.raises(ValueError, match=rf"max_workers \({max_workers}\)"): resolve(max_workers) @pytest.mark.parametrize( "resolve", [_resolve_max_workers_local, _clamp_max_workers_local] ) def test_max_workers_none_is_the_default(resolve): """None still asks for one worker per cpu""" assert resolve(None) == multiprocessing.cpu_count() @pytest.mark.parametrize( "resolve", [_resolve_max_workers_local, _clamp_max_workers_local] ) @pytest.mark.parametrize("max_workers", [True, False]) def test_max_workers_bool_refused(resolve, max_workers): """a bool is not a worker count, and None is how to ask for every cpu""" with pytest.raises(TypeError, match="must be an int or None"): resolve(max_workers) @pytest.mark.parametrize( "resolve", [_resolve_max_workers_local, _clamp_max_workers_local] ) @pytest.mark.parametrize("max_workers", [numpy.True_, numpy.False_, 2.5]) def test_max_workers_non_integer_refused(resolve, max_workers): """what is not a whole number is refused as a type, not as a range""" with pytest.raises(TypeError, match="must be an int or None"): resolve(max_workers) @pytest.mark.parametrize( "resolve", [_resolve_max_workers_local, _clamp_max_workers_local] ) def test_max_workers_numpy_integer_accepted(resolve): """a numpy integer is a worker count, and reaches the executor as an int""" got = resolve(numpy.int64(1)) assert got == 1 assert type(got) is int @pytest.mark.parametrize(("world_size", "expect"), [(4, 3), (8, 7), (2, 1), (1, 1)]) def test_worker_budget(world_size, expect): """the budget is the ranks launched, less the master These values come from mpi4py's own num_workers, which this test does not consult. test_mpi_get_size_is_the_pool_the_job_has does. """ assert _worker_budget(world_size) == expect def test_resolve_max_workers_mpi_none_takes_the_pool(): """None accepts the pool the job was launched with""" assert _resolve_max_workers_mpi(None, 7) == 7 assert _resolve_max_workers_mpi(None, 1) == 1 def test_resolve_max_workers_mpi_matching_request_is_quiet(): """asking for exactly what the job has is not worth a warning""" assert _resolve_max_workers_mpi(7, 7) == 7 @pytest.mark.parametrize("max_workers", [3, 8]) def test_resolve_max_workers_mpi_reports_a_request_it_cannot_meet(max_workers): """a count that is not the pool size is reported, above or below it""" with pytest.warns(UserWarning, match="this request is not used"): assert _resolve_max_workers_mpi(max_workers, 7) == 7 @pytest.mark.parametrize("max_workers", [-1, 0]) def test_resolve_max_workers_mpi_below_one_refused(max_workers): """MPI refuses a worker count below one, as the local backends do""" with pytest.raises(ValueError, match=rf"max_workers \({max_workers}\)"): _resolve_max_workers_mpi(max_workers, 8) @pytest.mark.parametrize("max_workers", [True, numpy.True_, 2.5]) def test_resolve_max_workers_mpi_non_integer_refused(max_workers): """MPI refuses what is not a worker count, as the local backends do""" with pytest.raises(TypeError, match="max_workers must be an int or None"): _resolve_max_workers_mpi(max_workers, 8) def test_resolve_max_workers_mpi_accepts_a_numpy_count(): """a numpy integer equal to the pool size is a matching request""" # numpy.int64(8) != 8 is numpy.False_, which must be falsy rather than # merely not True for the warning to stay quiet assert _resolve_max_workers_mpi(numpy.int64(8), 8) == 8 @pytest.mark.free_threaded def test_thread_imap(): """ThreadBackend.imap returns ordered results""" backend = ThreadBackend() data = list(range(10)) result = list(backend.imap(_double, data, max_workers=2)) assert result == [x * 2 for x in data] @pytest.mark.free_threaded def test_thread_as_completed(): """ThreadBackend.as_completed returns all results""" backend = ThreadBackend() data = list(range(10)) result = sorted(backend.as_completed(_double, data)) assert result == sorted(x * 2 for x in data) @pytest.mark.free_threaded def test_thread_accepts_closure(): """a closure is callable on threads, where pickling would refuse it""" factor = 3 def scale(x): return x * factor backend = ThreadBackend() assert list(backend.imap(scale, [1, 2, 3], max_workers=2)) == [3, 6, 9] @pytest.mark.free_threaded def test_thread_max_workers_too_large(): """max_workers above cpu_count raises, as it does for processes""" backend = ThreadBackend() n = multiprocessing.cpu_count() + 1 with pytest.raises(ValueError, match="must be less than or equal to"): list(backend.imap(_double, [1], max_workers=n)) @pytest.mark.free_threaded def test_thread_as_completed_max_workers_clamped(): """large max_workers makes no more workers than there are cpus The pool starts a thread per submitted task until it reaches its limit, so the number of distinct ranks over more tasks than cpus reports what the limit actually was. """ backend = ThreadBackend() cpu = multiprocessing.cpu_count() barrier = threading.Barrier(cpu, timeout=30) def blocked(_): barrier.wait() return parallel.get_rank() ranks = set(backend.as_completed(blocked, range(cpu * 3), max_workers=9999)) assert len(ranks) == cpu @pytest.mark.free_threaded @pytest.mark.skipif( multiprocessing.cpu_count() < 2, reason="requires at least 2 CPU cores" ) def test_thread_max_workers_limits_the_pool(): """max_workers reaches the executor rather than only being validated Without it the executor uses its own default, which is larger, so the number of distinct ranks over many tasks would exceed what was asked for. """ backend = ThreadBackend() barrier = threading.Barrier(2, timeout=30) def blocked(_): barrier.wait() return parallel.get_rank() ranks = set(backend.imap(blocked, range(20), max_workers=2)) assert len(ranks) == 2 @pytest.mark.free_threaded def test_thread_non_sized_iterable(): """a generator input works, having no length to chunk by""" backend = ThreadBackend() def gen(): yield from range(5) result = list(backend.imap(_double, gen(), max_workers=2)) assert result == [x * 2 for x in range(5)] @pytest.mark.free_threaded def test_thread_get_size(): """ThreadBackend.get_size returns cpu_count""" backend = ThreadBackend() assert backend.get_size() == multiprocessing.cpu_count() @pytest.mark.free_threaded def test_thread_get_rank_main_thread(): """the main thread is rank 0, as the master is for the process backends""" backend = ThreadBackend() assert backend.get_rank() == 0 @pytest.mark.free_threaded def test_thread_is_master_process_main_thread(): """the main thread reports master""" backend = ThreadBackend() assert backend.is_master_process() @pytest.mark.free_threaded def test_thread_is_master_process_foreign_thread(): """a thread scinexus did not create still reports master Someone driving scinexus from a web request handler or a GUI worker is the master, so anything gated on this must keep happening for them. """ backend = ThreadBackend() got = [] thread = threading.Thread(target=lambda: got.append(backend.is_master_process())) thread.start() thread.join() assert got == [True] @pytest.mark.free_threaded @pytest.mark.skipif( multiprocessing.cpu_count() < 2, reason="requires at least 2 CPU cores" ) def test_thread_workers_have_distinct_ranks(): """concurrent workers report distinct ranks above 0""" backend = ThreadBackend() barrier = threading.Barrier(2, timeout=30) def blocked(_): barrier.wait() return parallel.get_rank() ranks = set(backend.imap(blocked, [1, 2], max_workers=2)) assert len(ranks) == 2 assert min(ranks) > 0 @pytest.mark.free_threaded @pytest.mark.skipif( multiprocessing.cpu_count() < 2, reason="requires at least 2 CPU cores" ) def test_thread_ranks_are_not_reused_by_a_later_pool(): """a second pool's workers hold ranks the first pool's did not Two pools can be alive at once, nested or side by side, so a rank restarting at 1 for each pool would be held by two live threads. The process backends never return to 1 either. """ backend = ThreadBackend() def collect(): barrier = threading.Barrier(2, timeout=30) def blocked(_): barrier.wait() return parallel.get_rank() return set(backend.imap(blocked, [1, 2], max_workers=2)) first = collect() second = collect() assert len(first) == len(second) == 2 assert not (first & second) @pytest.mark.free_threaded @pytest.mark.skipif( multiprocessing.cpu_count() < 4, reason="requires at least 4 CPU cores" ) def test_thread_nested_pool_ranks_differ_from_the_outer_worker(): """a pool started inside a worker does not reuse the outer rank""" backend = ThreadBackend() def inner(_): return parallel.get_rank() def outer(_): return (parallel.get_rank(), set(backend.imap(inner, [1, 2], max_workers=2))) for outer_rank, inner_ranks in backend.imap(outer, [1, 2], max_workers=2): assert outer_rank not in inner_ranks @pytest.mark.free_threaded @pytest.mark.skipif( multiprocessing.cpu_count() < 2, reason="requires at least 2 CPU cores" ) def test_thread_workers_are_not_master(): """a worker thread does not report master, so it creates nothing The default is left at the process backend on purpose. What describes a worker is the pool that started it, not what the caller registered. """ set_parallel_backend("multiprocess") backend = ThreadBackend() barrier = threading.Barrier(2, timeout=30) def blocked(_): barrier.wait() return (parallel.is_master_process(), parallel.get_rank()) got = list(backend.imap(blocked, [1, 2], max_workers=2)) assert [is_master for is_master, _ in got] == [False, False] assert all(rank > 0 for _, rank in got) @pytest.mark.free_threaded def test_set_parallel_backend_threads(): """setting 'threads' returns ThreadBackend""" set_parallel_backend("threads") assert isinstance(get_parallel_backend(), ThreadBackend) @pytest.mark.free_threaded def test_get_parallel_backend_with_backend_threads(): """returns a ThreadBackend when backend='threads' without changing default""" set_parallel_backend("multiprocess") assert isinstance(get_parallel_backend(backend="threads"), ThreadBackend) assert isinstance(get_parallel_backend(), MultiprocessBackend) @pytest.mark.free_threaded def test_get_rank_thread_unknown_thread(): """a thread with no rank assigned to it reports 0""" got = [] thread = threading.Thread(target=lambda: got.append(_get_rank_thread())) thread.start() thread.join() assert got == [0] @pytest.mark.free_threaded def test_gil_enabled_reports_the_probe(monkeypatch): """the probe reports what sys._is_gil_enabled says raising=False because 3.11 and 3.12 have no such attribute to replace. """ monkeypatch.setattr(sys, "_is_gil_enabled", lambda: False, raising=False) assert _gil_enabled() is False monkeypatch.setattr(sys, "_is_gil_enabled", lambda: True, raising=False) assert _gil_enabled() is True @pytest.mark.free_threaded def test_gil_enabled_without_the_probe(monkeypatch): """an interpreter with no probe always has the GIL sys._is_gil_enabled arrived in 3.13, so its absence is the answer. """ monkeypatch.delattr(sys, "_is_gil_enabled", raising=False) assert _gil_enabled() is True @pytest.mark.free_threaded def test_default_is_threads_without_the_gil(monkeypatch): """the default is a thread backend when the GIL is off""" monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) set_parallel_backend(None) assert isinstance(get_parallel_backend(), ThreadBackend) @pytest.mark.free_threaded def test_default_is_processes_with_the_gil(monkeypatch): """the default is a process backend when the GIL is on""" monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) set_parallel_backend(None) assert isinstance(get_parallel_backend(), MultiprocessBackend) @pytest.mark.free_threaded def test_default_is_processes_in_a_worker_process(monkeypatch): """a worker process does not pick threads A spawned worker re-imports this module with no default set. A thread backend there would report the worker as rank 0 and as the master. """ monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) monkeypatch.setattr(multiprocessing, "parent_process", lambda: object()) set_parallel_backend(None) assert isinstance(get_parallel_backend(), MultiprocessBackend) @pytest.mark.free_threaded def test_default_follows_the_gil_being_switched_back_on(monkeypatch): """the probe runs on each call, not once Importing an extension without free-threading support re-enables the GIL, which can happen after the first parallel call. """ monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) set_parallel_backend(None) assert isinstance(get_parallel_backend(), ThreadBackend) monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) assert isinstance(get_parallel_backend(), MultiprocessBackend) @pytest.mark.free_threaded def test_chosen_backend_survives_the_gil_being_switched_on(monkeypatch): """an explicit choice is never replaced by the probe""" monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) set_parallel_backend("loky") assert isinstance(get_parallel_backend(), LokyBackend) monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) assert isinstance(get_parallel_backend(), LokyBackend) @pytest.mark.free_threaded def test_default_matches_this_interpreter(): """the unpatched default follows the GIL state of the running build Every other test here pins the probe, so this is the only one that fails if the choice is wired to the interpreter the wrong way round. """ set_parallel_backend(None) gil_on = getattr(sys, "_is_gil_enabled", None) is None or sys._is_gil_enabled() expected = MultiprocessBackend if gil_on else ThreadBackend assert isinstance(get_parallel_backend(), expected) @pytest.mark.free_threaded def test_refused_backend_leaves_the_choice_to_be_made_again(monkeypatch): """a call that raises does not stop the GIL being looked at again Clearing the automatic flag before the name is validated would leave a thread backend installed and unreplaceable after the GIL came back. """ monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) set_parallel_backend(None) assert isinstance(get_parallel_backend(), ThreadBackend) with pytest.raises(ValueError, match="unknown backend"): set_parallel_backend("thread") # type: ignore[arg-type] monkeypatch.setattr(parallel, "_gil_enabled", lambda: True) assert isinstance(get_parallel_backend(), MultiprocessBackend) @pytest.mark.free_threaded def test_locks_are_replaced_after_a_fork(): """a child gets its own locks rather than the ones the parent held""" before = (parallel._default_backend_lock, parallel._rank_lock) parallel._replace_locks_after_fork() after = (parallel._default_backend_lock, parallel._rank_lock) assert all(new is not old for new, old in zip(after, before, strict=True)) assert not any(lock.locked() for lock in after) @pytest.mark.free_threaded def test_module_map_runs_in_one_process_without_the_gil(monkeypatch): """the automatic default really runs the work on threads Threads share the interpreter, so every worker reports the caller's pid where a process backend would report its own. """ monkeypatch.setattr(parallel, "_gil_enabled", lambda: False) set_parallel_backend(None) assert parallel.map(_getpid, range(4)) == [os.getpid()] * 4 def test_loky_imap(): """LokyBackend.imap returns ordered results""" backend = LokyBackend() data = list(range(10)) result = list(backend.imap(_double, data, max_workers=1)) assert result == [x * 2 for x in data] def test_loky_as_completed(): """LokyBackend.as_completed returns all results""" backend = LokyBackend() data = list(range(10)) result = sorted(backend.as_completed(_double, data)) assert result == sorted(x * 2 for x in data) def test_loky_is_master_process(): """LokyBackend.is_master_process returns True in main""" backend = LokyBackend() assert backend.is_master_process() def test_loky_get_rank(): """LokyBackend.get_rank returns 0 in main process""" backend = LokyBackend() assert backend.get_rank() == 0 def test_loky_get_size(): """LokyBackend.get_size returns cpu_count""" backend = LokyBackend() assert backend.get_size() == multiprocessing.cpu_count() @pytest.mark.slow @pytest.mark.skipif( multiprocessing.cpu_count() < 2, reason="requires at least 2 CPU cores" ) def test_create_processes(): """Processor pool should create multiple distinct processes""" set_parallel_backend("multiprocess") max_worker_count = multiprocessing.cpu_count() - 1 index = list(range(max_worker_count)) result = parallel.map(get_process_value, index, max_workers=None) result_processes = [v[0] for v in result] result_values = [v[1] for v in result] assert sorted(result_values) == index assert len(set(result_processes)) == max_worker_count def test_random_seeding(): """Random seed should be set every function call""" index1 = [2, 3, 4, 5, 6, 7, 8, 9, 10] index2 = [2, 2, 2, 2, 2, 2, 2, 2, 2] result1 = parallel.map(get_ranint, index1, max_workers=1) result2 = parallel.map(get_ranint, index2, max_workers=1) assert result1[0] == result2[0] assert result1 != result2 def _get_rank(_x): return parallel.get_rank() def test_get_rank(): """get_rank() should return 0 on master, > 0 on workers""" assert parallel.get_rank() == 0 index = list(range(1, 5)) ranks = list(parallel.imap(_get_rank, index)) assert all(r > 0 for r in ranks) def test_is_master_process(): """is_master_process() should return True on master, False on workers""" assert parallel.is_master_process() index = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] master_processes = sum( bool(result) for result in parallel.imap(check_is_master_process, index) ) assert master_processes == 0 def test_as_completed(): """as_completed should return all results""" data = list(range(10)) result = sorted(as_completed(_double, data)) assert result == sorted(x * 2 for x in data) def test_get_size(): """get_size returns cpu_count when not using MPI""" assert get_size() == multiprocessing.cpu_count() def test_get_default_chunksize_exact(): """chunksize with no remainder""" assert get_default_chunksize(range(16), 4) == 1 def test_get_default_chunksize_remainder(): """chunksize rounds up when there is a remainder""" assert get_default_chunksize(range(17), 4) == 2 def test_get_default_chunksize_empty(): """an empty input gives a chunksize an executor will accept""" assert get_default_chunksize([], 1) == 1 assert get_default_chunksize([], 6) == 1 @pytest.mark.parametrize("max_workers", [-1, 0]) def test_get_default_chunksize_below_one_refused(max_workers): """a worker count below one is named rather than divided by""" with pytest.raises(ValueError, match=rf"max_workers \({max_workers}\)"): get_default_chunksize(range(16), max_workers) @pytest.mark.parametrize("max_workers", [True, numpy.True_, 2.5, None]) def test_get_default_chunksize_non_integer_refused(max_workers): """the public helper refuses what the resolvers refuse, None included""" with pytest.raises(TypeError, match=r"max_workers must be an int, got"): get_default_chunksize(range(16), max_workers) def test_get_default_chunksize_numpy_integer_accepted(): """a numpy worker count does not make a numpy chunk size""" got = get_default_chunksize(range(16), numpy.int64(2)) assert got == 2 assert type(got) is int def test_resolve_chunksize_empty(): """the empty case reaches the executor as 1 rather than 0""" assert _resolve_chunksize([], 6, None) == 1 def test_resolve_chunksize_keeps_what_the_caller_asked_for(): """a valid explicit chunk size is passed through untouched""" assert _resolve_chunksize([1, 2], 6, 3) == 3 @pytest.mark.parametrize("chunksize", [-1, 0]) def test_chunksize_below_one_refused(chunksize): """a chunk size below one is named in the message we raise""" with pytest.raises(ValueError, match=rf"chunksize \({chunksize}\)"): _resolve_chunksize([1, 2], 6, chunksize) @pytest.mark.parametrize("chunksize", [True, False]) def test_chunksize_bool_refused(chunksize): """a bool is not a chunk size, and None is how to ask for the default""" with pytest.raises(TypeError, match="must be an int or None"): _resolve_chunksize([1, 2], 6, chunksize) @pytest.mark.parametrize("chunksize", [numpy.True_, numpy.False_, 2.5]) def test_chunksize_non_integer_refused(chunksize): """what is not a whole number is refused as a type, not as a range""" with pytest.raises(TypeError, match="must be an int or None"): _resolve_chunksize([1, 2], 6, chunksize) def test_chunksize_numpy_integer_accepted(): """a numpy integer is a chunk size, and reaches the executor as an int""" got = _resolve_chunksize([1, 2], 6, numpy.int64(3)) assert got == 3 assert type(got) is int _LOCAL_BACKENDS = [ "multiprocess", "loky", pytest.param("threads", marks=pytest.mark.free_threaded), ] @pytest.mark.parametrize("backend_name", _LOCAL_BACKENDS) def test_backend_empty_input(backend_name): """an empty input yields nothing rather than raising""" backend = BACKEND_TYPES[backend_name]() assert list(backend.imap(_double, [])) == [] assert list(backend.as_completed(_double, [])) == [] @pytest.mark.parametrize("backend_name", _LOCAL_BACKENDS) def test_module_empty_input(backend_name): """the module functions take an empty input on every local backend""" set_parallel_backend(backend_name) assert list(parallel.imap(_double, [])) == [] assert parallel.map(_double, []) == [] assert list(parallel.as_completed(_double, [])) == [] @pytest.mark.parametrize("backend_name", _LOCAL_BACKENDS) def test_backend_chunksize_below_one(backend_name): """every call that accepts a chunk size refuses a bad one Only imap on the process backends chunks the work, but as_completed and the thread backend take the argument, so they check it too. """ backend = BACKEND_TYPES[backend_name]() with pytest.raises(ValueError, match=r"chunksize \(-1\)"): list(backend.imap(_double, [1], chunksize=-1)) with pytest.raises(ValueError, match=r"chunksize \(-1\)"): list(backend.as_completed(_double, [1], chunksize=-1)) @pytest.mark.parametrize("backend_name", _LOCAL_BACKENDS) def test_backend_max_workers_below_one(backend_name): """our message reaches the caller rather than the executor's own""" backend = BACKEND_TYPES[backend_name]() with pytest.raises(ValueError, match=r"max_workers \(-1\)"): list(backend.imap(_double, [1], max_workers=-1)) with pytest.raises(ValueError, match=r"max_workers \(-1\)"): list(backend.as_completed(_double, [1], max_workers=-1)) def test_picklable_and_callable(): """PicklableAndCallable wraps and delegates calls""" wrapped = PicklableAndCallable(_double) assert wrapped(5) == 10 def test_imap_invalid_if_serial(): """invalid if_serial raises ValueError""" with pytest.raises(ValueError, match="invalid choice"): list(parallel.imap(_double, [1], if_serial="invalid")) def test_imap_max_workers_too_large(): """max_workers > cpu_count raises ValueError""" n = multiprocessing.cpu_count() + 1 with pytest.raises(ValueError, match="max_workers"): list(parallel.imap(_double, [1], max_workers=n)) def test_as_completed_invalid_if_serial(): """invalid if_serial raises ValueError in as_completed""" with pytest.raises(ValueError, match="invalid choice"): list(as_completed(_double, [1], if_serial="invalid")) def test_as_completed_max_workers_clamped(): """large max_workers gets clamped""" data = list(range(4)) result = sorted(as_completed(_double, data, max_workers=9999)) assert result == sorted(x * 2 for x in data) def test_imap_use_mpi_when_unavailable(): """imap(use_mpi=True) raises RuntimeError when MPI unavailable""" with patch.object(parallel, "MPI", None): with pytest.raises(RuntimeError, match="Cannot use MPI"): list(parallel.imap(_double, [1], use_mpi=True)) def test_imap_non_sized_iterable(): """imap with a generator (non-Sized) defaults chunksize to 1""" def gen(): yield from range(4) result = list(parallel.imap(_double, gen(), max_workers=1)) assert sorted(result) == [0, 2, 4, 6] def test_get_rank_worker_process(): """get_rank parses rank from worker process name""" mock_process = type("FakeProcess", (), {"name": "LokyProcess-3"})() with patch("multiprocessing.current_process", return_value=mock_process): backend = MultiprocessBackend() assert backend.get_rank() == 3 def test_dont_use_mpi_env_var(): """DONT_USE_MPI env var disables MPI import""" import importlib with patch.dict("os.environ", {"DONT_USE_MPI": "1"}): importlib.reload(parallel) assert parallel.MPI is None assert parallel.USING_MPI is False # reload to restore original state os.environ.pop("DONT_USE_MPI", None) importlib.reload(parallel) def test_mpi_import_error_fallback(): """MPI is None when mpi4py cannot be imported""" import importlib import scinexus.parallel as par with patch.dict("sys.modules", {"mpi4py": None, "mpi4py.futures": None}): importlib.reload(par) assert par.MPI is None assert par.USING_MPI is False # reload to restore original state importlib.reload(par) def test_module_imap_delegates_to_backend(): """module-level imap delegates to the current backend""" set_parallel_backend("multiprocess") data = list(range(5)) result = list(parallel.imap(_double, data, max_workers=1)) assert result == [x * 2 for x in data] def test_module_as_completed_delegates_to_backend(): """module-level as_completed delegates to the current backend""" set_parallel_backend("multiprocess") data = list(range(5)) result = sorted(parallel.as_completed(_double, data)) assert result == sorted(x * 2 for x in data) def test_module_map_returns_list(): """module-level map returns a list""" data = list(range(5)) result = parallel.map(_double, data, max_workers=1) assert isinstance(result, list) assert result == [x * 2 for x in data] def test_imap_returns_generator(): """module-level imap returns a generator""" result = parallel.imap(_double, [1, 2, 3], max_workers=1) assert isinstance(result, Generator) def test_as_completed_returns_generator(): """module-level as_completed returns a generator""" result = parallel.as_completed(_double, [1, 2, 3]) assert isinstance(result, Generator) cogent3-scinexus-b16bc06/tests/test_parallel_mpi.py000066400000000000000000000227261525761402500225540ustar00rootroot00000000000000import sys from unittest.mock import patch import pytest from scinexus import parallel from scinexus.parallel import ( SIZE, as_completed, get_rank, imap, is_master_process, map, ) pytestmark = pytest.mark.mpi def _get_rank(n): # noqa: ARG001 return get_rank() def _is_master(n): # noqa: ARG001 return is_master_process() @pytest.mark.mpi def test_get_rank_mpi(): """get_rank() should return 0 on master, > 0 on MPI workers""" assert get_rank() == 0 ranks = list(imap(_get_rank, list(range(1, 5)), use_mpi=True)) assert all(r > 0 for r in ranks) @pytest.mark.mpi def test_is_master_process(): """is_master_process() should return False for all child processes""" assert is_master_process() index = list(range(1, 11)) master_processes = sum( bool(result) for result in imap(_is_master, index, use_mpi=True) ) assert master_processes == 0 def _double(x): return x * 2 @pytest.mark.mpi def test_imap_mpi(): """imap with use_mpi should process all items""" data = list(range(10)) result = list(imap(_double, data, use_mpi=True)) assert result == [x * 2 for x in data] @pytest.mark.mpi def test_map_mpi(): """map with use_mpi should return correct results""" data = list(range(10)) result = map(_double, data, use_mpi=True) assert result == [x * 2 for x in data] @pytest.mark.mpi def test_as_completed_mpi(): """as_completed with use_mpi should return all results""" data = list(range(10)) result = sorted(as_completed(_double, data, use_mpi=True)) assert result == sorted(x * 2 for x in data) @pytest.mark.mpi def test_imap_mpi_with_chunksize(): """imap with explicit chunksize under MPI""" data = list(range(20)) result = list(imap(_double, data, use_mpi=True, chunksize=5)) assert result == [x * 2 for x in data] @pytest.mark.mpi def test_imap_mpi_max_workers_warning(): """a worker count the job cannot change says so and still runs mpiexec -n fixed the pool before the program started, so the request cannot be met and the work runs on the pool regardless. """ data = list(range(10)) with pytest.warns(UserWarning, match="this request is not used"): result = list(imap(_double, data, use_mpi=True, max_workers=SIZE + 10)) assert result == [x * 2 for x in data] @pytest.mark.mpi @pytest.mark.parametrize("max_workers", [-1, 0]) def test_mpi_max_workers_below_one_refused(max_workers): """our message reaches the caller rather than the executor's own""" # imap and as_completed are generators, so an unconsumed call never # runs the body and never raises with pytest.raises(ValueError, match=rf"max_workers \({max_workers}\)"): list(imap(_double, [1], use_mpi=True, max_workers=max_workers)) with pytest.raises(ValueError, match=rf"max_workers \({max_workers}\)"): list(as_completed(_double, [1], use_mpi=True, max_workers=max_workers)) @pytest.mark.mpi def test_mpi_max_workers_bool_refused(): """a bool is not a worker count under MPI either""" with pytest.raises(TypeError, match="max_workers must be an int or None"): list(imap(_double, [1], use_mpi=True, max_workers=True)) with pytest.raises(TypeError, match="max_workers must be an int or None"): list(as_completed(_double, [1], use_mpi=True, max_workers=True)) @pytest.mark.mpi def test_imap_mpi_if_serial_warn(): """if_serial='warn' should be accepted without error when SIZE > 1""" data = list(range(10)) result = list(imap(_double, data, use_mpi=True, if_serial="warn")) assert result == [x * 2 for x in data] @pytest.mark.mpi def test_imap_mpi_if_serial_ignore(): """if_serial='ignore' should be accepted without error""" data = list(range(10)) result = list(imap(_double, data, use_mpi=True, if_serial="ignore")) assert result == [x * 2 for x in data] @pytest.mark.mpi def test_imap_mpi_invalid_if_serial(): """invalid if_serial value should raise ValueError""" with pytest.raises(ValueError, match="invalid choice"): list(imap(_double, [1], use_mpi=True, if_serial="invalid")) @pytest.mark.mpi def test_imap_mpi_if_serial_raise_size_1(): """if_serial='raise' with one worker raises RuntimeError""" backend = parallel.MPIBackend() backend._workers = 1 with pytest.raises(RuntimeError, match="Execution in serial"): list(backend.imap(_double, [1], if_serial="raise")) @pytest.mark.mpi def test_imap_mpi_if_serial_warn_size_1(): """if_serial='warn' with one worker warns and the work still runs""" backend = parallel.MPIBackend() backend._workers = 1 with pytest.warns(UserWarning, match="Execution in serial"): assert list(backend.imap(_double, [1], if_serial="warn")) == [2] @pytest.mark.mpi def test_imap_mpi_non_sized_iterable(): """imap with a generator under MPI defaults chunksize to 1""" def gen(): yield from range(4) result = list(imap(_double, gen(), use_mpi=True)) assert sorted(result) == [0, 2, 4, 6] @pytest.mark.mpi def test_as_completed_mpi_invalid_if_serial(): """invalid if_serial raises ValueError via as_completed with MPI""" with pytest.raises(ValueError, match="invalid choice"): list(as_completed(_double, [1], use_mpi=True, if_serial="invalid")) @pytest.mark.mpi def test_as_completed_mpi_max_workers_warning(): """max_workers > SIZE emits warning in _as_completed_mpi""" data = list(range(10)) with pytest.warns(UserWarning, match="this request is not used"): result = sorted( as_completed(_double, data, use_mpi=True, max_workers=SIZE + 10) ) assert result == sorted(x * 2 for x in data) @pytest.mark.mpi def test_as_completed_mpi_if_serial_raise_size_1(): """_as_completed_mpi with one worker and if_serial='raise' raises""" backend = parallel.MPIBackend() backend._workers = 1 with pytest.raises(RuntimeError, match="Execution in serial"): list(backend.as_completed(_double, [1], if_serial="raise")) @pytest.mark.mpi def test_as_completed_mpi_if_serial_warn_size_1(): """_as_completed_mpi with one worker and if_serial='warn' warns""" backend = parallel.MPIBackend() backend._workers = 1 with pytest.warns(UserWarning, match="Execution in serial"): got = sorted(backend.as_completed(_double, list(range(4)), if_serial="warn")) assert got == [0, 2, 4, 6] @pytest.mark.mpi def test_as_completed_mpi_if_serial_ignore_size_1(): """_as_completed_mpi with one worker and if_serial='ignore' runs on""" backend = parallel.MPIBackend() backend._workers = 1 got = sorted(backend.as_completed(_double, list(range(4)), if_serial="ignore")) assert got == [0, 2, 4, 6] def test_as_completed_mpi_not_using_mpi(): """MPIBackend raises RuntimeError when MPI is None""" with patch.object(parallel, "MPI", None): with pytest.raises(RuntimeError, match="Cannot use MPI"): list(as_completed(_double, [1], use_mpi=True)) @pytest.mark.mpi def test_as_completed_mpi_works_out_no_chunk_size(monkeypatch): """as_completed submits one item at a time, so there is nothing to chunk""" def fail(*args, **kwargs): msg = "as_completed has no use for a chunk size" raise AssertionError(msg) monkeypatch.setattr(parallel, "get_default_chunksize", fail) assert sorted(as_completed(_double, list(range(4)), use_mpi=True)) == [0, 2, 4, 6] @pytest.mark.mpi @pytest.mark.parametrize("chunksize", [None, 5]) def test_as_completed_mpi_sends_no_chunk_size(monkeypatch, chunksize): """nothing reaches the executor under a name it has no parameter for""" # MPIPoolExecutor keeps unrecognised keywords in _options rather than # refusing them, so passing one is invisible without looking there seen = [] real_executor = parallel.MPIfutures.MPIPoolExecutor class Spy(real_executor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) seen.append(self._options) monkeypatch.setattr(parallel.MPIfutures, "MPIPoolExecutor", Spy) got = sorted( as_completed(_double, list(range(4)), use_mpi=True, chunksize=chunksize) ) assert got == [0, 2, 4, 6] assert seen assert all("chunksize" not in options for options in seen) @pytest.mark.mpi def test_as_completed_mpi_chunksize_still_checked(): """a bad chunk size is refused even though a good one is ignored""" with pytest.raises(ValueError, match=r"chunksize \(-1\)"): list(as_completed(_double, [1], use_mpi=True, chunksize=-1)) @pytest.mark.mpi def test_as_completed_mpi_non_sized_iterable(): """_as_completed_mpi takes a generator, having no length to ask for""" def gen(): yield from range(4) result = sorted(as_completed(_double, gen(), use_mpi=True)) assert result == [0, 2, 4, 6] @pytest.mark.mpi def test_mpi_get_size(): """MPIBackend.get_size returns the module-level SIZE""" backend = parallel.MPIBackend() assert backend.get_size() == SIZE @pytest.mark.mpi def test_mpi_get_size_is_the_pool_the_job_has(): """get_size agrees with mpi4py's num_workers, the ground truth here""" backend = parallel.MPIBackend() with backend._futures.MPIPoolExecutor() as executor: assert backend.get_size() == executor.num_workers @pytest.mark.mpi def test_is_master_process_mpi_server(): """is_master_process returns False when argv[0] is server.py""" backend = parallel.MPIBackend() with patch.object(sys, "argv", ["server.py"]): assert backend.is_master_process() is False cogent3-scinexus-b16bc06/tests/test_progress.py000066400000000000000000000647751525761402500217710ustar00rootroot00000000000000from unittest.mock import MagicMock, patch import pytest from scinexus.progress import ( NoProgress, Progress, ProgressContext, RichProgress, TqdmProgress, get_progress, set_progress_backend, ) @pytest.fixture(autouse=True) def _reset_default(): """Reset the module-level default after each test.""" yield set_progress_backend(None) def test_progress_abc_cannot_instantiate(): with pytest.raises(TypeError): Progress() def test_progress_abc_missing_call(): class Incomplete(Progress): def child(self, *, leave=None): return self with pytest.raises(TypeError): Incomplete() def test_progress_abc_missing_child(): class Incomplete(Progress): def __call__(self, iterable, *, total=None, msg=""): yield from iterable with pytest.raises(TypeError): Incomplete() def test_no_progress_yields_all_items_from_list(): np = NoProgress() assert list(np([1, 2, 3])) == [1, 2, 3] def test_no_progress_yields_all_items_from_generator(): np = NoProgress() def gen(): yield "a" yield "b" assert list(np(gen())) == ["a", "b"] def test_no_progress_child_returns_self(): np = NoProgress() assert np.child() is np def test_no_progress_total_and_msg_accepted(): np = NoProgress() assert list(np([1], total=1, msg="test")) == [1] def test_no_progress_empty_iterable(): np = NoProgress() assert list(np([])) == [] def test_no_progress_mixed_types(): np = NoProgress() data = [1, "two", 3.0, None] assert list(np(data)) == data def test_no_progress_is_progress_subclass(): assert isinstance(NoProgress(), Progress) def test_tqdm_yields_all_items(): tp = TqdmProgress(disable=True) assert list(tp([1, 2, 3], total=3)) == [1, 2, 3] def test_tqdm_yields_from_generator(): tp = TqdmProgress(disable=True) def gen(): yield "x" yield "y" assert list(tp(gen(), total=2)) == ["x", "y"] def test_tqdm_default_position_is_zero(): tp = TqdmProgress() assert tp._position == 0 def test_tqdm_child_increments_position(): tp = TqdmProgress() child = tp.child() assert isinstance(child, TqdmProgress) assert child._position == 1 def test_tqdm_chained_child_positions(): tp = TqdmProgress() grandchild = tp.child().child() assert grandchild._position == 2 def test_tqdm_empty_iterable(): tp = TqdmProgress(disable=True) assert list(tp([], total=0)) == [] def test_tqdm_is_progress_subclass(): assert isinstance(TqdmProgress(), Progress) def test_tqdm_total_passed_to_tqdm(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress() list(tp([1, 2], total=42, msg="testing")) mock_tqdm.assert_called_once() call_kwargs = mock_tqdm.call_args assert call_kwargs.kwargs["total"] == 42 assert call_kwargs.kwargs["desc"] == "testing" def test_tqdm_leave_true_at_position_zero(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress() list(tp([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is True def test_tqdm_leave_false_at_position_nonzero(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress() child = tp.child() list(child([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is False def test_tqdm_custom_refresh_per_second(): tp = TqdmProgress(refresh_per_second=5.0) assert tp._refresh_per_second == 5.0 @pytest.mark.parametrize("cls", [TqdmProgress, RichProgress]) @pytest.mark.parametrize("value", [0, -1.0]) def test_refresh_per_second_non_positive_raises(cls, value): with pytest.raises(ValueError, match="refresh_per_second must be positive"): cls(refresh_per_second=value) def test_tqdm_custom_bar_format(): tp = TqdmProgress(bar_format="{l_bar}{bar}") assert tp._bar_format == "{l_bar}{bar}" def test_tqdm_extra_kwargs_stored(): tp = TqdmProgress(unit="B") assert tp._tqdm_kwargs == {"unit": "B"} def test_tqdm_child_inherits_options(): tp = TqdmProgress( refresh_per_second=5.0, bar_format="{l_bar}", ) child = tp.child() assert child._refresh_per_second == 5.0 assert child._bar_format == "{l_bar}" def test_tqdm_child_inherits_tqdm_kwargs(): tp = TqdmProgress(unit="B") child = tp.child() assert child._tqdm_kwargs == {"unit": "B"} def test_tqdm_options_passed_to_tqdm(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress( refresh_per_second=5.0, bar_format="{l_bar}", bar_width=None, ) list(tp([], total=0)) kw = mock_tqdm.call_args.kwargs assert kw["mininterval"] == 0.2 assert kw["bar_format"] == "{l_bar}" def test_rich_yields_all_items(): rp = RichProgress(disable=True) assert list(rp([1, 2, 3], total=3, msg="test")) == [1, 2, 3] def test_rich_child_shares_progress_context(): rp = RichProgress(disable=True) list(rp([1], total=1)) child = rp.child() assert child._progress is rp._progress def test_rich_custom_refresh_per_second(): rp = RichProgress(refresh_per_second=5.0) assert rp._refresh_per_second == 5.0 def test_rich_child_inherits_refresh_per_second(): rp = RichProgress(refresh_per_second=5.0) child = rp.child() assert child._refresh_per_second == 5.0 def test_rich_empty_iterable(): rp = RichProgress(disable=True) assert list(rp([], total=0)) == [] def test_rich_is_progress_subclass(): assert isinstance(RichProgress(), Progress) def test_get_progress_false_returns_no_progress(): assert isinstance(get_progress(show_progress=False), NoProgress) def test_get_progress_true_returns_tqdm_progress(): assert isinstance(get_progress(show_progress=True), TqdmProgress) def test_get_progress_falsy_int_returns_no_progress(): assert isinstance(get_progress(0), NoProgress) def test_get_progress_passthrough_no_progress(): np = NoProgress() assert get_progress(np) is np def test_get_progress_passthrough_tqdm_progress(): tp = TqdmProgress() assert get_progress(tp) is tp def test_get_progress_default_arg_returns_no_progress(): assert isinstance(get_progress(), NoProgress) def test_set_default_no_progress_instance(): np = NoProgress() set_progress_backend(np) assert isinstance(get_progress(show_progress=True), NoProgress) def test_set_default_reset_with_none(): set_progress_backend(NoProgress()) set_progress_backend(None) assert isinstance(get_progress(show_progress=True), TqdmProgress) def test_set_default_preserves_specific_instance(): tp = TqdmProgress(refresh_per_second=5.0) set_progress_backend(tp) result = get_progress(show_progress=True) assert result is tp def test_set_default_false_unaffected(): set_progress_backend(TqdmProgress()) assert isinstance(get_progress(show_progress=False), NoProgress) def test_set_default_passthrough_unaffected(): np = NoProgress() set_progress_backend(TqdmProgress()) assert get_progress(np) is np def test_set_default_string_tqdm(): set_progress_backend("tqdm") assert isinstance(get_progress(show_progress=True), TqdmProgress) def test_set_default_string_rich(): set_progress_backend("rich") assert isinstance(get_progress(show_progress=True), RichProgress) def test_set_default_invalid_string_raises(): with pytest.raises(ValueError, match="unknown progress type"): set_progress_backend("invalid") def test_set_default_string_tqdm_with_kwargs(): set_progress_backend("tqdm", colour="green") result = get_progress(show_progress=True) assert isinstance(result, TqdmProgress) assert result._colour == "green" def test_set_default_string_rich_with_kwargs(): set_progress_backend("rich", colour="blue", leave=True) result = get_progress(show_progress=True) assert isinstance(result, RichProgress) assert result._colour == "blue" assert result._leave is True def test_tqdm_nested_child_yields_all(): outer = TqdmProgress(disable=True) inner = outer.child() outer_data = list(outer([1, 2], total=2)) inner_data = list(inner([3, 4], total=2)) assert outer_data == [1, 2] assert inner_data == [3, 4] def test_no_progress_nested_chain(): p = NoProgress() c = p.child() assert list(c([1, 2, 3])) == [1, 2, 3] assert c is p def test_get_progress_child_type(): tp = TqdmProgress() result = get_progress(tp).child() assert isinstance(result, TqdmProgress) def test_partial_iteration_cleanup(): tp = TqdmProgress(disable=True) it = tp([1, 2, 3, 4, 5], total=5) assert next(it) == 1 assert next(it) == 2 del it def test_progress_context_abc_cannot_instantiate(): with pytest.raises(TypeError): ProgressContext() def test_progress_context_abc_missing_update(): class Incomplete(ProgressContext): def close(self): pass with pytest.raises(TypeError): Incomplete() def test_progress_context_close_default_is_noop(): class MinimalCtx(ProgressContext): def update(self, *, progress, msg=""): pass ctx = MinimalCtx() ctx.close() def test_no_progress_context_returns_progress_context(): np = NoProgress() ctx = np.context() assert isinstance(ctx, ProgressContext) def test_no_progress_context_update_is_noop(): np = NoProgress() with np.context(msg="test") as ctx: ctx.update(progress=0.5, msg="halfway") def test_no_progress_context_close_is_noop(): np = NoProgress() ctx = np.context() ctx.close() ctx.close() def test_tqdm_context_returns_progress_context(): tp = TqdmProgress(disable=True) ctx = tp.context() assert isinstance(ctx, ProgressContext) ctx.close() def test_tqdm_context_as_context_manager(): tp = TqdmProgress(disable=True) with tp.context(msg="test") as ctx: ctx.update(progress=0.0, msg="start") ctx.update(progress=0.5, msg="halfway") ctx.update(progress=1.0, msg="done") def test_tqdm_context_maps_start_end(): tp = TqdmProgress(disable=True) with tp.context(start=0.0, end=0.9) as ctx: ctx.update(progress=0.5) assert ctx._bar.n == pytest.approx(0.45) def test_tqdm_context_full_range(): tp = TqdmProgress(disable=True) with tp.context(start=0.0, end=1.0) as ctx: ctx.update(progress=1.0) assert ctx._bar.n == pytest.approx(1.0) def test_tqdm_context_subrange(): tp = TqdmProgress(disable=True) with tp.context(start=0.9, end=1.0) as ctx: ctx.update(progress=0.5) assert ctx._bar.n == pytest.approx(0.95) def test_tqdm_context_msg_updates_description(): tp = TqdmProgress(disable=True) with tp.context() as ctx: ctx.update(progress=0.5, msg="custom message") assert "custom message" in ctx._bar.desc def test_tqdm_context_no_msg_does_not_update_description(): tp = TqdmProgress(disable=True) with tp.context(msg="initial") as ctx: ctx.update(progress=0.5, msg="updated") ctx.update(progress=0.7) def test_tqdm_context_options_passed(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(refresh_per_second=5.0) ctx = tp.context(msg="test") kw = mock_tqdm.call_args.kwargs assert kw["total"] == 1.0 assert kw["desc"] == "test" assert kw["mininterval"] == 0.2 ctx.close() def test_rich_context_returns_progress_context(): rp = RichProgress(disable=True) ctx = rp.context() assert isinstance(ctx, ProgressContext) ctx.close() def test_rich_context_as_context_manager(): rp = RichProgress(disable=True) with rp.context(msg="test") as ctx: ctx.update(progress=0.5, msg="halfway") def test_rich_context_maps_start_end(): rp = RichProgress(disable=True) with rp.context(start=0.0, end=0.9) as ctx: ctx.update(progress=0.5) def test_rich_context_creates_progress_on_first_call(): rp = RichProgress(disable=True) assert rp._progress is None with rp.context() as ctx: ctx.update(progress=0.5) assert rp._progress is not None def test_tqdm_context_multiphase(): tp = TqdmProgress(disable=True) with tp.context() as ctx: for i in range(10): ctx.update(progress=i / 10 * 0.9, msg="Global") for i in range(10): ctx.update(progress=0.9 + i / 10 * 0.1, msg="Local") ctx.update(progress=1.0, msg="Done") assert ctx._bar.n == pytest.approx(1.0) def test_tqdm_leave_none_uses_position_logic(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(leave=None) list(tp([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is True child = tp.child(leave=None) list(child([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is False def test_tqdm_leave_true_overrides_position(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(leave=True) child = tp.child() list(child([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is True def test_tqdm_leave_false_overrides_position(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(leave=False) list(tp([], total=0)) assert mock_tqdm.call_args.kwargs["leave"] is False def test_tqdm_leave_propagated_to_child(): tp = TqdmProgress(leave=True) child = tp.child() assert child._leave is True def test_tqdm_child_leave_override(): tp = TqdmProgress(leave=True) child = tp.child(leave=False) assert child._leave is False def test_tqdm_child_leave_none_inherits(): tp = TqdmProgress(leave=True) child = tp.child(leave=None) assert child._leave is True def test_tqdm_context_respects_leave(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(leave=True) child = tp.child() ctx = child.context() assert mock_tqdm.call_args.kwargs["leave"] is True ctx.close() def test_tqdm_colour_none_by_default(): tp = TqdmProgress() assert tp._colour is None def test_tqdm_colour_passed_to_tqdm(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(colour="green") list(tp([], total=0)) assert mock_tqdm.call_args.kwargs["colour"] == "green" def test_tqdm_colour_propagated_to_child(): tp = TqdmProgress(colour="green") child = tp.child() assert child._colour == "green" def test_tqdm_context_colour_passed(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(colour="blue") ctx = tp.context() assert mock_tqdm.call_args.kwargs["colour"] == "blue" ctx.close() def test_rich_leave_false_by_default(): rp = RichProgress() assert rp._leave is False def test_rich_leave_false_removes_task_after_iteration(): rp = RichProgress(disable=True, leave=False) result = list(rp([1, 2], total=2)) assert result == [1, 2] assert len(rp._progress.tasks) == 0 def test_rich_leave_true_keeps_task(): rp = RichProgress(disable=True, leave=True) result = list(rp([1, 2], total=2)) assert result == [1, 2] assert len(rp._progress.tasks) == 1 assert rp._progress.tasks[0].completed == 2 def test_rich_leave_propagated_to_child(): rp = RichProgress(leave=True) child = rp.child() assert child._leave is True def test_rich_child_leave_override(): rp = RichProgress(leave=True) child = rp.child(leave=False) assert child._leave is False def test_rich_child_leave_none_inherits(): rp = RichProgress(leave=True) child = rp.child(leave=None) assert child._leave is True def test_rich_context_leave_false_removes_task(): rp = RichProgress(disable=True, leave=False) with rp.context(msg="test") as ctx: ctx.update(progress=0.5) assert len(rp._progress.tasks) == 0 def test_rich_context_leave_true_keeps_task(): rp = RichProgress(disable=True, leave=True) with rp.context(msg="test") as ctx: ctx.update(progress=0.5) assert len(rp._progress.tasks) == 1 assert rp._progress.tasks[0].completed == 1.0 def test_rich_colour_none_by_default(): rp = RichProgress() assert rp._colour is None def test_rich_colour_creates_styled_bar_column(): from rich.progress import BarColumn # type: ignore[import-not-found] rp = RichProgress(disable=True, colour="blue") rp._ensure_progress() bar_columns = [c for c in rp._progress.columns if isinstance(c, BarColumn)] assert len(bar_columns) == 1 assert bar_columns[0].complete_style == "blue" assert bar_columns[0].finished_style == "blue" def test_rich_colour_not_applied_when_progress_provided(): from rich.progress import ( # type: ignore[import-not-found] BarColumn, ) from rich.progress import ( Progress as RProgress, ) custom = RProgress(disable=True) rp = RichProgress(progress=custom, colour="red") result = rp._ensure_progress() assert result is custom bar_columns = [c for c in result.columns if isinstance(c, BarColumn)] for col in bar_columns: assert col.complete_style != "red" def test_rich_colour_propagated_to_child(): rp = RichProgress(colour="cyan") child = rp.child() assert child._colour == "cyan" def test_rich_default_columns_include_elapsed_and_remaining(): from rich.progress import ( # type: ignore[import-not-found] TimeElapsedColumn, TimeRemainingColumn, ) rp = RichProgress(disable=True) rp._ensure_progress() column_types = [type(c) for c in rp._progress.columns] assert TimeElapsedColumn in column_types assert TimeRemainingColumn in column_types def test_no_progress_child_accepts_leave(): np = NoProgress() assert np.child(leave=True) is np assert np.child(leave=False) is np assert np.child(leave=None) is np def test_tqdm_bar_width_default(): tp = TqdmProgress() assert tp._bar_width is None def test_tqdm_bar_width_passed_as_ncols(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(bar_width=80) list(tp([], total=0)) kw = mock_tqdm.call_args.kwargs assert kw["ncols"] == 80 assert kw["dynamic_ncols"] is False def test_tqdm_bar_width_none_uses_dynamic_ncols(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress(bar_width=None, dynamic_ncols=True) list(tp([], total=0)) kw = mock_tqdm.call_args.kwargs assert "ncols" not in kw assert kw["dynamic_ncols"] is True def test_tqdm_bar_width_propagated_to_child(): tp = TqdmProgress(bar_width=80) child = tp.child() assert child._bar_width == 80 def test_rich_bar_width_default(): rp = RichProgress() assert rp._bar_width is None def test_rich_bar_width_applied_to_bar_column(): from rich.progress import BarColumn # type: ignore[import-not-found] rp = RichProgress(disable=True, bar_width=80) rp._ensure_progress() bar_columns = [c for c in rp._progress.columns if isinstance(c, BarColumn)] assert len(bar_columns) == 1 assert bar_columns[0].bar_width == 80 def test_rich_bar_width_propagated_to_child(): rp = RichProgress(bar_width=80) child = rp.child() assert child._bar_width == 80 def test_tqdm_reuses_bar_across_calls(): tp = TqdmProgress(disable=True) assert list(tp([1, 2], total=2)) == [1, 2] assert list(tp([3, 4, 5], total=3)) == [3, 4, 5] def test_tqdm_reuses_bar_single_creation(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress() list(tp([1], total=1)) list(tp([2], total=1)) mock_tqdm.assert_called_once() def test_tqdm_reset_updates_total_and_msg(): tp = TqdmProgress(disable=True) list(tp([], total=5, msg="first")) list(tp([], total=10, msg="second")) assert tp._bar.total == 10 assert tp._bar.n == 0 def test_tqdm_del_closes_bar(): with patch("tqdm.auto.tqdm") as mock_tqdm: mock_bar = MagicMock() mock_tqdm.return_value = mock_bar tp = TqdmProgress() list(tp([], total=0)) del tp mock_bar.close.assert_called_once() def test_tqdm_del_without_use_is_safe(): tp = TqdmProgress() del tp def test_tqdm_print_after_single_bar_appears_after_bar(): import io buf = io.StringIO() tp = TqdmProgress(leave=True, file=buf) list(tp([1, 2, 3], total=3, msg="step")) tp.close() print("DONE", file=buf) lines = buf.getvalue().splitlines() done_idx = next(i for i, ln in enumerate(lines) if "DONE" in ln) bar_idx = next(i for i, ln in enumerate(lines) if "step" in ln) assert done_idx > bar_idx def test_tqdm_print_after_parent_and_child_bars_appears_after_bars(): import io buf = io.StringIO() tp = TqdmProgress(leave=True, file=buf) child = tp.child(leave=True) for _ in tp([1, 2], total=2, msg="outer"): list(child([10, 20, 30], total=3, msg="inner")) tp.close() print("DONE", file=buf) lines = buf.getvalue().splitlines() done_idx = next(i for i, ln in enumerate(lines) if "DONE" in ln) outer_idx = next(i for i, ln in enumerate(lines) if "outer" in ln) inner_idx = next(i for i, ln in enumerate(lines) if "inner" in ln) assert done_idx > outer_idx assert done_idx > inner_idx def test_tqdm_close_as_context_manager(): import io buf = io.StringIO() with TqdmProgress(leave=True, file=buf) as tp: list(tp([1, 2, 3], total=3, msg="step")) print("DONE", file=buf) lines = buf.getvalue().splitlines() done_idx = next(i for i, ln in enumerate(lines) if "DONE" in ln) bar_idx = next(i for i, ln in enumerate(lines) if "step" in ln) assert done_idx > bar_idx def test_tqdm_close_idempotent(): import io buf = io.StringIO() tp = TqdmProgress(leave=True, file=buf) list(tp([1, 2], total=2, msg="step")) tp.close() tp.close() def test_rich_close_stops_display(): rp = RichProgress(disable=True, leave=True) list(rp([1, 2], total=2)) assert rp._progress is not None rp.close() assert rp._task is None def test_rich_close_as_context_manager(): with RichProgress(disable=True, leave=True) as rp: list(rp([1, 2, 3], total=3)) assert rp._task is None def test_rich_close_idempotent(): rp = RichProgress(disable=True, leave=True) list(rp([1, 2], total=2)) rp.close() rp.close() def test_rich_reuses_task_across_calls(): rp = RichProgress(disable=True, leave=True) assert list(rp([1, 2], total=2)) == [1, 2] assert list(rp([3, 4, 5], total=3)) == [3, 4, 5] assert len(rp._progress.tasks) == 1 def test_rich_leave_false_removes_task_across_calls(): rp = RichProgress(disable=True, leave=False) assert list(rp([1, 2], total=2)) == [1, 2] assert len(rp._progress.tasks) == 0 assert list(rp([3, 4, 5], total=3)) == [3, 4, 5] assert len(rp._progress.tasks) == 0 def test_rich_reset_updates_total(): rp = RichProgress(disable=True, leave=True) list(rp([1], total=1)) list(rp([1, 2, 3], total=3)) task = rp._progress.tasks[0] assert task.total == 3 def test_rich_del_completes_task_when_leave_true(): rp = RichProgress(disable=True, leave=True) list(rp([1, 2], total=2)) progress = rp._progress del rp assert len(progress.tasks) == 1 assert progress.tasks[0].completed == 2 def test_rich_cleanup_removes_task_on_interrupted_iteration(): rp = RichProgress(disable=True, leave=False) it = iter(rp([1, 2, 3], total=3)) next(it) progress = rp._progress assert rp._task is not None rp._cleanup_task() assert rp._task is None assert len(progress.tasks) == 0 def test_rich_del_without_use_is_safe(): rp = RichProgress() del rp def test_child_reuses_bar_independently(): outer = TqdmProgress(disable=True) child = outer.child() for batch in outer([1, 2], total=2): assert list(child([10, 20], total=2)) == [10, 20] def test_no_progress_multiple_calls(): np = NoProgress() assert list(np([1, 2])) == [1, 2] assert list(np([3, 4])) == [3, 4] def test_get_progress_kwargs_forwarded(): result = get_progress(show_progress=True, colour="green") assert isinstance(result, TqdmProgress) assert result._colour == "green" def test_get_progress_kwargs_with_default_creates_new_instance(): set_progress_backend("tqdm") result = get_progress(show_progress=True, colour="green") assert isinstance(result, TqdmProgress) assert result._colour == "green" def test_get_progress_kwargs_with_rich_default(): set_progress_backend("rich") result = get_progress(show_progress=True, colour="blue") assert isinstance(result, RichProgress) assert result._colour == "blue" def test_get_progress_no_kwargs_returns_default(): set_progress_backend("tqdm") default = get_progress(show_progress=True) assert default is get_progress(show_progress=True) def test_get_progress_kwargs_with_instance_ignored(): tp = TqdmProgress() assert get_progress(tp, colour="green") is tp def test_set_progress_backend_rich_not_installed(): """set_progress_backend("rich") raises ImportError when rich is missing""" with patch.dict("sys.modules", {"rich": None}), pytest.raises(ImportError): set_progress_backend("rich") def test_get_progress_kwargs_false_ignored(): result = get_progress(show_progress=False, colour="green") assert isinstance(result, NoProgress) def test_get_progress_multiple_kwargs(): result = get_progress(show_progress=True, colour="green", refresh_per_second=5.0) assert isinstance(result, TqdmProgress) assert result._colour == "green" assert result._refresh_per_second == 5.0 cogent3-scinexus-b16bc06/tests/test_skills.py000066400000000000000000000212351525761402500214060ustar00rootroot00000000000000"""checks on the agent skill this repository publishes""" import importlib.util import json import re from pathlib import Path import pytest import scinexus REPO = Path(__file__).parent.parent MARKETPLACE = REPO / ".claude-plugin" / "marketplace.json" PLUGIN = REPO / ".claude-plugin" / "plugin.json" SKILL_DIR = REPO / "skills" / "scinexus" WORKFLOW = REPO / ".github" / "workflows" / "docs.yml" def _load_publisher(): """import the publish script without putting scripts/ on sys.path""" path = REPO / "scripts" / "publish_skills.py" spec = importlib.util.spec_from_file_location("publish_skills", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module publish_skills = _load_publisher() # a reference in SKILL.md is a file name in backticks _REFERENCE = re.compile(r"`([\w.-]+\.(?:md|txt))`") # the standard: 64 chars at most, lower case and hyphens, matching the # directory name _NAME = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") def _skill_text() -> str: return (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") def _referenced() -> set[str]: return set(_REFERENCE.findall(_skill_text())) def test_skill_llms_txt_matches_docs(): """the skill ships llms.txt verbatim, so a drifting copy fails here""" assert (SKILL_DIR / "llms.txt").read_bytes() == ( REPO / "docs" / "llms.txt" ).read_bytes() def test_frontmatter_meets_the_standard(): front = publish_skills.read_frontmatter(SKILL_DIR / "SKILL.md") assert front["name"] == SKILL_DIR.name assert _NAME.match(front["name"]) assert len(front["name"]) <= 64 description = front["description"] assert 0 < len(description) <= 1024 # the trigger vocabulary an agent matches a task against assert "scinexus" in description assert "define_app" in description @pytest.mark.parametrize( "text", [ "---\nname: x\ndescription: >\n folded onto\n two lines\n---\n", "---\nname: x\ndescription: |\n literal onto\n two lines\n---\n", '---\nname: x\ndescription: "quoted"\n---\n', ], ) def test_frontmatter_reads_every_form_the_standard_uses(tmp_dir, text): """the standard's own example writes the description as a folded block""" path = tmp_dir / "SKILL.md" path.write_text(text) front = publish_skills.read_frontmatter(path) assert front["name"] == "x" assert front["description"] assert ">" not in front["description"] assert "|" not in front["description"] @pytest.mark.parametrize( "text", ["# no frontmatter\n", "---\nname: x\n\nnever closed\n"] ) def test_frontmatter_refuses_a_file_it_cannot_read(tmp_dir, text): path = tmp_dir / "SKILL.md" path.write_text(text, encoding="utf-8") # escaped: match takes a pattern, and a Windows path is backslashes, so # C:\Users reaches re.compile as the incomplete escape \U with pytest.raises(ValueError, match=re.escape(str(path))): publish_skills.read_frontmatter(path) def test_every_file_the_skill_points_at_is_shipped(): """a pointer added to SKILL.md without the file is a dead end""" missing = {name for name in _referenced() if not (SKILL_DIR / name).exists()} assert not missing def test_every_shipped_file_is_pointed_at(): """a file nothing points at is never read, so it is dead weight""" shipped = {path.name for path in SKILL_DIR.iterdir() if path.name != "SKILL.md"} assert shipped <= _referenced() def test_publish_serves_every_source_file(tmp_dir): """compared against the source, not against the manifest it just wrote""" publish_skills.publish(tmp_dir) published = tmp_dir / publish_skills.WELL_KNOWN / "scinexus" expected = {path.name for path in SKILL_DIR.iterdir() if path.is_file()} assert {path.name for path in published.iterdir()} == expected for name in expected: assert (published / name).read_bytes() == (SKILL_DIR / name).read_bytes() def test_publish_writes_the_standard_endpoints(tmp_dir): """a standard-following installer asks for these three paths""" entries = publish_skills.publish(tmp_dir) skill = (SKILL_DIR / "SKILL.md").read_bytes() assert ( tmp_dir / publish_skills.WELL_KNOWN / "scinexus" / "SKILL.md" ).read_bytes() == skill assert (tmp_dir / publish_skills.LEGACY / "SKILL.md").read_bytes() == skill assert (tmp_dir / "skill.md").read_bytes() == skill manifest = tmp_dir / publish_skills.WELL_KNOWN / "index.json" (entry,) = json.loads(manifest.read_text(encoding="utf-8"))["skills"] assert entry == entries[0] # relative names, so they resolve wherever the site is mounted assert not any(name.startswith("/") for name in entry["files"]) def test_publish_serves_a_companion_in_a_subdirectory(tmp_dir): """the reference implementation ships references/ and scripts/""" source = tmp_dir / "skills" / "demo" (source / "references").mkdir(parents=True) (source / "SKILL.md").write_text("---\nname: demo\ndescription: d\n---\n") (source / "references" / "deep.md").write_text("deep\n") site = tmp_dir / "site" site.mkdir() publish_skills.SOURCE = tmp_dir / "skills" try: (entry,) = publish_skills.publish(site) finally: publish_skills.SOURCE = REPO / "skills" assert "references/deep.md" in entry["files"] assert ( site / publish_skills.WELL_KNOWN / "demo" / "references" / "deep.md" ).exists() def test_publish_stops_serving_a_file_removed_from_the_skill(tmp_dir): """a stale copy would be served forever, and absent from the manifest""" published = tmp_dir / publish_skills.WELL_KNOWN / "scinexus" published.mkdir(parents=True) (published / "gone.md").write_text("removed from the skill\n") publish_skills.publish(tmp_dir) assert not (published / "gone.md").exists() def test_publish_excludes_test_scaffolding(tmp_dir): """conftest.py runs the examples, it is not part of the skill""" source = tmp_dir / "skills" / "demo" source.mkdir(parents=True) (source / "SKILL.md").write_text("---\nname: demo\ndescription: d\n---\n") (source / "conftest.py").write_text("# scaffolding\n") site = tmp_dir / "site" site.mkdir() publish_skills.SOURCE = tmp_dir / "skills" try: (entry,) = publish_skills.publish(site) finally: publish_skills.SOURCE = REPO / "skills" assert entry["files"] == ["SKILL.md"] assert not (site / publish_skills.WELL_KNOWN / "demo" / "conftest.py").exists() def test_publish_refuses_an_unbuilt_site(tmp_dir): with pytest.raises(SystemExit): publish_skills.publish(tmp_dir / "never-built") def test_publish_rejects_a_name_that_is_not_its_directory(tmp_dir): """the standard requires the two to match, and installers rely on it""" source = tmp_dir / "skills" / "scinexus" source.mkdir(parents=True) (source / "SKILL.md").write_text("---\nname: something-else\ndescription: d\n---\n") site = tmp_dir / "site" site.mkdir() publish_skills.SOURCE = tmp_dir / "skills" try: with pytest.raises(ValueError, match="they must match"): publish_skills.publish(site) finally: publish_skills.SOURCE = REPO / "skills" def test_main_publishes_and_reports(tmp_dir, capsys): assert publish_skills.main([str(tmp_dir)]) == 0 assert "scinexus" in capsys.readouterr().out def test_the_docs_workflow_publishes_and_keeps_hidden_files(): """both are silent failures: no skill on the site, or a 404 for every URL""" workflow = WORKFLOW.read_text(encoding="utf-8") assert "scripts/publish_skills.py site" in workflow # upload-artifact drops hidden files by default, and .well-known is hidden assert "include-hidden-files: true" in workflow # a change to the skill has to rebuild the site that serves it assert "'skills/**'" in workflow def test_marketplace_resolves_to_the_plugin_holding_the_skill(): data = json.loads(MARKETPLACE.read_text(encoding="utf-8")) assert data["name"] == "scinexus" assert data["owner"]["name"] (entry,) = data["plugins"] assert entry["name"] == "scinexus" # the plugin root is the repository root, so one copy of the skill serves # the plugin and the published site root = (REPO / entry["source"]).resolve() assert root == REPO.resolve() plugin = json.loads(PLUGIN.read_text(encoding="utf-8")) assert plugin["name"] == entry["name"] for declared in plugin["skills"]: assert (root / declared).is_dir() assert SKILL_DIR.parent == root / "skills" def test_plugin_version_tracks_the_package(): """without it an install lands under an 'unknown' version directory""" assert ( json.loads(PLUGIN.read_text(encoding="utf-8"))["version"] == scinexus.__version__ ) cogent3-scinexus-b16bc06/tests/test_sqlite_data_store.py000066400000000000000000001737261525761402500236300ustar00rootroot00000000000000import contextlib import gc import os import sqlite3 import sys import threading from pathlib import Path, PureWindowsPath from pickle import dumps, loads import pytest from citeable import Software from scitrack import get_text_hexdigest from scinexus import sqlite_data_store from scinexus.composable import ( LOADER, NotCompleted, NotCompletedType, define_app, source_proxy, ) from scinexus.data_store import ( APPEND, OVERWRITE, READONLY, DataMemberABC, DataStoreDirectory, ) from scinexus.parallel import set_parallel_backend from scinexus.sqlite_data_store import ( _MEMORY, LOG_TABLE, RESULT_TABLE, DataStoreSqlite, _owner_token, has_valid_schema, open_sqlite_db_ro, open_sqlite_db_rw, ) @pytest.fixture def tmp_dir(tmp_path_factory): return Path(tmp_path_factory.mktemp("sqlitedb")) @pytest.fixture def sample_citations(): cite1 = Software( author=["Doe, J"], title="Tool One", year=2024, url="https://example.com/one", version="1.0", license="MIT", doi="10.0/one", publisher="test", ) cite2 = Software( author=["Smith, A"], title="Tool Two", year=2024, url="https://example.com/two", version="2.0", license="MIT", doi="10.0/two", publisher="test", ) return (cite1, cite2) @pytest.fixture def nc_objects(): return { f"id_{i}": NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"id_{i}" ) for i in range(3) } @pytest.fixture def sql_dstore(DATA_DIR, tmp_dir): ro_dir_dstore = DataStoreDirectory(DATA_DIR, suffix="fasta") path = tmp_dir / "data.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) for m in ro_dir_dstore: dstore.write(data=m.read(), unique_id=m.unique_id) return dstore @pytest.fixture def full_dstore_sqlite(tmp_dir, nc_objects, DATA_DIR): path = tmp_dir / "full.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) for uid, obj in nc_objects.items(): dstore.write_not_completed(unique_id=uid, data=obj.to_json()) ro = DataStoreDirectory(DATA_DIR, suffix="fasta") for m in ro: dstore.write(unique_id=m.unique_id, data=m.read()) log_text = (DATA_DIR / "scitrack.log").read_text() dstore.write_log(unique_id="scitrack.log", data=log_text) yield dstore dstore.close() @pytest.fixture def writable_store(tmp_dir): path = tmp_dir / "writable.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") yield dstore dstore.close() @pytest.fixture def populated_store(tmp_dir): """A store with data, closed and ready for read-only access.""" path = tmp_dir / "populated.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() return path def test_db_creation(tmp_dir): path = tmp_dir / "test.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert dstore.source == path dstore.close() def test_db_init_log(tmp_dir): path = tmp_dir / "test_log.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="test_record", data="test data") assert dstore._log_id is not None dstore.close() def test_open_sqlite_db_rw(tmp_dir): path = tmp_dir / "test_rw.sqlitedb" db = open_sqlite_db_rw(path) assert has_valid_schema(db) db.close() def test_rw_sql_dstore_mem(): """in memory dstore with multiple writes verified via SQL""" dstore = DataStoreSqlite(_MEMORY, mode=OVERWRITE) records = {f"r{i}": f"data {i}" for i in range(3)} for unique_id, data in records.items(): dstore.write(data=data, unique_id=unique_id) expect = len(records) query = f"SELECT count(*) as c FROM {RESULT_TABLE} WHERE is_completed=?" got = dstore.db.execute(query, (1,)).fetchone()["c"] assert got == expect assert len(dstore.completed) == expect def test_not_completed(tmp_dir): """multiple not_completed records are stored and retrievable""" nc_objects = { f"id_{i}": NotCompleted( NotCompletedType.ERROR, "location", "message", source=f"id_{i}" ) for i in range(3) } path = tmp_dir / "test_nc.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) for unique_id, obj in nc_objects.items(): dstore.write_not_completed(data=obj.to_json(), unique_id=unique_id) expect = len(nc_objects) query = f"SELECT count(*) as c FROM {RESULT_TABLE} WHERE is_completed=?" got = dstore.db.execute(query, (0,)).fetchone()["c"] assert got == expect assert len(dstore.not_completed) == expect dstore.close() def test_logdata(tmp_dir, DATA_DIR): path = tmp_dir / "test_logdata.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) log_text = (DATA_DIR / "scitrack.log").read_text() dstore.write_log(unique_id="test.log", data=log_text) assert len(dstore.logs) == 1 got = dstore.logs[0].read() assert got == log_text dstore.close() def test_log_member_ids_are_spelled_the_same_on_every_platform( tmp_dir, monkeypatch, ): """a sqlite store names its logs the way the other backends do""" # the platform half of this cannot run here, so stand in for it. the # store is built first because PureWindowsPath has no expanduser(), # which __init__ calls. the patch is inert against the code as it # stands, and is here to fail the day an id is joined with Path again path = tmp_dir / "test_log_id.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_log(unique_id="test.log", data="2024-01-01\tsetup\n") monkeypatch.setattr(sqlite_data_store, "Path", PureWindowsPath) assert [m.unique_id for m in dstore.logs] == [f"{LOG_TABLE}/test.log"] dstore.close() def test_drop_not_completed(nc_objects): dstore = DataStoreSqlite(_MEMORY, mode=OVERWRITE) for unique_id, obj in nc_objects.items(): dstore.write_not_completed(data=obj.to_json(), unique_id=unique_id) assert len(dstore.not_completed) == len(nc_objects) dstore.drop_not_completed() assert len(dstore.not_completed) == 0 def test_contains(tmp_dir): path = tmp_dir / "test_contains.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="record1", data="data1") assert "record1" in dstore assert "record2" not in dstore dstore.close() def test_iter(tmp_dir): path = tmp_dir / "test_iter.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.write(unique_id="r2", data="d2") ids = {m.unique_id for m in dstore} assert ids == {"r1", "r2"} dstore.close() def test_members(tmp_dir): path = tmp_dir / "test_members.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") nc = NotCompleted(NotCompletedType.FAIL, "dummy", "msg", source="src") dstore.write_not_completed(unique_id="nc1", data=nc.to_json()) assert len(dstore.members) == 2 assert all(isinstance(m, DataMemberABC) for m in dstore) dstore.close() def test_len(tmp_dir): path = tmp_dir / "test_len.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert len(dstore) == 0 dstore.write(unique_id="r1", data="d1") assert len(dstore) == 1 dstore.close() def test_md5_sum(tmp_dir): path = tmp_dir / "test_md5.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) data = "test data for md5" dstore.write(unique_id="r1", data=data) md5 = dstore.md5("r1") assert md5 == get_text_hexdigest(data) dstore.close() def test_iterall(tmp_dir): path = tmp_dir / "test_iterall.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.write(unique_id="r2", data="d2") all_members = list(dstore) assert len(all_members) == 2 dstore.close() def test_read(tmp_dir): path = tmp_dir / "test_read.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) data = "test data content" dstore.write(unique_id="r1", data=data) got = dstore.read("r1") assert got == data dstore.close() def test_read_all_record_types(full_dstore_sqlite): """reading from completed, not_completed, and log records all return str""" records = [ full_dstore_sqlite.completed[0], full_dstore_sqlite.not_completed[0], full_dstore_sqlite.logs[0], ] assert all(isinstance(r.read(), str) for r in records) def test_write_success_replaces_not_completed(tmp_dir): path = tmp_dir / "test_replace_nc.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) nc = NotCompleted(NotCompletedType.FAIL, "dummy", "msg", source="src") dstore.write_not_completed(unique_id="r1", data=nc.to_json()) assert len(dstore.not_completed) == 1 dstore.write(unique_id="r1", data="completed data") assert len(dstore.not_completed) == 0 assert len(dstore.completed) == 1 dstore.close() def test_read_log(tmp_dir, DATA_DIR): path = tmp_dir / "test_readlog.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) log_text = (DATA_DIR / "scitrack.log").read_text() dstore.write_log(unique_id="test.log", data=log_text) got = dstore.read(str(Path("logs") / "test.log")) assert got == log_text dstore.close() @pytest.mark.parametrize("binary", [False, True]) def test_write_text_binary(binary): """correctly write content whether text or binary data""" dstore = DataStoreSqlite(_MEMORY, mode=OVERWRITE) expect = "some text data" if binary: expect = dumps(expect) m = dstore.write(unique_id="record", data=expect) got = m.read() assert got == expect def test_write_if_member_exists(tmp_dir): path = tmp_dir / "test_exists.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="data1") dstore.write(unique_id="r1", data="data2") got = dstore.read("r1") assert got == "data2" assert len(dstore.completed) == 1 dstore.close() def test_new_write_read(tmp_dir): path = tmp_dir / "test_new_wr.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="content1") dstore.write(unique_id="r2", data="content2") assert dstore.read("r1") == "content1" assert dstore.read("r2") == "content2" dstore.close() def test_read_unknown_table(tmp_dir): path = tmp_dir / "test_unknown.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") with pytest.raises(ValueError): dstore.read("unknown_table/r1") dstore.close() def test_limit_on_writable(tmp_dir): path = tmp_dir / "test_limit.sqlitedb" with pytest.raises(ValueError): DataStoreSqlite(path, mode=OVERWRITE, limit=10) @pytest.mark.parametrize("table_name", ["", RESULT_TABLE]) def test_new_write_id_includes_table(table_name): """correctly handles table name if included in unique id""" dstore = DataStoreSqlite(_MEMORY, mode=OVERWRITE) identifier = "test1.fasta" if table_name: identifier = str(Path(table_name) / identifier) data = "test data" m = dstore.write(unique_id=identifier, data=data) got = dstore.read(m.unique_id) assert got == data @pytest.mark.parametrize( "operation", [ lambda d: d.read("r1"), lambda d: d.completed, lambda d: d.not_completed, lambda d: d.logs, lambda d: len(d), lambda d: d.write(unique_id="r2", data="d2"), ], ) def test_use_after_close_raises(tmp_dir, operation): """a closed store refuses to act rather than half working""" # the path is kept clear of the word the match looks for, since other # errors from this store quote the path and a file called # closed.sqlitedb would satisfy an assertion meant for "is closed" path = tmp_dir / "shut.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() with pytest.raises(OSError, match="is closed"): operation(dstore) def test_closing_twice(tmp_dir): """a second close is not an error""" path = tmp_dir / "twice.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() dstore.close() assert dstore._db is None def test_close_before_the_database_is_opened(tmp_dir): """closing a store that never opened its database still ends it""" # the connection is made on first use, so this is the ordinary shape of # a store that is built and then abandoned path = tmp_dir / "untouched.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.close() with pytest.raises(OSError, match="is closed"): dstore.write(unique_id="r1", data="d1") @pytest.mark.parametrize("operation", [lambda d: d.lock(), lambda d: d.unlock()]) def test_lock_operations_on_a_closed_store(tmp_dir, operation): """taking or releasing the lock of a closed store is refused""" path = tmp_dir / "shutlock.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() with pytest.raises(OSError, match="is closed"): operation(dstore) def test_collection_without_close_keeps_the_lock(tmp_dir): """a store dropped without being closed leaves its lock behind""" # the lock marks a session that did not end through close(), so the # finaliser must not release it. it also issues no SQL, which at # interpreter exit could block on another connection or find the # machinery it needs gone path = tmp_dir / "dropped.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") with pytest.warns(UserWarning, match="was not closed"): del dstore gc.collect() db = sqlite3.connect(path) try: held = db.execute("SELECT lock_pid FROM state").fetchone()[0] finally: db.close() assert held == f"{os.getpid()}:{threading.get_native_id()}" def test_collection_of_a_store_refused_the_lock_stays_quiet(tmp_dir, recwarn): """a store that never got the lock does not claim to be holding it""" # its open was refused, so it has a connection but no lock, and the # advice to close it would not release one taken by anybody else path = tmp_dir / "taken.sqlitedb" first = DataStoreSqlite(path, mode=OVERWRITE) first.write(unique_id="r1", data="d1") first._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) first._db.close() first._db = None first._closed = True second = DataStoreSqlite(path, mode=OVERWRITE) with pytest.raises(OSError, match="locked"): second.read("r1") assert second._db is not None del second gc.collect() assert [w for w in recwarn if issubclass(w.category, UserWarning)] == [] @pytest.mark.parametrize( ("source", "mode"), [ (_MEMORY, OVERWRITE), ("readonly", READONLY), ], ) def test_collection_without_close_stays_quiet(tmp_dir, source, mode, recwarn): """only a store that can strand a lock on disk is worth warning about""" # an in-memory store leaves nothing behind and a read only one never # takes the lock. a writable store on disk holds one from the moment it # is constructed, so there is no longer a case of one that holds nothing if source is not _MEMORY: populate = tmp_dir / f"{source}.sqlitedb" seed = DataStoreSqlite(populate, mode=OVERWRITE) seed.write(unique_id="r1", data="d1") seed.close() dstore = DataStoreSqlite(populate, mode=mode) if source == "readonly": dstore.read("r1") else: dstore = DataStoreSqlite(source, mode=mode) dstore.write(unique_id="r1", data="d1") del dstore gc.collect() assert [w for w in recwarn if issubclass(w.category, UserWarning)] == [] def test_close_hands_the_database_on(tmp_dir): """a closed store leaves the database open to a new one""" # the lock names the process, so without releasing it here the store # that follows cannot take the database in a writable mode path = tmp_dir / "handover.sqlitedb" first = DataStoreSqlite(path, mode=OVERWRITE) first.write(unique_id="r1", data="d1") first.close() second = DataStoreSqlite(path, mode=OVERWRITE) assert second.read("r1") == "d1" assert second._lock_id == f"{os.getpid()}:{threading.get_native_id()}" second.close() @pytest.mark.parametrize("mode", [OVERWRITE, APPEND]) def test_a_held_lock_refuses_a_writable_store(tmp_dir, mode): """a lock left by another session refuses any mode that would write""" # the lock marks a store whose session did not end through close(), so # its records were never confirmed. neither mode may write over that # without the deliberate unlock path = tmp_dir / "held.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=mode) with pytest.raises(OSError, match="locked"): dstore.read("r1") @pytest.mark.parametrize("mode", [OVERWRITE, APPEND]) def test_a_held_lock_refuses_every_time(tmp_dir, mode): """the refusal is not spent by being raised once""" # the connection is opened before the lock is sought, so a refusal # leaves one behind. taken as proof of a lock, it lets the next access # straight through path = tmp_dir / "sticky.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=mode) with pytest.raises(OSError, match="locked"): dstore.read("r1") with pytest.raises(OSError, match="locked"): dstore.read("r1") with pytest.raises(OSError, match="locked"): dstore.write(unique_id="r2", data="d2") @pytest.mark.parametrize("mode", [OVERWRITE, APPEND]) def test_unlock_reaches_a_lock_held_by_another_session(tmp_dir, mode): """the deliberate override works on the store it exists for""" # unlock has to open the database without seeking the lock, or the # refusal it is meant to clear is what stops it running path = tmp_dir / "override.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=mode) dstore.unlock(force=True) # read from the connection unlock opened, before anything else takes # the lock the store is now free to take assert dstore._db.execute("SELECT lock_pid FROM state").fetchone()[0] is None assert dstore.read("r1") == "d1" assert dstore._lock_id == f"{os.getpid()}:{threading.get_native_id()}" dstore.close() def test_a_lock_recorded_as_zero_still_locks(tmp_dir): """an owner recorded as 0 is an owner""" path = tmp_dir / "zero.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=0") abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=APPEND) assert dstore.locked with pytest.raises(OSError, match="locked"): dstore.write(unique_id="r2", data="d2") def test_a_held_lock_allows_a_readonly_store(tmp_dir): """a locked store can still be read, only not written""" path = tmp_dir / "readable.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=READONLY) assert dstore.read("r1") == "d1" dstore.close() def _abandon_with_a_lock_in_a_later_row(path, lock_pid): """leaves the state table as the racy implementation could have Row 1 free, because the session that held it called unlock(), and row 2 still holding the lock of a session that never closed. """ abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned.record_type = str # the racy lock() inserted rather than updated, so a second session got # its own row. seed it the same way to get row 2 for the right reason abandoned._db.execute("INSERT INTO state(lock_pid) VALUES (?)", (lock_pid,)) # that version's unlock() cleared state_id 1 alone, which is how a # database ends up free in its first row and locked below it abandoned._db.execute("UPDATE state SET lock_pid=NULL WHERE state_id=1") abandoned._db.close() abandoned._db = None abandoned._closed = True @pytest.mark.parametrize("mode", [OVERWRITE, APPEND]) def test_a_lock_in_a_later_state_row_refuses_a_writable_store(tmp_dir, mode): """a lock is a lock whichever state row records it""" # the racy lock() left rows past the first, and unlock() cleared only # row 1. read from row 1 alone, a lock below it is invisible, so the # store it marks as never confirmed opens for writing path = tmp_dir / "laterrow.sqlitedb" _abandon_with_a_lock_in_a_later_row(path, f"{os.getpid() + 1}:1") dstore = DataStoreSqlite(path, mode=mode) assert dstore.locked with pytest.raises(OSError, match="locked"): dstore.read("r1") def test_a_lock_in_a_later_state_row_shows_on_a_readonly_store(tmp_dir): """a read-only store reports the lock it cannot take""" # READONLY returns from lock() before any claim, so nothing on that path # can put a later row right: the reader itself has to see it path = tmp_dir / "laterrow_ro.sqlitedb" _abandon_with_a_lock_in_a_later_row(path, f"{os.getpid() + 1}:1") dstore = DataStoreSqlite(path, mode=READONLY) assert dstore.locked assert dstore.read("r1") == "d1" dstore.close() @pytest.mark.parametrize("mode", [OVERWRITE, APPEND]) def test_unlock_reaches_a_lock_in_a_later_state_row(tmp_dir, mode): """the deliberate override clears every lock recorded, not merely row 1""" # clearing one row per call leaves the store refusing after the user has # forced it open, and unlock() reads the lock before clearing it, so a # row it cannot see is a row it never clears from scinexus.misc import get_object_provenance path = tmp_dir / "laterrow_unlock.sqlitedb" _abandon_with_a_lock_in_a_later_row(path, f"{os.getpid() + 1}:1") dstore = DataStoreSqlite(path, mode=mode) dstore.unlock(force=True) assert dstore.read("r1") == "d1" assert dstore._lock_id == _owner_token() # claiming the store collapses the rows the racy version left behind, # keeping row 1 and the record_type it carries rows = dstore._db.execute("SELECT state_id FROM state").fetchall() assert [r["state_id"] for r in rows] == [1] assert dstore.record_type == get_object_provenance(str) dstore.close() def test_the_owner_token_tells_threads_apart(): """two threads of one process do not produce the same owner""" # the main thread's native id equals the pid on Linux, so a token built # from the pid twice is indistinguishable from a real one when it is # only ever read from the main thread tokens = [] def record(): tokens.append(_owner_token()) record() other = threading.Thread(target=record) other.start() other.join() assert tokens[0] != tokens[1] assert all(t.startswith(f"{os.getpid()}:") for t in tokens) def test_the_lock_names_the_thread_as_well_as_the_process(tmp_dir): """ownership identifies one store, not merely one process""" path = tmp_dir / "owner.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") assert dstore._lock_id == f"{os.getpid()}:{threading.get_native_id()}" dstore.close() def test_unlock_declines_a_lock_taken_by_another_thread(tmp_dir): """a lock this thread did not take is not this thread's to release""" # threads share a pid, so an owner recorded as one gave every thread of # a process the run of every lock any of them held path = tmp_dir / "otherthread.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") foreign = f"{os.getpid()}:{threading.get_native_id() + 1}" dstore._db.execute("UPDATE state SET lock_pid=?", (foreign,)) dstore.unlock() assert dstore._lock_id == foreign dstore.unlock(force=True) assert dstore._lock_id is None dstore.close() def test_unlock_declines_a_lock_in_the_older_format(tmp_dir): """a bare pid names a session this one cannot identify itself with""" path = tmp_dir / "legacy.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore._db.execute("UPDATE state SET lock_pid=?", (os.getpid(),)) dstore.unlock() assert dstore._lock_id == os.getpid() dstore.unlock(force=True) assert dstore._lock_id is None dstore.close() def test_a_lock_this_session_holds_points_at_the_open_store(tmp_dir): """the refusal names the store this session left open as the likely cause""" path = tmp_dir / "reopened.sqlitedb" first = DataStoreSqlite(path, mode=APPEND) first.write(unique_id="r1", data="d1") second = DataStoreSqlite(path, mode=APPEND) with pytest.raises(OSError, match="another store in this process") as refused: second.write(unique_id="r2", data="d2") message = str(refused.value) assert "another thread" not in message assert "close()" in message assert "unlock(force=True)" in message first.close() def test_a_lock_another_thread_holds_points_at_that_thread(tmp_dir): """the refusal names the thread of this process that holds the lock""" path = tmp_dir / "otherthread_reopen.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") foreign = f"{os.getpid()}:{threading.get_native_id() + 1}" dstore._db.execute("UPDATE state SET lock_pid=?", (foreign,)) second = DataStoreSqlite(path, mode=APPEND) with pytest.raises(OSError, match="another thread of this process") as refused: second.write(unique_id="r2", data="d2") message = str(refused.value) assert "another store in this process" not in message assert "close()" in message assert "unlock(force=True)" in message dstore.unlock(force=True) dstore.close() def test_a_lock_another_session_holds_does_not_blame_this_one(tmp_dir): """a lock from another process is not reported as this process's""" path = tmp_dir / "foreign.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", (os.getpid() + 1,)) abandoned._db.close() abandoned._db = None abandoned._closed = True dstore = DataStoreSqlite(path, mode=APPEND) with pytest.raises(OSError, match="locked by") as refused: dstore.write(unique_id="r2", data="d2") assert "this process" not in str(refused.value) dstore.unlock(force=True) dstore.close() _LOCK_RACERS = 8 _LOCK_ROUNDS = 5 _LOCK_TIMEOUT = 30 def _race_for_the_lock(path): """let several sessions reach for the lock of a fresh store at once Returns the rows left in the state table. """ open_sqlite_db_rw(path).close() barrier = threading.Barrier(_LOCK_RACERS, timeout=_LOCK_TIMEOUT) def claim(): store = DataStoreSqlite(path, mode=OVERWRITE) barrier.wait(timeout=_LOCK_TIMEOUT) with contextlib.suppress(OSError, sqlite3.OperationalError): _ = store.db # closed in the thread that opened the connection, which is the # only one sqlite3 will let touch it with contextlib.suppress(sqlite3.Error): store.close() threads = [threading.Thread(target=claim) for _ in range(_LOCK_RACERS)] old_interval = sys.getswitchinterval() sys.setswitchinterval(1e-6) try: for thread in threads: thread.start() for thread in threads: thread.join(timeout=_LOCK_TIMEOUT) finally: sys.setswitchinterval(old_interval) raw = sqlite3.connect(path) try: return raw.execute("SELECT state_id FROM state").fetchall() finally: raw.close() @pytest.mark.parametrize("attempt", list(range(_LOCK_ROUNDS))) def test_only_one_session_takes_the_lock(tmp_dir, attempt): """sessions racing for a free store leave one state row between them""" # reading the state table and claiming it are one transaction, so a # racer either finds the store free and takes it or finds it taken. # as two statements each racer found it free and inserted its own row, # and since unlock() clears state_id 1 the rest were held for good. # one round in two showed it, so a red case here reruns green: it is # the parametrised set that carries the test, not any single attempt rows = _race_for_the_lock(tmp_dir / f"race{attempt}.sqlitedb") assert rows == [(1,)] def test_a_failed_claim_leaves_the_connection_usable(tmp_dir, monkeypatch): """a claim that raises ends its transaction rather than stranding it""" path = tmp_dir / "failclaim.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.unlock() def explode(): msg = "no token" raise RuntimeError(msg) monkeypatch.setattr("scinexus.sqlite_data_store._owner_token", explode) with pytest.raises(RuntimeError): dstore.lock() assert not dstore._db.in_transaction monkeypatch.undo() dstore.lock() assert dstore._lock_id == _owner_token() dstore.close() def test_a_claim_refused_at_the_commit_leaves_the_store_usable(tmp_dir): """a commit that cannot complete does not strand the connection""" path = tmp_dir / "busycommit.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.unlock() dstore._db.execute("PRAGMA busy_timeout=100") # a reader inside a read transaction blocks the exclusive lock a commit # needs without blocking the reserved one the claim opens with, so the # claim fails at the last statement of the three reader = sqlite3.connect(path, isolation_level=None, timeout=1) reader.execute("BEGIN") reader.execute("SELECT * FROM state").fetchall() try: with pytest.raises(sqlite3.OperationalError): dstore.lock() assert not dstore._db.in_transaction finally: reader.close() dstore.lock() assert dstore._lock_id == _owner_token() dstore.close() def test_a_locked_store_is_refused_without_waiting_for_the_write_lock(tmp_dir): """the refusal reads the owner, which does not need the write lock""" path = tmp_dir / "refuse.sqlitedb" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.execute("UPDATE state SET lock_pid=?", ("999999:999999",)) abandoned._db.close() abandoned._db = None abandoned._closed = True # another session holding the write lock would make a refusal that # contends for it wait out the busy timeout and fail as OperationalError writer = sqlite3.connect(path, isolation_level=None) writer.execute("BEGIN IMMEDIATE") try: dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore._connection().execute("PRAGMA busy_timeout=30000") with pytest.raises(OSError, match="locked by"): dstore.lock() finally: writer.close() dstore.close() def test_is_locked(tmp_dir): path = tmp_dir / "test_locked.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") assert dstore.locked dstore.close() def test_lock_unlock(tmp_dir): path = tmp_dir / "test_lock_unlock.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") assert dstore.locked dstore.unlock() assert not dstore.locked dstore.lock() assert dstore.locked dstore.unlock() assert not dstore.locked dstore.close() def test_lock_on_a_store_holding_its_lock_does_nothing(tmp_dir): """a store is not refused the lock it already holds""" path = tmp_dir / "relock.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.lock() assert dstore._lock_id == _owner_token() dstore.write(unique_id="r2", data="d2") dstore.close() def test_lock_firsttime(tmp_dir): path = tmp_dir / "test_lockfirst.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) # accessing db triggers lock _ = dstore.db assert dstore.locked # delete state row and re-lock from empty state dstore.db.execute("DELETE FROM state WHERE state_id=1") dstore.lock() assert dstore.locked dstore.unlock() assert not dstore.locked dstore.close() def test_db_without_logs(tmp_dir): path = tmp_dir / "test_nologs.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") assert len(dstore.logs) == 0 dstore.close() def test_md5_none(tmp_dir): path = tmp_dir / "test_md5none.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert dstore.md5("nonexistent") is None dstore.close() def test_md5_missing(tmp_dir): path = tmp_dir / "test_md5missing.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert dstore.md5("missing_record") is None dstore.close() def test_open_data_store_sqlitedb_err(): from scinexus.io import open_data_store with pytest.raises(NotImplementedError): open_data_store(":memory:", mode="r") def test_pickleable_roundtrip(tmp_dir): path = tmp_dir / "test_pickle.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() dstore2 = DataStoreSqlite(path, mode=READONLY) re_dstore = loads(dumps(dstore2)) assert re_dstore.read("r1") == "d1" re_dstore.close() dstore2.close() def test_pickleable_member_roundtrip(tmp_dir): path = tmp_dir / "test_pickle_member.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() dstore2 = DataStoreSqlite(path, mode=READONLY) member = dstore2[0] re_member = loads(dumps(member)) assert re_member.read() == "d1" re_member.data_store.close() dstore2.close() def test_getitem(tmp_dir): path = tmp_dir / "test_getitem.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.write(unique_id="r2", data="d2") first = dstore[0] assert first.unique_id == "r1" with pytest.raises(IndexError): _ = dstore[len(dstore)] dstore.close() def test_empty_data_store(tmp_dir): path = tmp_dir / "test_empty.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert len(dstore) == 0 dstore.close() def test_no_logs(tmp_dir): path = tmp_dir / "test_nologs2.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert len(dstore.logs) == 0 dstore.close() def test_no_not_completed(tmp_dir): path = tmp_dir / "test_nonc.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) assert len(dstore.not_completed) == 0 dstore.close() def test_write_read_only_datastore(tmp_dir): path = tmp_dir / "test_ro.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.close() ro = DataStoreSqlite(path, mode=READONLY) with pytest.raises(IOError): ro.write(unique_id="r2", data="d2") ro.close() def test_write_citations_sqlite(tmp_dir, sample_citations): path = tmp_dir / "test_cite.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_citations(data=sample_citations) loaded = dstore._load_citations() assert len(loaded) == 2 assert loaded[0].title == "Tool One" assert loaded[1].title == "Tool Two" dstore.close() def test_write_citations_empty_sqlite(tmp_dir): path = tmp_dir / "test_cite_empty.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_citations(data=()) loaded = dstore._load_citations() assert len(loaded) == 0 dstore.close() def test_write_bib_sqlite(tmp_dir, sample_citations): path = tmp_dir / "test_bib.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_citations(data=sample_citations) bib_path = tmp_dir / "refs.bib" dstore.write_bib(bib_path) assert bib_path.exists() content = bib_path.read_text() assert "Tool One" in content assert "Tool Two" in content dstore.close() def test_summary_citations_sqlite(tmp_dir, sample_citations): path = tmp_dir / "test_sumcite.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_citations(data=sample_citations) result = dstore.summary_citations assert isinstance(result, list) assert len(result) == 2 dstore.close() def test_describe_sqlite_with_display(tmp_dir): from scinexus.data_store import set_summary_display path = tmp_dir / "test_display.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) captured = {} def display(data, *, name=""): captured["data"] = data captured["name"] = name return "DISPLAY" set_summary_display(display) try: result = dstore.describe assert result == "DISPLAY" assert captured["name"] == "describe" assert "title" in captured["data"] assert "completed" in captured["data"] finally: set_summary_display(None) dstore.close() def test_open_sqlite_db_ro_invalid_schema(tmp_dir): path = tmp_dir / "bad_schema.sqlitedb" db = sqlite3.connect(str(path)) db.execute("CREATE TABLE IF NOT EXISTS bogus(id INTEGER PRIMARY KEY)") db.close() with pytest.raises(ValueError, match="valid schema"): open_sqlite_db_ro(path) def test_lock_raises_when_db_none(tmp_dir): """locking a store whose connection has gone is a programming error""" path = tmp_dir / "lock_none.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore._db.close() dstore._db = None with pytest.raises(RuntimeError, match="unexpectedly None"): dstore.lock() def test_lock_overwrite_on_locked_db(tmp_dir): path = tmp_dir / "lock_ow.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) _ = dstore.db # opens and locks # fake a different pid in the lock dstore._db.execute( "UPDATE state SET lock_pid=? WHERE state_id=1", (os.getpid() + 1,), ) dstore2 = DataStoreSqlite(path, mode=OVERWRITE) # dstore2 opened a connection of its own as it was constructed. the # shared one that replaces it is a fiction of this test dstore2._db.close() dstore2._db = dstore._db with pytest.raises(OSError, match="locked"): dstore2.lock() # handed back before dstore2 is collected and reports a store it never # opened dstore2._db = None # and the lock handed back to the session the fake took it from, so the # close below releases it rather than reporting one left behind dstore._db.execute("UPDATE state SET lock_pid=?", (_owner_token(),)) dstore.close() def test_lock_update_existing_state(writable_store): writable_store.unlock() assert not writable_store.locked # re-lock: state row exists but lock_pid is NULL → UPDATE path writable_store.lock() assert writable_store.locked def test_unlock_readonly(populated_store): ro = DataStoreSqlite(populated_store, mode=READONLY) ro.unlock() # should be a no-op, no error ro.close() def test_unlock_already_unlocked(writable_store): writable_store.unlock() assert not writable_store.locked writable_store.unlock() # should be a no-op def test_write_duplicate_not_added_to_completed(writable_store): writable_store.write(unique_id="r1", data="d1_updated") assert len(writable_store.completed) == 1 @pytest.mark.parametrize("unique_id", ["", " ", ".hidden"]) def test_an_identifier_that_names_no_record_is_refused(writable_store, unique_id): """the identifier rule is the same one a directory store applies""" with pytest.raises(ValueError): writable_store.write(unique_id=unique_id, data="data") @pytest.mark.parametrize("unique_id", ["", ".hidden"]) def test_a_not_completed_identifier_is_refused_too(writable_store, unique_id): """both kinds of record, as in a directory store""" with pytest.raises(ValueError): writable_store.write_not_completed(unique_id=unique_id, data="{}") def test_write_log_with_table_prefix(tmp_dir, DATA_DIR): path = tmp_dir / "log_prefix.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) log_text = (DATA_DIR / "scitrack.log").read_text() dstore.write_log(unique_id=f"{LOG_TABLE}/test.log", data=log_text) assert len(dstore.logs) == 1 dstore.close() def test_write_not_completed_with_table_prefix(tmp_dir): path = tmp_dir / "nc_prefix.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) nc = NotCompleted(NotCompletedType.FAIL, "dummy", "msg", source="src") dstore.write_not_completed(unique_id=f"{RESULT_TABLE}/nc1", data=nc.to_json()) assert len(dstore.not_completed) == 1 dstore.close() def test_write_citations_update_existing(tmp_dir, sample_citations): path = tmp_dir / "cite_update.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write_citations(data=sample_citations) # write again to trigger UPDATE path dstore.write_citations(data=(sample_citations[0],)) loaded = dstore._load_citations() assert len(loaded) == 1 dstore.close() def test_load_citations_no_table(tmp_dir): path = tmp_dir / "no_cite_table.sqlitedb" # create db without citations table db = sqlite3.connect(str(path)) db.execute( "CREATE TABLE IF NOT EXISTS state" "(state_id INTEGER PRIMARY KEY, record_type TEXT, lock_pid INTEGER)", ) db.execute( f"CREATE TABLE IF NOT EXISTS {LOG_TABLE}" "(log_id INTEGER PRIMARY KEY, log_name TEXT, date timestamp, data BLOB)", ) db.execute( f"CREATE TABLE IF NOT EXISTS {RESULT_TABLE}" "(record_id TEXT PRIMARY KEY, log_id INTEGER, md5 BLOB, is_completed INTEGER, data BLOB)", ) db.close() dstore = DataStoreSqlite(path, mode=READONLY) result = dstore._load_citations() assert result == [] dstore.close() def test_describe_locked_by_another_session(writable_store): """the description names the holder and this session separately""" foreign = f"{os.getpid()}:{threading.get_native_id() + 1}" writable_store._db.execute( "UPDATE state SET lock_pid=? WHERE state_id=1", (foreign,), ) result = writable_store._describe() assert "Locked db store" in result["title"] assert foreign in result["title"] assert _owner_token() in result["title"] # handed back, so the fixture's close() releases the lock rather than # reporting one left behind writable_store._db.execute("UPDATE state SET lock_pid=?", (_owner_token(),)) def test_describe_locked_by_this_session(writable_store): """a store this session holds says so rather than naming tokens""" assert writable_store._lock_id == _owner_token() result = writable_store._describe() assert result["title"] == "Locked to the current process and thread." def test_describe_unlocked(writable_store): writable_store.unlock() result = writable_store._describe() assert result["title"] == "Unlocked db store." def test_record_type_getter_and_setter(writable_store): from scinexus.misc import get_object_provenance writable_store.record_type = str # set using a type object assert writable_store.record_type == get_object_provenance(str) def test_record_type_overwrite_error(writable_store): writable_store.record_type = str with pytest.raises(OSError, match="cannot overwrite"): writable_store.record_type = int def test_summary_not_completed(tmp_dir): path = tmp_dir / "summary_nc.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) nc = NotCompleted(NotCompletedType.FAIL, "dummy", "test msg", source="src") dstore.write_not_completed(unique_id="nc1", data=nc.to_json()) result = dstore._summary_not_completed() assert isinstance(result, list) assert len(result) == 1 dstore.close() def test_db_property_none_after_open(tmp_dir): from unittest.mock import patch path = tmp_dir / "db_none.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) # the store opened its database as it was constructed, so the open # under test is the one that follows a connection going away dstore._db.close() dstore._db = None with ( patch("scinexus.sqlite_data_store.open_sqlite_db_rw", return_value=None), patch.object(dstore, "lock"), ): with pytest.raises(ValueError, match="unexpectedly None"): _ = dstore.db def test_write_member_none(tmp_dir): from unittest.mock import patch path = tmp_dir / "write_none.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) with patch.object(dstore, "_write", return_value=None): with pytest.raises(RuntimeError, match="failed to produce a member"): dstore.write(unique_id="r1", data="d1") dstore.close() def test_write_not_completed_member_none(tmp_dir): from unittest.mock import patch path = tmp_dir / "nc_none.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) nc = NotCompleted(NotCompletedType.FAIL, "dummy", "msg", source="src") with patch.object(dstore, "_write", return_value=None): with pytest.raises(RuntimeError, match="failed to produce a member"): dstore.write_not_completed(unique_id="nc1", data=nc.to_json()) dstore.close() def test_write_citations_no_table(tmp_dir, sample_citations): path = tmp_dir / "no_cite_write.sqlitedb" db = sqlite3.connect(str(path)) db.execute( "CREATE TABLE IF NOT EXISTS state" "(state_id INTEGER PRIMARY KEY, record_type TEXT, lock_pid INTEGER)", ) db.execute( f"CREATE TABLE IF NOT EXISTS {LOG_TABLE}" "(log_id INTEGER PRIMARY KEY, log_name TEXT, date timestamp, data BLOB)", ) db.execute( f"CREATE TABLE IF NOT EXISTS {RESULT_TABLE}" "(record_id TEXT PRIMARY KEY, log_id INTEGER, md5 BLOB, " "is_completed INTEGER, data BLOB)", ) db.close() dstore = DataStoreSqlite(path, mode=OVERWRITE) # opening the store created the citations table, so take it away again: # what is under test is the store meeting a database without one dstore._db.execute("DROP TABLE citations") dstore._db.close() dstore._db = sqlite3.connect( str(path), detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, ) dstore._db.row_factory = sqlite3.Row assert not dstore._has_citations_table() dstore.write_citations(data=sample_citations) assert dstore._has_citations_table() loaded = dstore._load_citations() assert len(loaded) == 2 dstore.close() _RECORDS = 400 _READERS = 12 _OPEN_RACERS = 4 _OPEN_RACE_ROUNDS = 20 @define_app(app_type=LOADER) class read_member: """loader naming each record alongside what it holds""" def main(self, member: DataMemberABC) -> str: return f"{member.unique_id}={member.read()}" def _expected_records(): """identifier to contents, each naming the other""" return {f"k{i:05d}": f"v{i:05d}" for i in range(_RECORDS)} @pytest.fixture def many_record_store(tmp_dir): """path to a closed store holding _RECORDS records""" path = tmp_dir / "many.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) for unique_id, data in _expected_records().items(): dstore.write(unique_id=unique_id, data=data) dstore.close() return path def _run_on_threads(target, num_threads): """call target() on num_threads threads that start together Raises if any of them is still running afterwards. """ barrier = threading.Barrier(num_threads, timeout=_LOCK_TIMEOUT) def run(): barrier.wait(timeout=_LOCK_TIMEOUT) target() threads = [threading.Thread(target=run) for _ in range(num_threads)] # on a build with the GIL this makes the interleaving a check-then-set # races on happen within a few hundred instructions. on a free-threaded # one it does nothing, because the threads are already running at once old_interval = sys.getswitchinterval() sys.setswitchinterval(1e-6) try: for thread in threads: thread.start() for thread in threads: thread.join(timeout=_LOCK_TIMEOUT) finally: sys.setswitchinterval(old_interval) # a thread wedged on a lock leaves whatever it was counting empty, # which every assertion about "no wrong values" is satisfied by alive = [thread.name for thread in threads if thread.is_alive()] assert alive == [], f"still running after {_LOCK_TIMEOUT}s: {alive}" def _load_through_threads(dstore): """what a loader over dstore produces on the thread backend""" set_parallel_backend("threads") app = read_member() results = app.as_completed(dstore, parallel=True, show_progress=False) return {r.obj if isinstance(r, source_proxy) else r for r in results} @pytest.mark.free_threaded def test_concurrent_reads_answer_with_their_own_record(many_record_store): """a record read from any thread is the record that was asked for""" # a connection carries one statement cache, keyed by the text of the # statement and shared between threads without protection, so two # threads running this SELECT with different identifiers otherwise bind # to one statement and read each other's rows. wrong values are counted # rather than only exceptions: an exception is the loudest form of the # damage, not the usual one dstore = DataStoreSqlite(many_record_store, mode=READONLY) expected = _expected_records() right = [] wrong = [] errors = [] def read_every_record(): try: for unique_id, data in expected.items(): got = dstore.read(unique_id) (right if got == data else wrong).append((unique_id, got)) except Exception as err: # noqa: BLE001 errors.append(repr(err)) _run_on_threads(read_every_record, _READERS) assert errors == [] assert wrong == [] # counted, so that threads which never got to read cannot pass this by # leaving the other two empty assert len(right) == _READERS * _RECORDS dstore.close() @pytest.mark.free_threaded @pytest.mark.usefixtures("reset_parallel_backend") def test_a_loader_reads_a_store_through_the_thread_backend(many_record_store): """a pipeline over a .sqlitedb gets every record back, whole""" dstore = DataStoreSqlite(many_record_store, mode=READONLY) got = _load_through_threads(dstore) assert got == {f"{k}={v}" for k, v in _expected_records().items()} dstore.close() @pytest.mark.free_threaded @pytest.mark.usefixtures("reset_parallel_backend") def test_a_loader_reads_an_in_memory_store_through_the_thread_backend(): """an in-memory store is read from the pool like any other""" # its database lives inside its one connection, so there is nothing for # a second connection to open and serialising that one is the only # arrangement available to it dstore = DataStoreSqlite(_MEMORY, mode=OVERWRITE) for unique_id, data in _expected_records().items(): dstore.write(unique_id=unique_id, data=data) got = _load_through_threads(dstore) assert got == {f"{k}={v}" for k, v in _expected_records().items()} dstore.close() def _connections_from_racing_threads(dstore, num_threads): """the connection each of several threads got from one store at once""" opened = [] def open_it(): opened.append(dstore._connection()) _run_on_threads(open_it, num_threads) return opened @pytest.mark.free_threaded def test_racing_the_first_access_opens_one_connection(many_record_store): """the database is opened once, however many threads reach it together""" # the open is a check then a set, and every loser's connection is # orphaned: a descriptor and a page cache nothing will ever close for _ in range(_OPEN_RACE_ROUNDS): dstore = DataStoreSqlite(many_record_store, mode=READONLY) opened = _connections_from_racing_threads(dstore, _OPEN_RACERS) assert len(opened) == _OPEN_RACERS assert len({id(conn) for conn in opened}) == 1 dstore.close() @pytest.mark.free_threaded def test_concurrent_completed_scans_the_store_once(many_record_store, monkeypatch): """the members are read from the database once, not once per reader""" dstore = DataStoreSqlite(many_record_store, mode=READONLY) scans = [] select_members = dstore._select_members def counted(**kwargs): scans.append(kwargs["table_name"]) return select_members(**kwargs) monkeypatch.setattr(dstore, "_select_members", counted) counts = [] def take_completed(): counts.append(len(dstore.completed)) _run_on_threads(take_completed, _READERS) assert counts == [_RECORDS] * _READERS assert scans == [RESULT_TABLE] dstore.close() def _run_on_one_worker(target): """call target() on a single thread and wait for it""" worker = threading.Thread(target=target) worker.start() worker.join(timeout=_LOCK_TIMEOUT) assert not worker.is_alive(), f"still running after {_LOCK_TIMEOUT}s" @pytest.mark.free_threaded def test_a_worker_reading_a_writable_store_leaves_its_lock_alone(tmp_dir): """the lock names the session that opened the store, not one that read""" # ownership is thread scoped, so a worker that claimed would hold a # lock the master can neither write behind nor release path = tmp_dir / "workerread.sqlitedb" seed = DataStoreSqlite(path, mode=OVERWRITE) seed.write(unique_id="r1", data="d1") seed.close() dstore = DataStoreSqlite(path, mode=APPEND) got = [] _run_on_one_worker(lambda: got.append(dstore.read("r1"))) assert got == ["d1"] assert dstore._lock_id == _owner_token() dstore.close() db = sqlite3.connect(path) try: assert db.execute("SELECT lock_pid FROM state").fetchone()[0] is None finally: db.close() @pytest.mark.free_threaded def test_a_worker_reading_an_unlocked_store_does_not_take_it_over(tmp_dir): """a lock released by hand is not picked up by the next thread to read""" path = tmp_dir / "handreleased.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") dstore.unlock() assert not dstore.locked got = [] _run_on_one_worker(lambda: got.append(dstore.read("r1"))) assert got == ["d1"] assert dstore._lock_id is None # and the session that opened it still takes the lock when it reads assert dstore.read("r1") == "d1" assert dstore._lock_id == _owner_token() dstore.close() def test_a_store_built_off_the_master_claims_nothing_as_it_opens(tmp_dir, monkeypatch): """a store rebuilt in a worker is left to claim at its first use""" # a member of a writable store carries the store, so every worker # unpickling one would otherwise reach for a lock the master holds path = tmp_dir / "offmaster.sqlitedb" seed = DataStoreSqlite(path, mode=OVERWRITE) seed.write(unique_id="r1", data="d1") seed.close() monkeypatch.setattr("scinexus.sqlite_data_store.is_master_process", lambda: False) dstore = DataStoreSqlite(path, mode=APPEND) assert dstore._db is None assert not dstore._holds_lock assert not dstore.locked dstore.write(unique_id="r2", data="d2") assert dstore._lock_id == _owner_token() dstore.close() def _off_the_master(monkeypatch): """report every caller as a worker, as a pool worker would be""" monkeypatch.setattr("scinexus.sqlite_data_store.is_master_process", lambda: False) def test_a_worker_does_not_create_a_store(tmp_dir, monkeypatch): """only the master process creates a store, as for a directory store""" path = tmp_dir / "workermade.sqlitedb" _off_the_master(monkeypatch) dstore = DataStoreSqlite(path, mode=OVERWRITE) with pytest.raises(OSError, match="only the master process"): dstore.write(unique_id="r1", data="d1") assert not path.exists() def test_a_worker_does_not_add_a_schema_to_an_empty_file(tmp_dir, monkeypatch): """an empty file is a database with no tables, and stays one""" path = tmp_dir / "noschema.sqlitedb" path.write_text("") _off_the_master(monkeypatch) dstore = DataStoreSqlite(path, mode=OVERWRITE) with pytest.raises(OSError, match="only the master process"): dstore.write(unique_id="r1", data="d1") def test_a_worker_refused_a_file_that_is_no_database_drops_its_connection( tmp_dir, monkeypatch ): """the schema question raises rather than answering, and still closes""" path = tmp_dir / "notadatabase.sqlitedb" path.write_bytes(b"not a database" * 100) _off_the_master(monkeypatch) dstore = DataStoreSqlite(path, mode=OVERWRITE) opened = [] real_connect = sqlite3.connect def spy(*args, **kwargs): db = real_connect(*args, **kwargs) opened.append(db) return db monkeypatch.setattr(sqlite3, "connect", spy) with pytest.raises(sqlite3.DatabaseError): dstore.write(unique_id="r1", data="d1") assert len(opened) == 1 with pytest.raises(sqlite3.ProgrammingError, match="[Cc]losed"): opened[0].execute("SELECT 1") def test_a_worker_writes_to_a_store_the_master_made(tmp_dir, monkeypatch): """the refusal is about creating, not about using""" path = tmp_dir / "mastermade.sqlitedb" seed = DataStoreSqlite(path, mode=OVERWRITE) seed.write(unique_id="r1", data="d1") seed.close() _off_the_master(monkeypatch) dstore = DataStoreSqlite(path, mode=APPEND) dstore.write(unique_id="r2", data="d2") assert dstore.read("r1") == "d1" assert dstore.read("r2") == "d2" dstore.close() def test_a_worker_creates_an_in_memory_store(monkeypatch): """an in-memory store is reachable from nowhere else, so it is exempt""" _off_the_master(monkeypatch) dstore = DataStoreSqlite(":memory:", mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") assert dstore.read("r1") == "d1" dstore.close() def test_close_reports_a_lock_it_cannot_release(tmp_dir): """a store going away says so if its lock stays behind""" # the lock outlives the store that took it, so silence here leaves the # next session refused by a thread that no longer exists path = tmp_dir / "stranded.sqlitedb" dstore = DataStoreSqlite(path, mode=OVERWRITE) dstore.write(unique_id="r1", data="d1") foreign = f"{os.getpid()}:{threading.get_native_id() + 1}" dstore._db.execute("UPDATE state SET lock_pid=?", (foreign,)) with pytest.warns(UserWarning, match="is still locked by"): dstore.close() db = sqlite3.connect(path) try: assert db.execute("SELECT lock_pid FROM state").fetchone()[0] == foreign finally: db.close() def _leave_a_lock_behind(path): """a database carrying a lock whose store is gone""" abandoned = DataStoreSqlite(path, mode=OVERWRITE) abandoned.write(unique_id="r1", data="d1") abandoned._db.close() abandoned._db = None abandoned._closed = True def test_a_locked_database_is_taken_quietly_as_the_store_opens(tmp_dir, recwarn): """opening a store for writing says nothing about a lock it could not take""" # opening is not writing: this store is also what unlock(force=True) has # to be called on, and what a caller reading a locked store gets path = tmp_dir / "lockedatopen.sqlitedb" _leave_a_lock_behind(path) dstore = DataStoreSqlite(path, mode=OVERWRITE) assert [w for w in recwarn if issubclass(w.category, UserWarning)] == [] assert not dstore._holds_lock # the write that meets the lock is what raises with pytest.raises(OSError, match="locked"): dstore.write(unique_id="r2", data="d2") dstore.unlock(force=True) dstore.write(unique_id="r2", data="d2") assert dstore.read("r2") == "d2" dstore.close() def test_claiming_reports_a_lock_it_could_not_take_when_asked(tmp_dir): """the report is available to a caller that wants one""" path = tmp_dir / "claimreport.sqlitedb" _leave_a_lock_behind(path) dstore = DataStoreSqlite(path, mode=OVERWRITE) with pytest.warns(UserWarning, match="locked by"): dstore._open_and_claim(warn=True) assert not dstore._holds_lock dstore.unlock(force=True) dstore.close() @pytest.mark.free_threaded def test_a_worker_may_read_a_writable_store_but_not_write_it(writable_store): """a store is written by the session that opened it""" # part of the same model: the record would be logged against a session # that never wrote it, and written while no lock of this session's is # on the database got = [] refused = [] tokens = [] def read_then_write(): tokens.append(_owner_token()) got.append(writable_store.read("r1")) try: writable_store.write(unique_id="r2", data="d2") except OSError as err: refused.append(str(err)) _run_on_one_worker(read_then_write) assert got == ["d1"] assert len(refused) == 1 # both halves: the session that may write, and the one turned away. # taken inside the worker, since the token names the thread assert _owner_token() in refused[0] assert tokens[0] in refused[0] assert tokens[0] != _owner_token() assert "r2" not in writable_store cogent3-scinexus-b16bc06/tests/test_sync.py000066400000000000000000000042751525761402500210660ustar00rootroot00000000000000import copy import pickle import threading from typing import Self from scinexus._sync import LockMixin class _Owner(LockMixin): """minimal owner, following the contract of making the lock in __new__""" def __new__(cls, value: int = 0) -> Self: obj = object.__new__(cls) obj._cache_lock = cls.new_lock() obj.value = value return obj def test_new_lock_is_reentrant(): """the guarded regions re-enter each other, so a plain lock would hang""" lock = LockMixin.new_lock() with lock, lock: assert True def test_lock_is_created_by_new(): owner = _Owner(3) assert isinstance(owner._cache_lock, type(LockMixin.new_lock())) def test_getstate_drops_the_lock(): owner = _Owner(3) state = owner.__getstate__() assert "_cache_lock" not in state assert state["value"] == 3 def test_getstate_tolerates_a_missing_lock(): """an instance built without __new__ has no lock to drop""" owner = object.__new__(_Owner) owner.value = 1 assert owner.__getstate__() == {"value": 1} def test_pickle_roundtrip_restores_a_usable_lock(): owner = _Owner(7) restored = pickle.loads(pickle.dumps(owner)) assert restored.value == 7 with restored._cache_lock: assert True def test_deepcopy_restores_a_usable_lock(): """apps deepcopy their arguments on every call, so this is a hot path""" owner = _Owner(7) restored = copy.deepcopy(owner) assert restored.value == 7 with restored._cache_lock: assert True def test_locks_are_per_instance(): first = _Owner(1) second = _Owner(2) assert first._cache_lock is not second._cache_lock def test_lock_actually_excludes(): """two threads must not be inside the guarded region at once""" owner = _Owner() inside = 0 overlapped = False def enter(): nonlocal inside, overlapped with owner._cache_lock: inside += 1 if inside > 1: overlapped = True inside -= 1 threads = [threading.Thread(target=enter) for _ in range(8)] for thread in threads: thread.start() for thread in threads: thread.join(timeout=30) assert not overlapped cogent3-scinexus-b16bc06/tests/test_typing.py000066400000000000000000000233131525761402500214160ustar00rootroot00000000000000from pathlib import Path from typing import Any, ForwardRef, TypeVar, Union, get_args import pytest from scinexus.data_store import DataMemberABC from scinexus.typing import ( IdentifierType, SerialisableType, _clear_type_namespace_providers, _resolve_name, _type_namespace_providers, check_type_compatibility, get_type_display_names, register_type_namespace, resolve_type_hint, ) @pytest.fixture def clean_namespace_providers(): """isolate tests that register namespace providers""" saved = list(_type_namespace_providers) _clear_type_namespace_providers() try: yield finally: _clear_type_namespace_providers() for provider in saved: register_type_namespace(provider) def test_resolve_type_hint_concrete_class(): """Concrete classes pass through unchanged""" resolved = resolve_type_hint(int) assert resolved is int def test_resolve_type_hint_protocol(): """Protocol classes pass through unchanged""" resolved = resolve_type_hint(SerialisableType) assert resolved is SerialisableType def test_resolve_type_hint_union(): """Union types are resolved recursively""" resolved = resolve_type_hint(IdentifierType) from typing import get_args args = set(get_args(resolved)) assert args == {str, Path, DataMemberABC} def test_resolve_type_hint_unconstrained_typevar(): """unconstrained TypeVar raises TypeError""" T = TypeVar("T") with pytest.raises(TypeError, match="unconstrained TypeVar"): resolve_type_hint(T) def test_resolve_type_hint_unresolvable(): """Unresolvable string raises TypeError""" with pytest.raises(TypeError, match="cannot resolve"): resolve_type_hint("NoSuchType") def test_resolve_type_hint_user_module_globals(): """module_globals are checked first for resolution""" class MyCustomType: pass resolved = resolve_type_hint("MyCustomType", {"MyCustomType": MyCustomType}) assert resolved is MyCustomType def test_register_type_namespace_resolves_forward_ref(clean_namespace_providers): """a registered namespace provider supplies fallback types""" class MyCustomType: pass register_type_namespace(lambda: {"MyCustomType": MyCustomType}) assert resolve_type_hint("MyCustomType") is MyCustomType def test_register_type_namespace_is_lazy(clean_namespace_providers): """providers are invoked on resolution, not at registration time""" calls = {"count": 0} class MyCustomType: pass def provider(): calls["count"] += 1 return {"MyCustomType": MyCustomType} register_type_namespace(provider) assert calls["count"] == 0 resolve_type_hint("MyCustomType") assert calls["count"] == 1 resolve_type_hint("MyCustomType") assert calls["count"] == 2 def test_register_type_namespace_dedupes(clean_namespace_providers): """re-registering the same provider is a no-op""" def provider(): return {} register_type_namespace(provider) register_type_namespace(provider) assert _type_namespace_providers.count(provider) == 1 def test_module_globals_take_precedence_over_namespace_provider( clean_namespace_providers, ): """module_globals win over registered providers when both supply a name""" class FromGlobals: pass class FromProvider: pass register_type_namespace(lambda: {"Foo": FromProvider}) assert resolve_type_hint("Foo", {"Foo": FromGlobals}) is FromGlobals def test_typevar_bound_forwardref_resolved_via_provider(clean_namespace_providers): """a TypeVar with a ForwardRef bound resolves via a registered provider""" class MyCustomType: pass register_type_namespace(lambda: {"MyCustomType": MyCustomType}) T = TypeVar("T", bound=ForwardRef("MyCustomType")) assert resolve_type_hint(T) is MyCustomType def test_unresolved_name_with_empty_providers_raises(clean_namespace_providers): """with no providers, an unknown name still raises TypeError""" with pytest.raises(TypeError, match="cannot resolve"): resolve_type_hint("NoSuchType") def test_serialisable_type_not_isinstance(): """objects without to_rich_dict do not satisfy SerialisableType""" assert not isinstance("hello", SerialisableType) assert not isinstance(42, SerialisableType) def test_serialisable_type_custom_class(): """custom class with to_rich_dict satisfies SerialisableType""" class MyObj: def to_rich_dict(self) -> dict: return {} assert isinstance(MyObj(), SerialisableType) def test_get_type_display_names_concrete(): """concrete class returns its name""" names = get_type_display_names(int) assert names == frozenset({"int"}) def test_get_type_display_names_union(): """Union returns names of all constituents""" names = get_type_display_names(Union[str, int]) # noqa: UP007 assert names == frozenset({"str", "int"}) def test_get_type_display_names_protocol(): """Protocol returns its own name""" names = get_type_display_names(SerialisableType) assert names == frozenset({"SerialisableType"}) def test_get_type_display_names_typevar_fallback(): """unresolved TypeVar returns its __name__""" T = TypeVar("T") names = get_type_display_names(T) assert names == frozenset({"T"}) def test_check_type_compatibility_protocol_input(): """Protocol on input side is lenient""" assert check_type_compatibility(int, SerialisableType) is True def test_check_type_compatibility_protocol_return(): """Protocol on return side is lenient""" assert check_type_compatibility(SerialisableType, int) is True def test_check_type_compatibility_incompatible(): """incompatible concrete types""" assert check_type_compatibility(int, str) is False @pytest.fixture def broken_subclasscheck(): """a class whose metaclass makes issubclass() raise TypeError""" class BadMeta(type): def __subclasscheck__(cls, subclass: Any): msg = "broken" raise TypeError(msg) class Weird(metaclass=BadMeta): pass return Weird def test_check_type_compatibility_issubclass_typeerror_same(broken_subclasscheck): """issubclass TypeError with identity match returns True""" assert check_type_compatibility(broken_subclasscheck, broken_subclasscheck) is True def test_check_type_compatibility_issubclass_typeerror_diff(broken_subclasscheck): """issubclass TypeError with different classes returns False""" assert check_type_compatibility(int, broken_subclasscheck) is False def test_resolve_name_non_type_in_globals(): """non-type value in module_globals falls through to TypeError""" with pytest.raises(TypeError, match="cannot resolve"): _resolve_name("foo", {"foo": 42}) def test_resolve_type_hint_typevar_bound(): """TypeVar with bound resolves to the bound type""" T = TypeVar("T", bound=int) assert resolve_type_hint(T) is int def test_resolve_type_hint_typevar_bound_str(): """TypeVar with string bound resolved via module_globals""" class Custom: pass T = TypeVar("T", bound="Custom") resolved = resolve_type_hint(T, {"Custom": Custom}) assert resolved is Custom def test_resolve_type_hint_typevar_bound_forwardref(): """TypeVar with ForwardRef bound resolved via module_globals""" class Custom: pass T = TypeVar("T", bound=ForwardRef("Custom")) resolved = resolve_type_hint(T, {"Custom": Custom}) assert resolved is Custom def test_resolve_type_hint_typevar_constraints(): """TypeVar with constraints resolves to Union of constraints""" T = TypeVar("T", int, str) resolved = resolve_type_hint(T) assert set(get_args(resolved)) == {int, str} def test_resolve_type_hint_union_type(): """PEP 604 X | Y syntax resolves correctly""" resolved = resolve_type_hint(int | str) assert set(get_args(resolved)) == {int, str} def test_resolve_type_hint_list(): """list[int] resolves correctly""" resolved = resolve_type_hint(list[int]) assert get_args(resolved) == (int,) def test_resolve_type_hint_tuple(): """tuple[str, int] resolves correctly""" resolved = resolve_type_hint(tuple[str, int]) assert get_args(resolved) == (str, int) def test_resolve_type_hint_set(): """set[int] resolves correctly""" resolved = resolve_type_hint(set[int]) assert get_args(resolved) == (int,) def test_resolve_type_hint_forwardref(): """ForwardRef resolves via module_globals""" resolved = resolve_type_hint(ForwardRef("int"), {"int": int}) assert resolved is int def test_get_type_display_names_list(): """list[int] returns inner type names""" names = get_type_display_names(list[int]) assert names == frozenset({"int"}) def test_get_type_display_names_tuple(): """tuple[str, int] returns all inner type names""" names = get_type_display_names(tuple[str, int]) assert names == frozenset({"str", "int"}) def test_check_type_compatibility_any_return(): """Any as return type is compatible with anything""" assert check_type_compatibility(Any, int) is True def test_check_type_compatibility_any_input(): """Any as input type is compatible with anything""" assert check_type_compatibility(int, Any) is True def test_check_type_compatibility_subclass(): """bool is subclass of int, so they are compatible""" assert check_type_compatibility(bool, int) is True def test_check_type_compatibility_union_subclass(): """Union types with subclass relationship are compatible""" assert check_type_compatibility(Union[bool, str], int) is True def test_check_type_compatibility_protocol_in_union(): """Union containing a Protocol is lenient""" assert check_type_compatibility(Union[SerialisableType, int], str) is True cogent3-scinexus-b16bc06/tests/test_warning.py000066400000000000000000000147371525761402500215630ustar00rootroot00000000000000import pickle import warnings import pytest from scinexus.warning import deprecated_args, deprecated_callable def test_function_deprecated_args(): @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(a: int, b: int) -> int: return a + b with pytest.deprecated_call(): expected = changed(a=5, b=3) got = changed(x=5, y=3) assert got == expected def test_function_deprecated_args_docstring(): @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(a: int, b: int) -> int: """This is a test function""" return a + b assert changed.__doc__ == "This is a test function" @pytest.mark.parametrize( "kwargs", [{"x": 5, "y": 3}, {"a": 5, "y": 3}, {"x": 5, "b": 3}], ) def test_function_deprecated_args_warn(kwargs): @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(a: int, b: int) -> int: return a + b with pytest.deprecated_call(): changed(**kwargs) def test_function_correct_args_do_not_warn(): @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(a: int, b: int) -> int: return a + b with warnings.catch_warnings(): warnings.simplefilter("error") changed(a=5, b=3) def test_function_deprecated_args_pickled(): @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(a: int, b: int) -> int: return a + b with pytest.deprecated_call(): myfunc = changed(x=1, y=2) pickled_func = pickle.dumps(myfunc) assert isinstance(pickled_func, bytes) unpickled_func = pickle.loads(pickled_func) assert unpickled_func == changed(a=1, b=2) class foo: def __init__(self): self.a = 0 self.b = 0 @deprecated_args( version="a future release", reason="x and y are not descriptive", old_new=[("x", "a"), ("y", "b")], ) def changed(self, a: int, b: int): """This is a test function""" self.a = a self.b = b def test_method_deprecated_args(): foo_instance = foo() foo_instance.changed(a=5, b=3) assert foo_instance.a == 5 assert foo_instance.b == 3 def test_method_deprecated_args_docstring(): assert foo.changed.__doc__ == "This is a test function" foo_instance = foo() assert foo_instance.changed.__doc__ == "This is a test function" @pytest.mark.parametrize( "kwargs", [{"x": 5, "y": 3}, {"a": 5, "y": 3}, {"x": 5, "b": 3}], ) def test_method_deprecated_args_warn(kwargs): with pytest.deprecated_call(): foo().changed(**kwargs) def test_method_correct_args_do_not_warn(): with warnings.catch_warnings(): warnings.simplefilter("error") foo().changed(a=5, b=3) def test_method_deprecated_args_pickled(): foo_instance = foo() assert foo_instance.a == 0 assert foo_instance.b == 0 with pytest.deprecated_call(): foo_instance.changed(x=1, y=2) assert foo_instance.a == 1 assert foo_instance.b == 2 pickled_foo = pickle.dumps(foo_instance) assert isinstance(pickled_foo, bytes) unpickled_foo = pickle.loads(pickled_foo) assert isinstance(unpickled_foo, foo) assert unpickled_foo.a == 1 assert unpickled_foo.b == 2 with pytest.deprecated_call(): unpickled_foo.changed(x=2, y=3) assert unpickled_foo.a == 2 assert unpickled_foo.b == 3 class foo2: @deprecated_callable(version="2023.9", reason="test meth", new="new_meth") def old_meth(self, v): return v**2 @deprecated_callable(version="2023.9", reason="redundant", is_discontinued=True) def squared(self, v): return v * v @deprecated_callable(version="2023.9", reason="test func", new="new_func") def old_func(v): return v**3 @deprecated_callable(version="2023.9", reason="redundant", is_discontinued=True) def cubed(v): return v * v @pytest.mark.parametrize("func", [foo2().old_meth, foo2().squared, old_func, cubed]) def test_deprecated_callable_warn(func): with pytest.deprecated_call(): func(2) @pytest.mark.parametrize("func", [cubed, old_func]) def test_method_deprecated_function_pickling(recwarn, func): pickled_func = pickle.dumps(func) assert isinstance(pickled_func, bytes) unpickled_func = pickle.loads(pickled_func) assert unpickled_func(20) == func(20) def test_method_deprecated_method_pickling(recwarn): instance = foo2() pickled = pickle.dumps(instance) assert isinstance(pickled, bytes) unpickled = pickle.loads(pickled) assert unpickled.old_meth(20) == instance.old_meth(20) assert unpickled.squared(20) == instance.squared(20) @pytest.mark.parametrize( ("func", "_type"), [(foo2().old_meth, "method"), (cubed, "function")], ) def test_deprecated_callable_resolves_type(recwarn, func, _type): func(2) assert any(_type in str(e) for e in recwarn.list), recwarn.list def test_function_deprecated_args_deprecated_callable_chained_decorators(recwarn): @deprecated_args( version="2023.6", reason="x is not descriptive", old_new=[("x", "a")] ) @deprecated_args( version="2023.6", reason="b is no longer required", discontinued=["b"] ) @deprecated_callable( version="2023.6", reason="Improved change function", new="changed2", is_discontinued=True, ) def changed(a: int) -> int: return a got = changed(x=5, b=3) assert got == 5 warn_msgs = [warning.message.args[0] for warning in recwarn.list] assert any("argument x will be removed" in w for w in warn_msgs) assert any("argument b is discontinued" in w for w in warn_msgs) assert any("function changed is discontinued" in w for w in warn_msgs) def test_class_deprecated(recwarn): class fooclass: @deprecated_callable( version="2023.6", reason="Improved change function", is_discontinued=True ) def __init__(self): ... fooclass() warn_msgs = [warning.message.args[0] for warning in recwarn.list] assert any("fooclass is discontinued" in w for w in warn_msgs) cogent3-scinexus-b16bc06/uv.lock000066400000000000000000022122401525761402500166360ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "appnope" version = "0.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] [[package]] name = "argcomplete" version = "3.7.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, ] [[package]] name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi-bindings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] [[package]] name = "argon2-cffi-bindings" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, ] [[package]] name = "arrow" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] [[package]] name = "ast-serialize" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, ] [[package]] name = "asttokens" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] [[package]] name = "async-lru" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "beautifulsoup4" version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] name = "bleach" version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, ] [package.optional-dependencies] css = [ { name = "tinycss2" }, ] [[package]] name = "certifi" version = "2026.7.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[package]] name = "charset-normalizer" version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] name = "choreographer" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "logistro" }, { name = "platformdirs" }, { name = "simplejson" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/69/3058cd4f16d6b75c80e8f95e5b713d930526353ce294df9a7887453ba215/choreographer-1.3.0.tar.gz", hash = "sha256:6c44a0e48e9b37977344d40bfa5a9ed88575fe4bc0fd836771bf702bc24d6884", size = 48291, upload-time = "2026-04-28T22:57:45.114Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/6c/ff8bf52315064dbeb55cb5067e191120a5b2e58bb648d0d34cf7969dc2c2/choreographer-1.3.0-py3-none-any.whl", hash = "sha256:cea4cb739e4f61625e4b53888a8d3fa1d3bf73948b56753e460ab44da7d8d44f", size = 52622, upload-time = "2026-04-28T22:57:44.015Z" }, ] [[package]] name = "citeable" version = "2026.3.11b1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/2e/145346ec16350c8c1ced7cfeefb190e056928e7a39d65d86d22488be3bb6/citeable-2026.3.11b1.tar.gz", hash = "sha256:185915fb852b58b64ab82a4d90cbc91ef4033650e42e286467d1758c10edfac0", size = 52516, upload-time = "2026-03-13T00:01:11.783Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f0/76/f208c2b7167b39ea15c116683930200a450219db8a382ecd6584779b2129/citeable-2026.3.11b1-py3-none-any.whl", hash = "sha256:04a939569c6dd6cd0e934440562d8fc026e59e8ae05887dbfc93c0690672efa0", size = 14439, upload-time = "2026-03-13T00:01:10.482Z" }, ] [[package]] name = "click" version = "8.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] name = "click-log" version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, ] [[package]] name = "cloudpickle" version = "3.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, ] [[package]] name = "cogapp" version = "3.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/64/ea/4ffa8095e0b675e9961cbdcad002c09d35d4ab76ff99d61a014e9e6bcd53/cogapp-3.6.0.tar.gz", hash = "sha256:ec2a9170bfa644bf0d91996fdb5576c13d7e5e848bb2378a6f92727b48f92604", size = 59742, upload-time = "2025-09-21T15:54:13.553Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/1c/23b5110b7cb305f0fe3a5d6809d6804c1cb57d57b59516dbb5e24aaf95fc/cogapp-3.6.0-py3-none-any.whl", hash = "sha256:6ea11c63221f92978d37d8d76208eadeae2a6e4641d323f165738a5464a19d32", size = 30550, upload-time = "2025-09-21T15:54:12.205Z" }, ] [[package]] name = "cogent3" version = "2026.9.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, { name = "citeable" }, { name = "numba", marker = "python_full_version < '3.15'" }, { name = "numpy" }, { name = "scinexus", extra = ["loky"] }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "stevedore" }, { name = "tqdm" }, { name = "typeguard" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/83/3deceb17d63dbcc30457a7a26ad9c9f77cc5b129c3c0de5d6ecce05541a6/cogent3-2026.9.10.tar.gz", hash = "sha256:4d86b31ad6e00c7420cfd8fd5d5a716035ace8ac594c4de6289049ac4205d772", size = 17160020, upload-time = "2026-09-10T12:18:22.253Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/23/40/42405c2c05b597fca7f4e1fea8fd556ff63cdbe02e0b27da65f6c2e735ac/cogent3-2026.9.10-py3-none-any.whl", hash = "sha256:ef548a47460e2c68681101246ba393cd3e30f03ede9058d62ebd784c546c30a1", size = 695947, upload-time = "2026-09-10T12:18:20Z" }, ] [package.optional-dependencies] extra = [ { name = "ipykernel" }, { name = "ipython" }, { name = "ipywidgets" }, { name = "jupyterlab" }, { name = "kaleido" }, { name = "notebook" }, { name = "pandas" }, { name = "pillow" }, { name = "plotly" }, { name = "psutil" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "colorlog" version = "6.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" }, ] [[package]] name = "comm" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] [[package]] name = "coverage" version = "7.15.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "debugpy" version = "1.8.21" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/89/fb/cbf306d6e07a313a91e7171a98669054502840931432c227cfd505ee367f/debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264", size = 2203120, upload-time = "2026-06-01T19:30:43.964Z" }, { url = "https://files.pythonhosted.org/packages/aa/57/aa739bd4ad2cbf96aeb1b20b56918ddd5ae4c28b68709bfcd327f02123ee/debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc", size = 3059958, upload-time = "2026-06-01T19:30:45.622Z" }, { url = "https://files.pythonhosted.org/packages/a8/31/453d2c9a23d133fe2c8ec7ca1d816ded52a913487fe3ffef7c01b4b706af/debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e", size = 5236515, upload-time = "2026-06-01T19:30:47.461Z" }, { url = "https://files.pythonhosted.org/packages/60/94/6660de2f2d7bf388f229335ba4637646eebabdbf38564cb439a95a9193c9/debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7", size = 5256138, upload-time = "2026-06-01T19:30:49.113Z" }, { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] name = "deepmerge" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] name = "dependency-groups" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/55/f054de99871e7beb81935dea8a10b90cd5ce42122b1c3081d5282fdb3621/dependency_groups-1.3.1.tar.gz", hash = "sha256:78078301090517fd938c19f64a53ce98c32834dfe0dee6b88004a569a6adfefd", size = 10093, upload-time = "2025-05-02T00:34:29.452Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, ] [[package]] name = "distlib" version = "0.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] name = "execnet" version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "fastjsonschema" version = "2.22.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/a4/9473c7c3b87009d9c1d74034e4a0f6a35ff0d42dd0f9866d0c3ec4e9217b/fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf", size = 385171, upload-time = "2026-08-15T19:47:08.853Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/82/2755c7c982086f00d4dab85bc120ec35045a9fc2191893a6ce79afe94443/fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", size = 27413, upload-time = "2026-08-15T19:47:04.406Z" }, ] [[package]] name = "filelock" version = "3.32.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" }, ] [[package]] name = "fqdn" version = "1.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "griffelib" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "humanize" version = "4.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" }, ] [[package]] name = "idna" version = "3.18" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipykernel" version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, { name = "nest-asyncio2" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, ] [[package]] name = "ipython" version = "9.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ipython-pygments-lexers" }, { name = "jedi" }, { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/96/b150fe7e25a5a29ae9ac1374e71488639605d39a1ea4abb74c9ce33af235/ipython-9.16.1.tar.gz", hash = "sha256:5a3d1f9a47ff216d6cf9cf863124f6a2c1a198d1354c546a4d24a370a283b64c", size = 4515302, upload-time = "2026-08-03T08:36:15.571Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/8e/1239df488393d61076653bfb29f759d0f60cab8e030abdf7c17c31539b51/ipython-9.16.1-py3-none-any.whl", hash = "sha256:4acae635506f6d352d94c4899a19d5f85f8bc4d230932342dca556fdab1c69b4", size = 625974, upload-time = "2026-08-03T08:36:13.654Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "ipywidgets" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, { name = "ipython" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, ] [[package]] name = "isoduration" version = "20.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arrow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] [[package]] name = "jedi" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "json5" version = "0.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, ] [[package]] name = "jsonpointer" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [package.optional-dependencies] format-nongpl = [ { name = "fqdn" }, { name = "idna" }, { name = "isoduration" }, { name = "jsonpointer" }, { name = "rfc3339-validator" }, { name = "rfc3986-validator" }, { name = "rfc3987-syntax" }, { name = "uri-template" }, { name = "webcolors" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "jupyter-builder" version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/e1/e4ef07e2be228271b59e011a746d97feef69577af1f465b1cff0f1d7c760/jupyter_builder-1.2.2.tar.gz", hash = "sha256:b6cea88f58e44b2c5eba96f28d2e0d16fd453d3ca6dc9c4492ff8a1f2e97f601", size = 981074, upload-time = "2026-08-07T06:47:18.742Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c4/3b/920bc7f3c2ad25abc0071ae7ae55789f4b85ecf3e4467f0602a88dc668cb/jupyter_builder-1.2.2-py3-none-any.whl", hash = "sha256:6ebcd4c49daf5df6a18068a74a48010406700ed90a76c189fac43eaf85c60c63", size = 915266, upload-time = "2026-08-07T06:47:16.249Z" }, ] [[package]] name = "jupyter-client" version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, { name = "python-dateutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] [[package]] name = "jupyter-events" version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, { name = "packaging" }, { name = "python-json-logger" }, { name = "pyyaml" }, { name = "referencing" }, { name = "rfc3339-validator" }, { name = "rfc3986-validator" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, ] [[package]] name = "jupyter-lsp" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, ] [[package]] name = "jupyter-server" version = "2.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "argon2-cffi" }, { name = "jinja2" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "jupyter-events" }, { name = "jupyter-server-terminals" }, { name = "nbconvert" }, { name = "nbformat" }, { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, { name = "tornado" }, { name = "traitlets" }, { name = "websocket-client" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, ] [[package]] name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, ] [[package]] name = "jupyterlab" version = "4.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, { name = "httpx" }, { name = "ipykernel" }, { name = "jinja2" }, { name = "jupyter-builder" }, { name = "jupyter-core" }, { name = "jupyter-lsp" }, { name = "jupyter-server" }, { name = "jupyterlab-server" }, { name = "notebook-shim" }, { name = "packaging" }, { name = "tornado" }, { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c0/45934229995d4c6e38192ca118d93185f60808f64157e3df3c5c28f8c5fd/jupyterlab-4.6.3.tar.gz", hash = "sha256:2e3db6e3a12495ebd188276e985bf5ac502fbde3d1e8628819920210008de498", size = 28319771, upload-time = "2026-08-10T18:50:57.947Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/47/242f46de028074651c9bd6d8000fc340ed0d3cdd1a0eae4387826123413a/jupyterlab-4.6.3-py3-none-any.whl", hash = "sha256:0a1ebc6567186f1eabd99536e94df7ed9e96d1e7c5ddf3e4406ae16e88abacb7", size = 17168312, upload-time = "2026-08-10T18:50:53.044Z" }, ] [[package]] name = "jupyterlab-pygments" version = "0.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, ] [[package]] name = "jupyterlab-server" version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "jinja2" }, { name = "json5" }, { name = "jsonschema" }, { name = "jupyter-server" }, { name = "packaging" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, ] [[package]] name = "jupyterlab-widgets" version = "3.0.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] [[package]] name = "kaleido" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "choreographer" }, { name = "logistro" }, { name = "orjson" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/64/53eac73d31dbfc3310ee2e87bcac1ae7417427f0fbe3dd800eaf676db324/kaleido-1.3.0.tar.gz", hash = "sha256:5e0378a7475e98852773deeb6483dee91f8aa7b364dde7b5f2b3622cb468a3e6", size = 68938, upload-time = "2026-05-04T19:45:28.932Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/b9/a6d8bb7d228940f01885bd9f327ab7f9d366a9be775c4bf366bf9d9477ae/kaleido-1.3.0-py3-none-any.whl", hash = "sha256:52714dfd38e8f2a114831826200c40bb10d0ca0c11d4272f3f48ad499cd8f8ea", size = 55580, upload-time = "2026-05-04T19:45:27.483Z" }, ] [[package]] name = "lark" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] [[package]] name = "librt" version = "0.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] name = "llvmlite" version = "0.49.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/27/72ae94ea5c8f7349ec1c229d4cd058feb799cbd0833ad6d1b47c919b37b7/llvmlite-0.49.0.tar.gz", hash = "sha256:00f16db782f4a13c78c5804aedc434e46794a77e89999a168f9401106270e50a", size = 194467, upload-time = "2026-08-11T16:26:00.489Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d0/ab52de2328e97ca96cdf0331a5f774796bddc420a51768f4501193f80cbb/llvmlite-0.49.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:4b0e710880b7cc910392bd6b9f1bbf468fed99b182e4420d51598f36114b3dce", size = 40479230, upload-time = "2026-08-11T16:23:28.744Z" }, { url = "https://files.pythonhosted.org/packages/1f/80/0989432d12b7c86a6f5f380eb92eca7de779af9b34dedbd311b694d7da8d/llvmlite-0.49.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a8c0fc9d624bdc30a3d2db11eb2fb98f80fb209d20b37604eda516cd9b699cf4", size = 59890659, upload-time = "2026-08-11T16:23:37.346Z" }, { url = "https://files.pythonhosted.org/packages/58/e9/76859ca36aaa460b6ae0508e01637f0e9bdb9b59faaa4637ade3b94bbcca/llvmlite-0.49.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20496a5c9fdb8179fb9300e7d19f6782555d98aeeb4a322264aa7fd99f980618", size = 58344482, upload-time = "2026-08-11T16:23:44.199Z" }, { url = "https://files.pythonhosted.org/packages/7d/49/47cd23e05d52d117b6119871ec299adedc9d8d332a2296964d9b2adc06d9/llvmlite-0.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a5b06c1b5fc4ae4c9b169b065f42b719448ef1f873687ef224ef69969b75ec3", size = 41865253, upload-time = "2026-08-11T16:23:50.198Z" }, { url = "https://files.pythonhosted.org/packages/6b/ae/3f699ebe3590e15e023a6372dd147526fd8ec398aacf9ceb844e854964a8/llvmlite-0.49.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b541c8fac3450db7574d1f53cf9dff83f285bfed9d69bf81fe71fc2a7d4f97fe", size = 40479231, upload-time = "2026-08-11T16:23:56.773Z" }, { url = "https://files.pythonhosted.org/packages/be/3c/e97f69c62a2d972066d9a2612ce1f3de313035ac897a5b9f787cad8b55f7/llvmlite-0.49.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6acba646d88abbc87d5c113a3d62c1fbf8b8fee11c6493f516803e30f21ae870", size = 59890658, upload-time = "2026-08-11T16:24:05.451Z" }, { url = "https://files.pythonhosted.org/packages/69/e6/e942ee08605fc0526ff3854260c384d8315a5830e16c4c2a5aebc14dc9bf/llvmlite-0.49.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec8ad805e7515cb8440a690eb3cef4d34acb29eef80b705ec4e1c1ad3c43c68", size = 58344481, upload-time = "2026-08-11T16:24:13.503Z" }, { url = "https://files.pythonhosted.org/packages/84/49/2a44871cac6b5a2fd4aabd68cfdaf6de9a5c7edb36dee5d47b77bda4b50f/llvmlite-0.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a9c9e3af4e214acfefa4f73ebe7bc3fb35854a62b654edb3953f5ae33c08ba3", size = 41865543, upload-time = "2026-08-11T16:24:20.41Z" }, { url = "https://files.pythonhosted.org/packages/7d/85/0b536a3c59f2636d9dd51dda832b6c1d0ffec37608429dedf128664918f1/llvmlite-0.49.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:039fa4054a06f537fb39248d4472284ca96be311a142ec09e69f95630ab469cc", size = 40479230, upload-time = "2026-08-11T16:24:27.295Z" }, { url = "https://files.pythonhosted.org/packages/8e/1c/ca8ba47b057b793099784475499771780ec46839f2782f753a7079d23520/llvmlite-0.49.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddc7aecd4f56397ed6e8f120ec5dcd5a1a8f0e6032ca4af413462792d4dca2e3", size = 59890659, upload-time = "2026-08-11T16:24:35.595Z" }, { url = "https://files.pythonhosted.org/packages/de/af/9526dfdd33a923f33e29a18b8f9801ee7ee4b7397e88d28192c1024c4a75/llvmlite-0.49.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dee64784201b64c13a8df62c48a4f4218858faaa65889866bb29bdc243c038", size = 58344482, upload-time = "2026-08-11T16:24:45.79Z" }, { url = "https://files.pythonhosted.org/packages/96/7f/9f5afcf6476b228d6b170408f377a0c4f91477fc1fc91f8141088b45bf46/llvmlite-0.49.0-cp313-cp313-win_amd64.whl", hash = "sha256:a1b414dc6b164738ec39dd8987cea73829057b7dd92fc6d91b52838385fc1dd2", size = 41865544, upload-time = "2026-08-11T16:24:53.962Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/16599b8c9f21802448059482eab48a9e74086dc56b901a677ba355565e64/llvmlite-0.49.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:80a84683d04516bb51da1bbeebddaf2c2f558809c93078a8f91807909ae331f8", size = 40479230, upload-time = "2026-08-11T16:25:01.513Z" }, { url = "https://files.pythonhosted.org/packages/3a/61/0b23849141a4c4e7091fcd158ebb45195896974bebca3e58fee7cad4b4f4/llvmlite-0.49.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4281a0171d66d2098adce4ba706b8c550b1b10718650f682d64cde16e84e4de5", size = 59890659, upload-time = "2026-08-11T16:25:08.733Z" }, { url = "https://files.pythonhosted.org/packages/c4/92/628692b74b31e27af9ba7e8ba651941ee4956959d5478123c453f59aad4a/llvmlite-0.49.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b095f15fb12c4d90495df5b1a3772b4732cc408398b204a787dbedd370e09c69", size = 58344479, upload-time = "2026-08-11T16:25:15.731Z" }, { url = "https://files.pythonhosted.org/packages/96/8a/412fc273521b02cbfe0b5f8ad56cc696385f6eaeecdb9e9ae6a90111d98d/llvmlite-0.49.0-cp314-cp314-win_amd64.whl", hash = "sha256:294e2f0b70aef8f92d0ae7b203e2609f08beb39437eee73de59a21669331aae9", size = 42986588, upload-time = "2026-08-11T16:25:22.534Z" }, { url = "https://files.pythonhosted.org/packages/fc/15/f47cf45c00c8b165ac3d268502dcb21d900e86f27fd338268a66ce922ab0/llvmlite-0.49.0-cp314-cp314-win_arm64.whl", hash = "sha256:95d1071023ed858b79f6971954fd7cc1f5dbcbab987718a4ccbe1411e47d0b81", size = 37441881, upload-time = "2026-08-11T16:25:28.324Z" }, { url = "https://files.pythonhosted.org/packages/e8/2e/eafd488766d1c02413cba24f7b22acb9b3ccdfd8407e98d30eb16bac4e2a/llvmlite-0.49.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:f3f2ff0aeb17d34fcce9f79b99baac441cfd3efa41b83e233ca4530a72381f72", size = 40479230, upload-time = "2026-08-11T16:25:35.125Z" }, { url = "https://files.pythonhosted.org/packages/98/07/a2c4f04e2111ccc274b4d5e3331398a9dcf6d6e5e55d6444b1ad9d6381cf/llvmlite-0.49.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5555ea1d63928481cbf7fcb1d67452b216c7e5b393a4eb7aa1401e67f2a4fc4", size = 59890658, upload-time = "2026-08-11T16:25:43.294Z" }, { url = "https://files.pythonhosted.org/packages/80/f9/7b7b50f80b4585bcd78675ff3110c256877b11df32a8cde284f851762f57/llvmlite-0.49.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32adb84fdaae28aeb86fdb6253084ee707ee157289a2e98fe3caf48a62bee82", size = 58344482, upload-time = "2026-08-11T16:25:51.527Z" }, { url = "https://files.pythonhosted.org/packages/d8/c6/32d68bfbf1d0c36888530ef6fd72864861af23dc546302b41033471a8c3a/llvmlite-0.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:be637e465010bc9c50f070468f7f1cf5385e92fee364d192dd5e6cea790ecba9", size = 42986602, upload-time = "2026-08-11T16:25:57.69Z" }, ] [[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 = "loky" version = "3.5.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/61/690a4503ede61d36cc7fa2d5fa11fe02d94f030bd82155cd8935b2694580/loky-3.5.6.tar.gz", hash = "sha256:d96935ed689aa53eeb7b329769544950fa10a52706968f5d0af3d9c33a761e77", size = 102275, upload-time = "2025-08-27T09:53:52.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/80/7f1f1bf8c2d5dfd8e9c0e1191aa355ff8b80b5619f84d6dcc2703fa7fd5a/loky-3.5.6-py3-none-any.whl", hash = "sha256:6d5300ac68cbd5084e89a6a0a187785d6a79950d461c80223d2c9a41d672b3d4", size = 56515, upload-time = "2025-08-27T09:53:51.145Z" }, ] [[package]] name = "markdown" version = "3.10.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mistune" version = "3.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] [[package]] name = "mkdocstrings" version = "1.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, ] [[package]] name = "mkdocstrings-python" version = "2.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/65/0db919b7c91bf033b42d04f15ff16a56f4edd6afb26e84d448359660ece3/mkdocstrings_python-2.0.8.tar.gz", hash = "sha256:34545b54681cf0b212aab95394d37b0f5326a93b102f6ad1df3d965c79384c9d", size = 201771, upload-time = "2026-08-31T20:16:06.698Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/b7/a5a854d4e44853f26decc0c5e0d68df052399f3da344a313da4178f00816/mkdocstrings_python-2.0.8-py3-none-any.whl", hash = "sha256:e555d9ef53b0febdeae4dd603144d4f86ec90fef672276645ea3bb61232c3d24", size = 105443, upload-time = "2026-08-31T20:16:05.25Z" }, ] [[package]] name = "mpi4py" version = "4.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/83/231445bbcf7ef10864746c244ff2d82000011449b79275642c5d4ed8c8f4/mpi4py-4.1.2.tar.gz", hash = "sha256:56860286dc45f20e8821e93cb06669e30462348bf866f685553fa4b712d58d02", size = 501709, upload-time = "2026-05-16T10:35:23.618Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/7e/1524baa98c3b4e3ddc030ec2ad5f6e5a362930317ed3e561d6da1c2e97b1/mpi4py-4.1.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ceb3b6e6f27ba7a39f7721583c04514013b860b829e721769e77398dee97bfa3", size = 1411317, upload-time = "2026-05-16T10:33:56.706Z" }, { url = "https://files.pythonhosted.org/packages/2f/73/5918e062a65f40e3bd82a2dccf0c8ba22a284d327ccda46d84d2717985cc/mpi4py-4.1.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:251e8880f4cb98e9c8f63c6f6b2c7e819e22b5e4949d47767a0092eed6f814c2", size = 1304635, upload-time = "2026-05-16T10:33:58.685Z" }, { url = "https://files.pythonhosted.org/packages/a3/0a/1da7f403e0d8ce0e26d541f7538302cec00cf5b0a98a7a52b929f938a25c/mpi4py-4.1.2-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2ef63b2e3083e6062fd90e4de8c4e3acbf81e0772406e0226eb8dde6a48cab8e", size = 1327130, upload-time = "2026-05-16T10:34:00.269Z" }, { url = "https://files.pythonhosted.org/packages/e6/f9/65999152ae82bad914c6a083821ee774afefd6d0544e633b940c9a9ebf3f/mpi4py-4.1.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6508e654b9c8ff9f611b19548b2a17d1e323b520a15168189f92221e6757b8ff", size = 1182268, upload-time = "2026-05-16T10:34:02.206Z" }, { url = "https://files.pythonhosted.org/packages/66/8f/90de8d5e0676f86f7866fd4665193252ea17764a54800d9545c7b53ac00a/mpi4py-4.1.2-cp310-abi3-win_amd64.whl", hash = "sha256:eb69f6273ad155f191850a593deebdf52aed6722979ba0693e02db5663e59699", size = 1466751, upload-time = "2026-05-16T10:34:04.278Z" }, { url = "https://files.pythonhosted.org/packages/27/ac/4b288dad1f72847fbeb9102447bf493da5ee9ce404bdf211b8c1f90bc51f/mpi4py-4.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e62b4413430f18e342a19b3ebb6aa1b256939b9e09200f624b9dad7e5c8d9df", size = 1546877, upload-time = "2026-05-16T10:34:14.954Z" }, { url = "https://files.pythonhosted.org/packages/7b/f2/5f9b7706d6596043d3ed9ba5767e6e5f7f283d5b92e36ffa9176f7df6c89/mpi4py-4.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0040edc6d1255186d4b5e8b4d72ae013cacf68f1c3509d55a5a9ebdafa4b090e", size = 1370778, upload-time = "2026-05-16T10:34:16.421Z" }, { url = "https://files.pythonhosted.org/packages/4f/6f/ce96e87eaec49e5dfb45cda0e600effda485d9909aa978cfb1526ce88b13/mpi4py-4.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de484475b80f9e1ffe98cb85e1b776eb9f42bb73ae4d79449d42953f80025bf4", size = 1333716, upload-time = "2026-05-16T10:34:18.38Z" }, { url = "https://files.pythonhosted.org/packages/6d/1c/8875166f33a9eb4a1d820bd4d0ccd986ff9ad4b33799fa23179f89ee3168/mpi4py-4.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a05ce06c564044d6a605f197ad81335af760f4294963263d953e46c38819f554", size = 1206258, upload-time = "2026-05-16T10:34:20.55Z" }, { url = "https://files.pythonhosted.org/packages/d1/55/16b70427b5c0d8f4e5174eb591e236296f22d6819868724d9706a4e3a60e/mpi4py-4.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f7069dbcd1bf2d25b9726866c882be1216fcd292ae0a2ddaf012a03aac0d0478", size = 1617875, upload-time = "2026-05-16T10:34:22.84Z" }, { url = "https://files.pythonhosted.org/packages/3e/73/d3d747f74c5aa7be6faf84aef6b234458baeeb0acb35143c288bc04133c5/mpi4py-4.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78983a9606e105af1af907878ec41627360344ea576c75afdaca4124db9d0b13", size = 1621733, upload-time = "2026-05-16T10:34:24.804Z" }, { url = "https://files.pythonhosted.org/packages/cb/e0/2d029ea1907a477126039a48aadb30113cf3df5bd3c2f27331334b281415/mpi4py-4.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4e5fdba71cf27d0d3b45e706736d6690060976345f45a6408b3594884bb0cc6e", size = 1393929, upload-time = "2026-05-16T10:34:26.585Z" }, { url = "https://files.pythonhosted.org/packages/9f/2b/1e48c4c5f9acbdca8dd28beeba9123dde140cd2ca520f8e3a3cf22faeeaa/mpi4py-4.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00f4cce8999d19f35243c3442ea22debbe3336f69c309cd5d3176df4e51c717a", size = 1358844, upload-time = "2026-05-16T10:34:28.247Z" }, { url = "https://files.pythonhosted.org/packages/94/46/a37225d47997fcf30adca25d3849d035bbb61d972118b024db900306e528/mpi4py-4.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0a58e164776acb7b52414548b1bf0e5caafce0ee90345deff147873a64b6b2cc", size = 1227206, upload-time = "2026-05-16T10:34:29.866Z" }, { url = "https://files.pythonhosted.org/packages/b5/48/14173d8baaf622b76e0e8983f53fffff8538edcf24a020028432e63f20d3/mpi4py-4.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:0a7227a53ba24102f2c4c2c1b226d028fabc473b20ceef5435f1a7ac8cbd508c", size = 1706260, upload-time = "2026-05-16T10:34:31.806Z" }, { url = "https://files.pythonhosted.org/packages/35/15/324cfa61674b8f73aa3347590df937232d898a993f3348121f9003f43b16/mpi4py-4.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:632a94e355cf1960eaae2b668d88399f53680207f5a4777e15fdaf7d96c72d09", size = 1660517, upload-time = "2026-05-16T10:34:33.836Z" }, { url = "https://files.pythonhosted.org/packages/00/ec/b670c9250b59ed255571e679f1e0ae7bd4e08c67f2f7f7f11a928b4becd7/mpi4py-4.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0aedf3e6c937b48d244e7c6bb72d0b068a1b1adf99ec6001bc9c7e381c581806", size = 1426478, upload-time = "2026-05-16T10:34:35.342Z" }, { url = "https://files.pythonhosted.org/packages/0d/5e/493415cdb0da0c1c0b6ec9a1fb65ab57a174e8111b25c87f7507f663d0b4/mpi4py-4.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:085e0cb05427398fe2281856f27a87984b9f234cd6d98a2a384a6fbfe679a56f", size = 1358571, upload-time = "2026-05-16T10:34:37.478Z" }, { url = "https://files.pythonhosted.org/packages/6f/51/3822e834fc9cafd96811501b85232e02bd9b31596d42965536a269a6c112/mpi4py-4.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:429437883b4511583d56a57bc58ad0d02965fb6c19f84f39e30c0646e6c0cab9", size = 1226562, upload-time = "2026-05-16T10:34:39.652Z" }, { url = "https://files.pythonhosted.org/packages/72/27/919831e5c843890b1a5b475ca1a8d83d8eb3453c1fe66f6bbdaca35ee548/mpi4py-4.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f984aa35d61fd95c7aa49829f94c3d61bb9ec2272bd8e404ae8d1ea84b620b58", size = 1703612, upload-time = "2026-05-16T10:34:41.133Z" }, { url = "https://files.pythonhosted.org/packages/ef/ee/3c1128b5c393a8a2abeb9db6cc2aba2d3d604fee20d0fa99c02ba5aab5c6/mpi4py-4.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:16ae4d0fac11e60056ec7c46ce101f70f4941634ad8165c54a24304820a816b0", size = 1740466, upload-time = "2026-05-16T10:34:42.566Z" }, { url = "https://files.pythonhosted.org/packages/17/ed/7a163be7da48ace29ab4c54407e7f2e510953df14dea90e122f9ca7310c7/mpi4py-4.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:364da611dda94e8a26e418ccfc4c8ff2a1492e74f4c28b24df09521fe888de3b", size = 1518426, upload-time = "2026-05-16T10:34:44.081Z" }, { url = "https://files.pythonhosted.org/packages/ec/5e/d358fadb8672d58abd6dce16c95eda56f497b378dec5642a31cd6ededfc4/mpi4py-4.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85e332feaac3323d8ed1c71f17478fe3f529468f28f6f9bb4c9133ad2d3a3a6d", size = 1419007, upload-time = "2026-05-16T10:34:45.47Z" }, { url = "https://files.pythonhosted.org/packages/27/eb/5cd53880337009cab9a9d17a007ad5aec731f3a55e211bdcc99dbb98a0e3/mpi4py-4.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e05dba75f6ad17ae761bdc01ee5c5271d15cd296e8a508e22c99c91b540dc99", size = 1298482, upload-time = "2026-05-16T10:34:46.858Z" }, { url = "https://files.pythonhosted.org/packages/13/d9/eb35b0e8328a02c4d6288bf2bcd156b387f08ff6146a5c8f3f2abd75a078/mpi4py-4.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2d0a131a255f61a2d313623f91b496c345abd5976147f866b39383264f275449", size = 1835283, upload-time = "2026-05-16T10:34:48.899Z" }, { url = "https://files.pythonhosted.org/packages/2e/0d/6ad35d0d50cb519f1c1658d754bfcf553a48611043d91d6b5e7c9d3cdca1/mpi4py-4.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0bfd4739cf1189d3d1f44b360e8fab203cbab91d762e581b32764e7dbe7e8c44", size = 1672866, upload-time = "2026-05-16T10:34:50.293Z" }, { url = "https://files.pythonhosted.org/packages/34/0b/f9bf119248f3269076a44ae4c83a06ae609e6aa11c04db7306aed011e0d9/mpi4py-4.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a8e9d25dd3b304ac2a57ab8ac4eca87f81411e535dc24ac0cd044f6f8304d9d7", size = 1440551, upload-time = "2026-05-16T10:34:51.777Z" }, { url = "https://files.pythonhosted.org/packages/26/e4/32575e9b0d5380b08ffa04958c7a13b1bab86e3674f33b6de3827fbbe14c/mpi4py-4.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d53e8f7182bf125b37155f679b301a21cd64e5ae6861ca0ab989c9d2b2073bbd", size = 1368388, upload-time = "2026-05-16T10:34:53.665Z" }, { url = "https://files.pythonhosted.org/packages/87/a8/ad5e925da9de402704ecfe8715348b068fb0b65059109c65dafb111f95e4/mpi4py-4.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cbf982d3425fd06890fbcff79ba19a7435e93bc74e5711ca8ac0b64d0c3ece9", size = 1238211, upload-time = "2026-05-16T10:34:55.094Z" }, { url = "https://files.pythonhosted.org/packages/c3/5e/3fbcbe18712fecd509eedb91dcd4947dba0618a4d2add6231b772604590b/mpi4py-4.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:50a1f5c5b210b8175860174fe670e058883df6f50e8998356dca5c73e6177d89", size = 1712936, upload-time = "2026-05-16T10:34:56.745Z" }, { url = "https://files.pythonhosted.org/packages/14/ea/6f1836e46355f46eca2bd8d2b989fb29d62acff0a6a5dc61d356e0e36e44/mpi4py-4.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5f255ed29f4fc66bd1387956e5fe207cae19e418c7db5559f5d6a77370171389", size = 1753847, upload-time = "2026-05-16T10:34:58.184Z" }, { url = "https://files.pythonhosted.org/packages/cc/b3/999f743330dec8543a2b0f804c12e28a45afc6e5546aa16ef849a67d91f1/mpi4py-4.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d2704dfd19663200677a7c98b971a385d2ed58bb9deee544dbd38fbec1a91ef1", size = 1527795, upload-time = "2026-05-16T10:34:59.965Z" }, { url = "https://files.pythonhosted.org/packages/39/29/45a41d081342896af52d38a848017728bf6834c4c92814c50aa791dc750b/mpi4py-4.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db189238c37be98933eeb078a3e2b827eef969837e8fe0f3a2e6cdb7f4b1b05f", size = 1423092, upload-time = "2026-05-16T10:35:01.672Z" }, { url = "https://files.pythonhosted.org/packages/0f/6f/c2127d426d87f2f7cdb4beacbb4febe7b0c93047a3c615d0e56cecb42563/mpi4py-4.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0fbf0bc100ed966c3d7f44231a19aef0025d523d1b813de468a1b114d0d9436", size = 1303134, upload-time = "2026-05-16T10:35:03.122Z" }, { url = "https://files.pythonhosted.org/packages/24/9c/f80cd1f9591be4c5c55a60676306b19847194cf168e0cc9db07f2e181152/mpi4py-4.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:34d94bd254947d17033aa5a9b6b1872175bad7e73ccef6b0373f2b8054e30f2e", size = 1838114, upload-time = "2026-05-16T10:35:05.306Z" }, ] [[package]] name = "mypy" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" }, { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" }, { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" }, { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" }, { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" }, { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" }, { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "narwhals" version = "2.24.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, ] [[package]] name = "nbclient" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "nbformat" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, ] [[package]] name = "nbconvert" version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "bleach", extra = ["css"] }, { name = "defusedxml" }, { name = "jinja2" }, { name = "jupyter-core" }, { name = "jupyterlab-pygments" }, { name = "markupsafe" }, { name = "mistune" }, { name = "nbclient" }, { name = "nbformat" }, { name = "packaging" }, { name = "pandocfilters" }, { name = "pygments" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, ] [[package]] name = "nbformat" version = "5.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastjsonschema" }, { name = "jsonschema" }, { name = "jupyter-core" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/80f407a9525bc5bd9865e5c37db3b78867fa43217f8aac5eab22b5f028b3/nbformat-5.11.0.tar.gz", hash = "sha256:7dbaed4a69cae28c2b4d44ab7430a6af4544fb89455023f6f21550be757b60c8", size = 151822, upload-time = "2026-08-06T12:29:55.597Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/4a/0eece9dad5e73230ca972f7bc29456ce7d74772f123d401fcf67379008f7/nbformat-5.11.0-py3-none-any.whl", hash = "sha256:f70a17f591a9ccd1c601d5e61a4b20972703926df0ba42458ce14bf575766bb6", size = 79820, upload-time = "2026-08-06T12:29:54.178Z" }, ] [[package]] name = "nest-asyncio2" version = "1.7.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, ] [[package]] name = "notebook" version = "7.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-builder" }, { name = "jupyter-server" }, { name = "jupyterlab" }, { name = "jupyterlab-server" }, { name = "notebook-shim" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/d9/5c76de84e96e1cf8aae3fce930875e0615e14f3557bbcdb634aab52da9d3/notebook-7.6.2.tar.gz", hash = "sha256:cc02b5f0bb972160cccfe44ad8a1a202036206ba3439469c514f03aefa9ae807", size = 5500147, upload-time = "2026-08-11T13:21:16.632Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/fd/3e552ff5b24dd305c6e9a10e8645e29c03091c317429f326ae10dbae1ac6/notebook-7.6.2-py3-none-any.whl", hash = "sha256:5fe9e09c335cb4b7de21627b860f77210e70e54b1fb1276ad942a4a7e1d858d3", size = 5547102, upload-time = "2026-08-11T13:21:13.302Z" }, ] [[package]] name = "notebook-shim" version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] [[package]] name = "nox" version = "2026.8.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, { name = "attrs" }, { name = "colorlog" }, { name = "dependency-groups" }, { name = "humanize" }, { name = "packaging" }, { name = "platformdirs" }, { name = "python-discovery" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/78/74026b59fd3becd3a549498e8f7809e768c9a1db808f6f016f88b638eda6/nox-2026.8.10.tar.gz", hash = "sha256:13f45e46552eade9f4b60474133e31e1e0bb341234a3cc0c9d6fff17570ce8bb", size = 4075448, upload-time = "2026-08-10T21:53:18.045Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/6a/020c992d7121377b27676af368d5d5287121a9bb12216264d979961e90d8/nox-2026.8.10-py3-none-any.whl", hash = "sha256:943f1ebcace1efb35d91ad2034007df1942d2a1bed75fb33e1bf0ed5a0df1cdb", size = 94662, upload-time = "2026-08-10T21:53:16.477Z" }, ] [[package]] name = "numba" version = "0.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8c/ed/55ba4e54ee878396de6b18e6533cc4a92fa519e8c82d55cf40f98c0a6831/numba-0.67.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3fa3d1b27f96f2c0d54513d953d7197886aa1eaa7d2439a0eedc44d993fb181a", size = 2744821, upload-time = "2026-08-11T23:03:17.321Z" }, { url = "https://files.pythonhosted.org/packages/be/78/3f3c45dbaec3cf02bbb1825731beca50f591227e95143d6bd7a64897641c/numba-0.67.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c80c847301dc33dc8f84a97a952004023d9a05578ae4512b087176264cc1960", size = 3827182, upload-time = "2026-08-11T23:03:19.684Z" }, { url = "https://files.pythonhosted.org/packages/a5/24/4e70cb86534283d859c3aea2302da523e41539b98dd6c3c4d0a42af95cda/numba-0.67.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7a7b0121466f1e9a8a074b0545fe90e16389623abf979b5d7c299dca1294d7e", size = 3532817, upload-time = "2026-08-11T23:03:22.06Z" }, { url = "https://files.pythonhosted.org/packages/26/4d/23dab7f4233be0fc34f54a169ed85238467cd24d8adf2498e5c12ea19dc7/numba-0.67.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfba1ac34f0363fb1a250a10e97240780d11e05227892f7286b26fbfd0ad58ce", size = 2815700, upload-time = "2026-08-11T23:03:23.812Z" }, { url = "https://files.pythonhosted.org/packages/0d/58/915cddba90010348ed0444451132fdde9b000bcbaff1582029b5bf115d11/numba-0.67.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6004d8d5f28d4028687fb2d972d629295b13685943bd2ed5cd8810c3b848e219", size = 2745050, upload-time = "2026-08-11T23:03:25.607Z" }, { url = "https://files.pythonhosted.org/packages/bb/38/926757caaac18a66f057d7544a63620bf360a07d281c9f7ecadd2aa83963/numba-0.67.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f63d43db06b4756424d6d2484737c902e0ae944a0eec3e8b0b4de2c695b15caa", size = 3884596, upload-time = "2026-08-11T23:03:27.688Z" }, { url = "https://files.pythonhosted.org/packages/4d/6d/58291dc58da39d98b32db7f044729f6d8d4920cd9622fbab3179b54ff4c4/numba-0.67.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76d3335aaeffb9dc88309420890e73497a00be08a7530441bc2b58ffe025bfa5", size = 3585290, upload-time = "2026-08-11T23:03:29.684Z" }, { url = "https://files.pythonhosted.org/packages/6e/63/ab21828b4056afed71f9ecb40f4de26c2c19de731cc001961aca74b79464/numba-0.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:50e2b72406c18cda5dd7431b0082cb85ea94e06c64c33607248fc8bef92cfb81", size = 2815645, upload-time = "2026-08-11T23:03:31.732Z" }, { url = "https://files.pythonhosted.org/packages/49/dd/bd9fe772f6c84597b76cac229b3f2890f01a2c64fd70e48ceaae10dd65cb/numba-0.67.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:77e1c7173fee57a0d84e006c7e70346689d6cb3e7db503489bae58646b4eff7b", size = 2744872, upload-time = "2026-08-11T23:03:33.649Z" }, { url = "https://files.pythonhosted.org/packages/a1/1c/c05609739cc41116d36e30cb2b41fb00f126bb52e1b0bac907051ad8a35d/numba-0.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4953387c77864b596d8296e2cfbdef82b0eea4166ab4864b05d226c51143e0", size = 3892004, upload-time = "2026-08-11T23:03:35.797Z" }, { url = "https://files.pythonhosted.org/packages/4a/77/a5276ad4178250403e0e2251f3e1f8ac18feac779b0474a8bcb08558490d/numba-0.67.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f6e0f5cb6c545e158b6ef0496c01b6d6958a7ccc6634a1576a94bbbab29ff2", size = 3591878, upload-time = "2026-08-11T23:03:37.845Z" }, { url = "https://files.pythonhosted.org/packages/0f/80/d48f0ba7442516ceb5a1585f0c81d3aa531bc96bfcabcd9f8f925768c426/numba-0.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:b68ad5125fe245339cc8dcc036081fc1ea482c5063387b9612a76ccd83dc91cd", size = 2815504, upload-time = "2026-08-11T23:03:39.736Z" }, { url = "https://files.pythonhosted.org/packages/d7/16/345b1e4774a08247aafcfdb93d4e8d24a3646366cbe72de33053fc0de1b5/numba-0.67.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f99f880ff25f418a67f9a1d00d0ddfbc63430f627b523e515085a592a7567f4b", size = 2745088, upload-time = "2026-08-11T23:03:41.864Z" }, { url = "https://files.pythonhosted.org/packages/1b/36/e614ba2bc0f005ed0f37a6413f08fe705210297ddb9a37a475a8b9fdab61/numba-0.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269245a675abdd3e2c35ec6bb2f250355effa9032514d8f2354f0d2d10854bd", size = 3861040, upload-time = "2026-08-11T23:03:43.842Z" }, { url = "https://files.pythonhosted.org/packages/40/90/30c42a1dbc4176cf355e8e8be61803732c55597b1332925fe233912a43d9/numba-0.67.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f074a8e23db78490f11a3930c940be758316c10ac5985be83d2f298dc080acf7", size = 3561811, upload-time = "2026-08-11T23:03:46.037Z" }, { url = "https://files.pythonhosted.org/packages/18/6d/21bd16f770476e394c5e5f504935817967442a71251d6b86c244a2767980/numba-0.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4d576e62bf2c9370f61312b51573c4bb1f3fe96798bbab56730847a368a316c4", size = 2817421, upload-time = "2026-08-11T23:03:47.922Z" }, { url = "https://files.pythonhosted.org/packages/95/06/bb41b0e59b9ff52c94a2f01db24f6437df058caebb377b5f372fc343a6a2/numba-0.67.0-cp314-cp314-win_arm64.whl", hash = "sha256:7930748ce8355d2a5a28602abab056a61fdc676d17377f27d17993905428171f", size = 2788885, upload-time = "2026-08-11T23:03:49.967Z" }, { url = "https://files.pythonhosted.org/packages/10/7c/aa07151fbd0f4283f78de437cc196f9084789be89a2b4de3fdc2f6a4b414/numba-0.67.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:4a2ed006635bbd0fe45681ed49f3b4f4bad1abf0c233bcc5842c9e3a34cabd61", size = 2748150, upload-time = "2026-08-11T23:03:51.755Z" }, { url = "https://files.pythonhosted.org/packages/74/62/b8174ca95a4cc1a7ba1520767734e016991545590b8fbde521b681701a9f/numba-0.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa5f002f665bec321b950dacaa26ee009e1d720f6ac9d9856eed5efe1caa03a6", size = 3896986, upload-time = "2026-08-11T23:03:53.752Z" }, { url = "https://files.pythonhosted.org/packages/8c/f9/3a7b6dbf81e01a48958b45ad2239edbc64707522ab17f11f9f18c44bf6d1/numba-0.67.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83ab968b0e0fa744eba03351282dd8000796e6ec8e4518f47bd3ed86c0a20c7b", size = 3614644, upload-time = "2026-08-11T23:03:55.794Z" }, { url = "https://files.pythonhosted.org/packages/d9/5b/248f5681c121ca853a9f4e39d342a3e01b8a0261b0275853eb3d0d56aa20/numba-0.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00c964a5b94d3ae82d83ac162cd610755875b98dadb779fdde06e6bfcdbca47e", size = 2822870, upload-time = "2026-08-11T23:03:58.097Z" }, ] [[package]] name = "numpy" version = "2.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] [[package]] name = "orjson" version = "3.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, ] [[package]] name = "packaging" version = "26.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pandas" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] name = "pandocfilters" version = "1.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, ] [[package]] name = "parso" version = "0.8.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathspec" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "pillow" version = "12.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] name = "platformdirs" version = "4.11.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, ] [[package]] name = "plotly" version = "6.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/07/795c79dbce40c39bece88e69d049babbd23ffa95b5d117f248db8ea03abb/plotly-6.9.0.tar.gz", hash = "sha256:967ad33e8c704fed051800d11d985eb206a9c795c14206b30a6f463ed9c67d0d", size = 6919903, upload-time = "2026-07-09T14:55:59.982Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/18/d8544811ab076f876c4892b3714f5b0dad335e1dc33aef826df431b8325d/plotly-6.9.0-py3-none-any.whl", hash = "sha256:36bebe2f1bb13884774fe61689c329071446f6ce4a8927fb1f0d6fb24f581236", size = 9909646, upload-time = "2026-07-09T14:55:55.421Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "prometheus-client" version = "0.26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pymdown-extensions" version = "11.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "pytest-markdown-docs" version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/c7/6953e9e25d90d038600e65b05265693aef7ae2a2b67e0f170d65f4f16c24/pytest_markdown_docs-0.9.2.tar.gz", hash = "sha256:bf7510fdeec90d3e98ac924561c01de8dbaf4c3ad34f2418ed2b0b12e5a3f4f5", size = 10929, upload-time = "2026-03-23T12:35:04.804Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/b9/c3df11997d29e69b3f8edae1e903bf44eaf4774ccf4c5b6ddcebde88931c/pytest_markdown_docs-0.9.2-py3-none-any.whl", hash = "sha256:9c05a5bee48214cb36583d4a5131d3a23b327a8d82c92ae860106053fd7d1f9e", size = 13536, upload-time = "2026-03-23T12:35:03.862Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] 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 = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-discovery" version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/b7/ac44da2cf0e53ada0e419033c2d058219c95dc1403126f163304c9e814b1/python_discovery-1.5.2.tar.gz", hash = "sha256:45fd4f20a4e3f9b7bf2e0817870bc8e3b320a19658da177af800768c82dbf354", size = 82350, upload-time = "2026-08-12T14:05:26.419Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/45/689603d04b3bb8d7faa00f25c24acef993aab7813b3dbbfc472a459ab0b5/python_discovery-1.5.2-py3-none-any.whl", hash = "sha256:3e338c2d0f15dfaeea57493f4c2c6caebe0e998ea815c30ae8bf8ee21f1112d3", size = 38350, upload-time = "2026-08-12T14:05:25.113Z" }, ] [[package]] name = "python-json-logger" version = "4.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/25/5473e46b179f8e8b4ad3aeeb36773d1701b7770eaf5e5bc2025c7303b598/python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa", size = 18211, upload-time = "2026-08-15T11:36:38.232Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/55/6467fde553886cb293e41538f3a8b4e4fd4688c6df242cf982162d8367fb/python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9", size = 14988, upload-time = "2026-08-15T11:36:36.821Z" }, ] [[package]] name = "pywinpty" version = "3.0.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b0/5c/31feb3dd82d1b33ae0bd09ca601edb993d9da1b7f0226b3336d4b4c39e1e/pywinpty-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:af7a8720c78776ddd6259b71dd567944f766a6cd67f8d2887fbc4973967bacda", size = 2092466, upload-time = "2026-06-10T23:44:24.453Z" }, { url = "https://files.pythonhosted.org/packages/ee/fe/fe23e2229ffec0c10190cef5964f5c9b2dba179d23b69ae537b7ea90bcab/pywinpty-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:c2406f54f699eab75953fb75ce805f2ae55a33a957cd070890abd454fb4b7680", size = 818395, upload-time = "2026-06-10T23:41:56.93Z" }, { url = "https://files.pythonhosted.org/packages/45/34/942cc95ca4e26489875aa8a95192766247a687379ec29543eebe73ec945f/pywinpty-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:d62946adf14b15b54c0b8d785f93fe18b04da23f4ad59e2e8c4612646e9abd23", size = 2090915, upload-time = "2026-06-10T23:43:14.98Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/5b9053004844139ea8bd86209c57ade12b134b2782f383a095784c8531ec/pywinpty-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:e9391c05fbfa7a992a97e831fc6849887b4014a614192e3d984a7ca59592b376", size = 815934, upload-time = "2026-06-10T23:41:42.384Z" }, { url = "https://files.pythonhosted.org/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471, upload-time = "2026-06-10T23:42:11.071Z" }, { url = "https://files.pythonhosted.org/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518, upload-time = "2026-06-10T23:42:02.363Z" }, { url = "https://files.pythonhosted.org/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920, upload-time = "2026-06-10T23:44:13.395Z" }, { url = "https://files.pythonhosted.org/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520, upload-time = "2026-06-10T23:43:28.588Z" }, { url = "https://files.pythonhosted.org/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663, upload-time = "2026-06-10T23:43:59.845Z" }, { url = "https://files.pythonhosted.org/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700, upload-time = "2026-06-10T23:40:50.001Z" }, { url = "https://files.pythonhosted.org/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093, upload-time = "2026-06-10T23:40:58.933Z" }, { url = "https://files.pythonhosted.org/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517, upload-time = "2026-06-10T23:42:34.946Z" }, ] [[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "rfc3339-validator" version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] [[package]] name = "rfc3986-validator" version = "0.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, ] [[package]] name = "rfc3987-syntax" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lark" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] [[package]] name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rpds-py" version = "2026.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] name = "ruff" version = "0.16.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] name = "scinexus" source = { editable = "." } dependencies = [ { name = "charset-normalizer" }, { name = "citeable" }, { name = "scitrack" }, { name = "tqdm" }, { name = "typeguard" }, ] [package.optional-dependencies] loky = [ { name = "loky" }, ] mpi = [ { name = "mpi4py" }, ] rich = [ { name = "rich" }, ] [package.dev-dependencies] dev = [ { name = "cogent3" }, { name = "mypy" }, { name = "nox" }, { name = "numpy" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "rich" }, { name = "ruff" }, { name = "scriv" }, ] doc = [ { name = "click" }, { name = "cogapp" }, { name = "cogent3", extra = ["extra"] }, { name = "mkdocstrings-python" }, { name = "pymdown-extensions" }, { name = "pytest-markdown-docs" }, { name = "zensical" }, ] [package.metadata] requires-dist = [ { name = "charset-normalizer" }, { name = "citeable" }, { name = "loky", marker = "extra == 'loky'" }, { name = "mpi4py", marker = "extra == 'mpi'" }, { name = "rich", marker = "extra == 'rich'" }, { name = "scitrack" }, { name = "tqdm" }, { name = "typeguard" }, ] provides-extras = ["loky", "mpi", "rich"] [package.metadata.requires-dev] dev = [ { name = "cogent3", specifier = ">=2026.4.20a0" }, { name = "mypy" }, { name = "nox" }, { name = "numpy" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "rich" }, { name = "ruff" }, { name = "scriv" }, ] doc = [ { name = "click", specifier = ">=8.3.2" }, { name = "cogapp", specifier = ">=3.6.0" }, { name = "cogent3", extras = ["extra"], specifier = ">=2026.4.20a0" }, { name = "mkdocstrings-python", specifier = ">=2.0.3" }, { name = "pymdown-extensions", specifier = ">=10.21.2" }, { name = "pytest-markdown-docs" }, { name = "zensical", specifier = ">=0.0.59" }, ] [[package]] name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version < '3.12' and sys_platform == 'emscripten'", "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] [[package]] name = "scipy" version = "1.18.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, ] [[package]] name = "scitrack" version = "2026.8.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/02/61/631d9322aa824f7733ab7ce3d269fc1aab72be42d7755849a2f4769926ca/scitrack-2026.8.5.tar.gz", hash = "sha256:1eb6fba03107021d43cb8890bc13f4be1a7e342cdc695c9f5e1dca95b1e4f27f", size = 13779, upload-time = "2026-08-04T23:48:26.169Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/e7/a212ae6fb7827ac45c163706dcc90d923d87b833b7afb8ba2efeef2ef12e/scitrack-2026.8.5-py3-none-any.whl", hash = "sha256:264cc1326995f00095e4236fa5be5d556754c64e70ffb2b1cf1b62936f6f64f2", size = 14003, upload-time = "2026-08-04T23:48:25.127Z" }, ] [[package]] name = "scriv" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "click" }, { name = "click-log" }, { name = "jinja2" }, { name = "markdown-it-py" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, ] [[package]] name = "send2trash" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] [[package]] name = "simplejson" version = "4.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0e/2a/54837395a3487c725669428d513293612a48d82b95a0642c936932e5d898/simplejson-4.1.1.tar.gz", hash = "sha256:c08eb9f7a90f77ae470e19a07472e9a79ebc0d1c2315d86a72767665bd5ba79f", size = 118860, upload-time = "2026-04-24T19:24:59.819Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/25/39013ffe279d90093ec1c848565b3683c586906c10fa55d9000ec29d046b/simplejson-4.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2867c64d92abd1992c15666fae198203093f593e43d6b81adf176bae530d493a", size = 111538, upload-time = "2026-04-24T19:22:49.051Z" }, { url = "https://files.pythonhosted.org/packages/f2/ae/2c272971c8a87e2539c54a98eb6ff037bee1e2e93943c3986cf7500a4f3a/simplejson-4.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c47c46e16c8ea9e4850061e6ed5aa2b9cd2074cb2274bfd9c138cba15ce7453", size = 90594, upload-time = "2026-04-24T19:22:50.408Z" }, { url = "https://files.pythonhosted.org/packages/4e/a2/6eebfb99dedc139f549200f61ade6d1890ac5707c5d427bdfa6fe39c9313/simplejson-4.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e294e33dbf316a9bbdd4030d46503c9b0f19470ae7ad6af5bae6c426bc2e869f", size = 90718, upload-time = "2026-04-24T19:22:51.694Z" }, { url = "https://files.pythonhosted.org/packages/80/7e/c9e6c0c4ad8415e64dad0c47f619b556b02680a41631b4dbc281d55dc54d/simplejson-4.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ce252b28fddbdd83db5bd7d93dad2a8a591d7ada098afec9c1b23d6b722a7a4", size = 180901, upload-time = "2026-04-24T19:22:53.025Z" }, { url = "https://files.pythonhosted.org/packages/34/09/69e331e3994b1ed9be6ce9ace4ade704e7ed503edf869929ca7bb404eda8/simplejson-4.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c44ef6b02a4eb67ed17a72342341792149b3ff46f15426c26e970e49addf327", size = 178133, upload-time = "2026-04-24T19:22:54.574Z" }, { url = "https://files.pythonhosted.org/packages/5d/40/ed806f24afef295c1032448f5ff6f6f2979392d5645ddb9f4fed7f38194d/simplejson-4.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82bfca2b85a34178c25829c703f0a9e9f113a5af7539285bd3efb583a0bf1ba3", size = 188155, upload-time = "2026-04-24T19:22:56.044Z" }, { url = "https://files.pythonhosted.org/packages/38/94/8d6f515b827b0f7881a49c8c1ac6920b7ae9428939ef04238c973278b42a/simplejson-4.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e4b23f71dd781f8830f1663dc01a4944d3dbf87a1f93d78fba1cf64722d0ccf", size = 176225, upload-time = "2026-04-24T19:22:57.981Z" }, { url = "https://files.pythonhosted.org/packages/c9/fd/6dffb4956563d48bbe46b91ff341adae34920e94008fd6b8d728072abfc7/simplejson-4.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:82fee635d7b73ad801030b05a75fbd34a098da0c2ecf600667a03636d09e1e42", size = 185535, upload-time = "2026-04-24T19:22:59.618Z" }, { url = "https://files.pythonhosted.org/packages/de/d2/a509ee37763e79aec75d68f8521db1440306edeba3b8b4064ab4ee8bf1d9/simplejson-4.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68e62eda21192c5ea9bb92d571ca46a4477fef48762f50d433de2b4253051551", size = 179302, upload-time = "2026-04-24T19:23:01.324Z" }, { url = "https://files.pythonhosted.org/packages/d8/23/5b343bfd2a79d3b6818e4db3586c405a001a090d4c89d336e31273ce7177/simplejson-4.1.1-cp311-cp311-win32.whl", hash = "sha256:ffd3d82294b47f5ec64050021ace95fd62628a0c1cc8bbf4d06d2d1fb697e055", size = 88408, upload-time = "2026-04-24T19:23:02.808Z" }, { url = "https://files.pythonhosted.org/packages/38/04/df9b37aedbd524dca20840d25ebe01d6ae486b89792aeff5d15b9c4114f7/simplejson-4.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:78a3fe0995be42bed62a26aa78e0e0b4d87c6545785346b9cc898f3389569a35", size = 90526, upload-time = "2026-04-24T19:23:04.408Z" }, { url = "https://files.pythonhosted.org/packages/60/25/e90998fe8e480eb43b966c09e835379887d427567ebd496563d3b1e16b19/simplejson-4.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:19040a17154dc03d289bab68d73ce0a6a0be01de30c584bbdd93490bead14b22", size = 112414, upload-time = "2026-04-24T19:23:06.084Z" }, { url = "https://files.pythonhosted.org/packages/9c/a0/abd4785f36c3400f1fbb21f517be39295a750a714f04b7ee175adf6ef580/simplejson-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a94ebaecdbaa80d9551a3ec6bf0c9302fc8b53ab6c1b2bfd498a1df4cb28158d", size = 91120, upload-time = "2026-04-24T19:23:07.877Z" }, { url = "https://files.pythonhosted.org/packages/b8/78/fc060d2e3b13c6ec59288574b8efac64075e316b2afba4396a56b2422f78/simplejson-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67341c95c0a168ab4a6d1e807e50463f1c8da932c3286d81e201266c427061fa", size = 91055, upload-time = "2026-04-24T19:23:09.264Z" }, { url = "https://files.pythonhosted.org/packages/0c/b6/156a8de1e1b47694f0e7de6675866936608d45dc68388fd017d36f8693be/simplejson-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45ec18e337fec538b7e902d489505c450b2454653d1290f3f50385e6fd8aa607", size = 190297, upload-time = "2026-04-24T19:23:11.226Z" }, { url = "https://files.pythonhosted.org/packages/86/1c/e4d0eab695be3eb21d0f46bce820752031f03e7113f9c80a9b3c73ee7157/simplejson-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:820c69a4710400e9b248d5670647d60be58824369282d3925e516b3ff1a7cd82", size = 187002, upload-time = "2026-04-24T19:23:12.982Z" }, { url = "https://files.pythonhosted.org/packages/76/0e/7f5a59d29426b062d5928fb88b403c3f797129d53be7102f955dbe51aa44/simplejson-4.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e708d373a10e4378ef2d59f8361850c7150fd907ed49efe49bc5492160476d1", size = 195146, upload-time = "2026-04-24T19:23:14.517Z" }, { url = "https://files.pythonhosted.org/packages/78/18/9943db224dd4d5fa3c090c3e56a94c37b254338c83995ec5680285111c40/simplejson-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:980fc33353f81fd12d8c49d44f8c2760d1dc8192285e627c5180d141035b228a", size = 183931, upload-time = "2026-04-24T19:23:16.742Z" }, { url = "https://files.pythonhosted.org/packages/c2/08/9a690da9a766161c06c627d805362cf159f1abe480969372b2897649b955/simplejson-4.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de2ed102fff88dacf543699f53ee3a533cc11539a39baa176b7e09dd783069d6", size = 192228, upload-time = "2026-04-24T19:23:18.33Z" }, { url = "https://files.pythonhosted.org/packages/05/88/bd8aad36b451ffb0e0a3f721d695a88befa6d1ac7d1e02ae788ca7ff4029/simplejson-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785ff8edc0e28bf773a32543a6bbed46351453c997b3f6709c744e3c2f7eabb", size = 187808, upload-time = "2026-04-24T19:23:21.165Z" }, { url = "https://files.pythonhosted.org/packages/04/ee/14f91db0d1f481533b651dafbf8cd0da088d9817f7af30c68f7f19f9c847/simplejson-4.1.1-cp312-cp312-win32.whl", hash = "sha256:2e0d5ead6d14610467ec356ec1f6b5d8a56aa216abaad8d41c8b873b16cf313f", size = 88512, upload-time = "2026-04-24T19:23:22.764Z" }, { url = "https://files.pythonhosted.org/packages/b9/c4/90de06b2d8737c68c05ff9274113f854dbf6a5f28b7a955212111672cb57/simplejson-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63a5451f557d6be48a231bae932458655c620902b868170b2f1c8afed496f6b4", size = 90748, upload-time = "2026-04-24T19:23:24.494Z" }, { url = "https://files.pythonhosted.org/packages/37/a9/47b445eeb559c9593453a0648e0fd6d08e8adff64dd5e5ced66726da8a09/simplejson-4.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dff52fc7af272e84fc21cc5a06c927c823ca6ae00af14f3b0d7707b42775ed98", size = 113160, upload-time = "2026-04-24T19:23:26.033Z" }, { url = "https://files.pythonhosted.org/packages/4c/65/cb72db31523c164dea5dc55b02dad065a40c478856bc7534b279d2b51906/simplejson-4.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971aed0647ad6e840a3943bec812fcda5f2d26a5497a4981d1fb49aa4f9a396c", size = 91521, upload-time = "2026-04-24T19:23:27.572Z" }, { url = "https://files.pythonhosted.org/packages/9a/e5/54cb7c50ad5fdc1e0a86b7df4b135c2cbd5c4623605aa94466659098e8da/simplejson-4.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:249e2e220aa6d9b9d936bde84eb7bf79d5b6c5a8273c6e411f8b1635a9073f2d", size = 91407, upload-time = "2026-04-24T19:23:28.991Z" }, { url = "https://files.pythonhosted.org/packages/38/2e/21a3ede87f0bf82d6c7bcb90480d50a6490eb974c6ab20881188e440957c/simplejson-4.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e5cdd6a5d52299f345c15ab5678cc4249e24f383f361d986afbc3c7072a6b6b", size = 192451, upload-time = "2026-04-24T19:23:30.56Z" }, { url = "https://files.pythonhosted.org/packages/59/df/9903edd3102bf0b5984edfcb90c88612330996efa3b4fbf8a971d6e17839/simplejson-4.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642cec364e0676e2d5a73fa4d31d0c7c55886997caa2fde24e8292ca44d32728", size = 189015, upload-time = "2026-04-24T19:23:32.647Z" }, { url = "https://files.pythonhosted.org/packages/98/cd/33230927a780e1398b857e3944abb914556994d252b1d765ae40d112cb25/simplejson-4.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:76fe296ca1df23d290033f10aaacf534fd1b3e3007e7f9ff8aa68b21413aaa78", size = 196658, upload-time = "2026-04-24T19:23:34.563Z" }, { url = "https://files.pythonhosted.org/packages/cd/84/2c5a7444eb53e9a86d3738299bffddd9f53aeed799ded2f45368221fdb19/simplejson-4.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f0ad25b7dc4e0fb23858355819f2e994f1a5badcdcde8737eac7921c2f1ed2a", size = 185967, upload-time = "2026-04-24T19:23:36.191Z" }, { url = "https://files.pythonhosted.org/packages/d3/68/454378e06d059cd412a7ed5d87fb6d29fd5b60f13a4d89fc1f764ff434df/simplejson-4.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a59ebd0533f03fd06ff0c42ba0f02d93cbcdd7944922bf3b93911327a95b901f", size = 193940, upload-time = "2026-04-24T19:23:38.151Z" }, { url = "https://files.pythonhosted.org/packages/d5/d5/a15bf915f623a2c5a079d6e3be8256fdb8ef06f110669493a09b9d6933e0/simplejson-4.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bccbf4419676b517939852e5aeff2af6aee4dc046881c67a1581fa6f1cb01abd", size = 189795, upload-time = "2026-04-24T19:23:40.139Z" }, { url = "https://files.pythonhosted.org/packages/d2/c9/37212ae7dc4b607f0978c408e8633f05c810884e054c33113184c6c2c8a2/simplejson-4.1.1-cp313-cp313-win32.whl", hash = "sha256:6c845363eb5fd166fb7c72243da38f4fcfde666ede7fdf2cc6fd7762894626f7", size = 88773, upload-time = "2026-04-24T19:23:41.754Z" }, { url = "https://files.pythonhosted.org/packages/fe/a5/c7a0a47883a9015b54c9d8a4b62f2aba17bd4335b1787b9b8a0fc2fa6d52/simplejson-4.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:104d8324c34f25b4b90800bc5fa363780cbc3d8496aef061cba7ce1af9162270", size = 90888, upload-time = "2026-04-24T19:23:43.11Z" }, { url = "https://files.pythonhosted.org/packages/d3/18/4a118a6a92eb33bb08c8e2fe7ec85cb96f0673491bb2b829930831ee4fbe/simplejson-4.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ed7473602b6625de793b6acba49aa949f144a475f538792067e4cf2fda2071f5", size = 110492, upload-time = "2026-04-24T19:23:44.957Z" }, { url = "https://files.pythonhosted.org/packages/07/f4/84d160e9fa8cada1e0a9381cae4fa81eecd573577a5b34366d8ced59bdf7/simplejson-4.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:225c9caa324c5b554d009fb9cac22aee7711e71bd96f487938c659af467e828e", size = 90152, upload-time = "2026-04-24T19:23:46.355Z" }, { url = "https://files.pythonhosted.org/packages/68/31/9a5432c433a7671107182cdc9a20ea78a70f99c4e5334aa54b6d4d0d79ed/simplejson-4.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:95407269340c7f22f09776ea7b717a52cf56cfcf119b5e45f66faa4a26445bea", size = 90115, upload-time = "2026-04-24T19:23:47.743Z" }, { url = "https://files.pythonhosted.org/packages/78/91/3635cdb13318cb0a328abaa69e2b91251caad39d6779aa308098f341f6cb/simplejson-4.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3851658d642c1184d2023f0e6c9ce44a21eb1629e74e7c84ef956b128841fe12", size = 184036, upload-time = "2026-04-24T19:23:49.472Z" }, { url = "https://files.pythonhosted.org/packages/fa/ba/149b6ec5393f6849d98c59cadba888b710a8ef4b805ab91e11a566960d40/simplejson-4.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95a3bb0f78e85f4937f99092239f2011ce06f0f2d803df5c299cc05abbeae008", size = 180543, upload-time = "2026-04-24T19:23:51.023Z" }, { url = "https://files.pythonhosted.org/packages/df/7c/a5d968d0b527a748b667e62bea94309ccbcb1e2b108e8f0cf8547efaa12b/simplejson-4.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbfdaa7c0603f75b7b14b211b7f2be44696d4e26833ad2d91d5c87bf5fb9a920", size = 188725, upload-time = "2026-04-24T19:23:52.995Z" }, { url = "https://files.pythonhosted.org/packages/db/e3/6a8d11181d587ef00e2db9112357e6832111e56dd56b01b5c11758a1965d/simplejson-4.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e3c584071dced8c21b4689f0254303521daeb9b5bc1f4289755d71fa3cb0d3", size = 177492, upload-time = "2026-04-24T19:23:54.581Z" }, { url = "https://files.pythonhosted.org/packages/67/e3/8b0eb8b06e8198cfbd1270487da163d0093df05cc4f557350cd65e2f7e79/simplejson-4.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:036a27bd0469b9d79557cbddb392969f876cd7f278cfbd0fba81534927a06575", size = 185281, upload-time = "2026-04-24T19:23:56.13Z" }, { url = "https://files.pythonhosted.org/packages/dc/5f/64990f07ec9e2cb1a814c674e2e21b5693207f74ac70eb72151b847ea4e6/simplejson-4.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b70bfd2f67f3351baba08aa3ae9233c83f21fd95ae5e6b3d0ecb8c647929112f", size = 181848, upload-time = "2026-04-24T19:23:57.92Z" }, { url = "https://files.pythonhosted.org/packages/61/a5/bbc1bc0447f339f79f99ab8c37f7f037cb2f1f93af75d6a4d553096bb0c3/simplejson-4.1.1-cp314-cp314-win32.whl", hash = "sha256:37233c72ce88d06acb92747347742b3c07871eba6789f060c179c9302dde8efe", size = 88761, upload-time = "2026-04-24T19:23:59.397Z" }, { url = "https://files.pythonhosted.org/packages/18/72/ec1b5cbdcb140c132e6c7bdf99bd73e4f675439e77126c88f472fcffa09c/simplejson-4.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:cc0442dea71cd9cbf30a0b8b9929ab5aa6c02c0443a3d977351e6ec5bada4388", size = 91018, upload-time = "2026-04-24T19:24:00.85Z" }, { url = "https://files.pythonhosted.org/packages/3d/97/4fa437f68ff72219bac3bf3d050de9c6265691f3a170e16954bd69d7cddd/simplejson-4.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c996a4d38290c515af347740659ce095b425449c164a5c9fa3977caa6eff5dbe", size = 113919, upload-time = "2026-04-24T19:24:02.287Z" }, { url = "https://files.pythonhosted.org/packages/c2/83/59de041d09eb4a9577f7015d7263c32095dfb7fde49717dff62145d89809/simplejson-4.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c65c763fb20d7ca113c1c14dce2fc04a0fc3a57aceff533d6fdac707c7bffb40", size = 91904, upload-time = "2026-04-24T19:24:03.812Z" }, { url = "https://files.pythonhosted.org/packages/03/8e/46bb345d540f6eb31427d984a4e518cdb182d0621814fee4fee045e8815b/simplejson-4.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0da5c9f57206ee7ef280ff7f1d924937b0a64f9a271a5ef371a2ecdbebba7421", size = 91752, upload-time = "2026-04-24T19:24:05.622Z" }, { url = "https://files.pythonhosted.org/packages/83/e2/1b2ce97f068835eb3d253c116a4df7a3f436b7bf2fb5ff1ba29287e8b0ec/simplejson-4.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ea3426e786425d10e9e82f8a6eda74a7d6eb10d99165ac3d0d3bbcb65c0ea343", size = 214021, upload-time = "2026-04-24T19:24:07.447Z" }, { url = "https://files.pythonhosted.org/packages/48/70/d93e556df6a0786298644a7c08304fcbeddc248325f23f38acbebeb21165/simplejson-4.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d75cea7a1025edd7e439b2966b3d977c45b5b899e2adaf422811b3ac702ed9fb", size = 213530, upload-time = "2026-04-24T19:24:09.289Z" }, { url = "https://files.pythonhosted.org/packages/1b/a5/c93bf305b9f00d7259e09e713d60e75bd0f7f53da970f716ab90491770e7/simplejson-4.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63c2ada8e58f266491f19eed2eeeb7c25c6141e52f8f9e820f6bb94156cf8dbc", size = 218282, upload-time = "2026-04-24T19:24:10.991Z" }, { url = "https://files.pythonhosted.org/packages/0c/20/a9b5d2e27ec44b069ee251bd55544fc76929a067107b1050001566ba86f3/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1fffb56305c5b475ee746cf9e04f97423ba5aaacd292dc1255bd75b1d3b124b", size = 209249, upload-time = "2026-04-24T19:24:12.662Z" }, { url = "https://files.pythonhosted.org/packages/97/e4/e06ee682ed5df67592181f5ecb062e35878967e27f5b6e087237d4548d95/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a6525ec733f43d0541206cffa64fd2aad5a7ae3eb76566aff49cd4db6382209a", size = 213963, upload-time = "2026-04-24T19:24:14.302Z" }, { url = "https://files.pythonhosted.org/packages/9c/9f/1e160e4cd8cdbf062bf6a454cdf814dc7a48eb47e566fdb8f80ccb202605/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:861e393260508efa64d8805a8e49c416c3484907e3f146ce966c69552b49b9a3", size = 210474, upload-time = "2026-04-24T19:24:15.917Z" }, { url = "https://files.pythonhosted.org/packages/7a/e6/cecd913df322df5bbe7ebb8ba39e0708e505a165553900da8a7761026d6f/simplejson-4.1.1-cp314-cp314t-win32.whl", hash = "sha256:d083b89d30948a751d3d97476c2ed91e4caaa24a1a1459bdbadb8876242c71fe", size = 91134, upload-time = "2026-04-24T19:24:17.635Z" }, { url = "https://files.pythonhosted.org/packages/97/73/f540dde99cc1d393bd062ab3b5735b777561a5d8f8a5f2e241164444d77a/simplejson-4.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4cbb299d0528ec0447fe366d8c9641860e28f997a62730690fef905f1f41046e", size = 94467, upload-time = "2026-04-24T19:24:19.109Z" }, { url = "https://files.pythonhosted.org/packages/ce/6a/8b74c52ffd33dbbde00fe7251fee6a0acdc8cea33f7a43805aed258fb79b/simplejson-4.1.1-py3-none-any.whl", hash = "sha256:2ce92b3748f02423e26d2bfb636fb9d7a8f67c8f5854dcae69d350d123b2eee2", size = 69195, upload-time = "2026-04-24T19:24:57.962Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "soupsieve" version = "2.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, ] [[package]] name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "stevedore" version = "5.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, ] [[package]] name = "terminado" version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, ] [[package]] name = "tinycss2" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "tornado" version = "6.5.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" }, { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" }, { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" }, { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" }, { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" }, { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" }, { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" }, { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" }, { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" }, ] [[package]] name = "tqdm" version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] name = "traitlets" version = "5.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" }, ] [[package]] name = "typeguard" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/de/4420db493fa8fc0856d5e5c1b159c63a323d2de2317babe36b01568928e8/typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21", size = 82330, upload-time = "2026-07-26T08:40:23.207Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/eb/461d5f167b6f5c7d97696f397c82f82e3480e003fce3f0a1cd1dd26e2eb2/typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7", size = 36884, upload-time = "2026-07-26T08:40:21.868Z" }, ] [[package]] name = "typing-extensions" version = "4.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "tzdata" version = "2026.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] name = "uri-template" version = "1.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "virtualenv" version = "21.7.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2d/dc/a6eb1ddfa7f1e390fa599b078453c97edb3f6f846b34fb4eac3e8ea16401/virtualenv-21.7.4.tar.gz", hash = "sha256:c9d960c95fa458171e58222a5ccab7465298e4b6559977865e627c4719f1e825", size = 5345511, upload-time = "2026-08-10T22:54:33.316Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/40/4c/eb2f52aeeaf30dbd073d315a251a63ae2b8263171ec4428c135140cb0802/virtualenv-21.7.4-py3-none-any.whl", hash = "sha256:376ec93cd6aab3044fa395d7db226db38043b7b5748948044b2a87168525e843", size = 5324444, upload-time = "2026-08-10T22:54:31.515Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wcwidth" version = "0.8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] name = "webcolors" version = "25.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, ] [[package]] name = "webencodings" version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" }, ] [[package]] name = "websocket-client" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] [[package]] name = "widgetsnbextension" version = "4.0.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, ] [[package]] name = "zensical" version = "0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "deepmerge" }, { name = "jinja2" }, { name = "markdown" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "pyyaml" }, { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/ff/6705d462176c3e25b44349813c1cc536b9ce0ee5ab38aaf5eb6efc73f75b/zensical-0.0.61.tar.gz", hash = "sha256:4c3c3f5a115daa1e3b348d3dab09abc8a67ecc4329c5d1d980325e1187d9e35d", size = 4126960, upload-time = "2026-09-11T18:34:24.645Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/6f/505263e0412f979f625da7c655c281d67e59c6f2f9ab1b2ed69a3d2c824f/zensical-0.0.61-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:03047f4844ed01705c09ea0a79ce80030e61270ff0f3d1e7af36a0bd41aa5c72", size = 14219247, upload-time = "2026-09-11T18:34:04.366Z" }, { url = "https://files.pythonhosted.org/packages/7f/6d/680a4ada7b6fe2bf1469c31ebe0a1e781cfd6b7b3a1b26e41d0e26955c45/zensical-0.0.61-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b54948785683226d5c691ece7d16e4521ceead55627f0473b2ea7ae817e27977", size = 13962599, upload-time = "2026-09-11T18:34:06.852Z" }, { url = "https://files.pythonhosted.org/packages/06/7d/3ba29151f03db7b778c12cc1c05340833af0800c860c7260fb8df89fa352/zensical-0.0.61-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5227b58f1a4269a677c20e6b9c056febe6199a5055bfd01f19abddb88c0981ad", size = 14237890, upload-time = "2026-09-11T18:34:09.278Z" }, { url = "https://files.pythonhosted.org/packages/fa/d9/babd2836f6d5e1d76076b07e12dfd2e3cf4d26aa4bb355e190e2c628c998/zensical-0.0.61-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f2ca93fa4caa35b355d19ff091c634583d170192ac77e511ce579994d76b22a", size = 14261983, upload-time = "2026-09-11T18:34:11.476Z" }, { url = "https://files.pythonhosted.org/packages/11/66/2965577d409fbed7905c62ed5fe00541dc7b8616ed3bafa286e0a1f55fc5/zensical-0.0.61-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad55d63d7f5903456f105f27bf3830212c3e94bd7daa249f606fbc6f428fe067", size = 14517863, upload-time = "2026-09-11T18:34:13.654Z" }, { url = "https://files.pythonhosted.org/packages/26/a4/73a4bd891bced6057b8b48b2ae03bb550ae16858f117b0f6a1a65f801f57/zensical-0.0.61-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:21d999d68265a2ec5872d39392252ea66baba80c1a99c1206ffa51962981328b", size = 14412502, upload-time = "2026-09-11T18:34:15.804Z" }, { url = "https://files.pythonhosted.org/packages/05/44/e729b8a1825e5032caa2d7cbc90db1079e17657a51a020406f7d75dbbd95/zensical-0.0.61-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b9bd453fcf5d8555c9bf27489a001fa34bc6c2ef24a35291d42fda5556d687cb", size = 14746792, upload-time = "2026-09-11T18:34:17.955Z" }, { url = "https://files.pythonhosted.org/packages/09/a1/aeeee6d0a06cfb41ec8d2c4c46e38b7330119eb32c8a4364bef0140dfb96/zensical-0.0.61-cp310-abi3-win_amd64.whl", hash = "sha256:a94ee261cfc45d61470cfd2a314eb2e8d305877792b5cc07f2b57b2ce3955c2f", size = 14530898, upload-time = "2026-09-11T18:34:20.122Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/993804a9f94dcc1bb514312f18be5e22d9a58f57488626b7a9b44b13a67c/zensical-0.0.61-cp310-abi3-win_arm64.whl", hash = "sha256:45c5c6cda9fd83c3e2b7c69bd7d646d874200c5e1fc350538c778b789fa345d3", size = 14239387, upload-time = "2026-09-11T18:34:22.589Z" }, ] cogent3-scinexus-b16bc06/zensical.toml000066400000000000000000000326311525761402500200410ustar00rootroot00000000000000# ============================================================================ # # The configuration produced by default is meant to highlight the features # that Zensical provides and to serve as a starting point for your own # projects. # # ============================================================================ [project] # The site_name is shown in the page header and the browser window title # # Read more: https://zensical.org/docs/setup/basics/#site_name site_name = "scinexus" repo_url = "https://github.com/cogent3/scinexus" # The site_description is included in the HTML head and should contain a # meaningful description of the site content for use by search engines. # # Read more: https://zensical.org/docs/setup/basics/#site_description site_description = "A framework for rapid development of data processing applications." # The site_author attribute. This is used in the HTML head element. # # Read more: https://zensical.org/docs/setup/basics/#site_author site_author = "Gavin Huttley" # The site_url is the canonical URL for your site. When building online # documentation you should set this. # Read more: https://zensical.org/docs/setup/basics/#site_url #site_url = "https://www.example.com/" # The copyright notice appears in the page footer and can contain an HTML # fragment. # # Read more: https://zensical.org/docs/setup/basics/#copyright copyright = """ Copyright © 2026 The authors """ # Zensical supports both implicit navigation and explicitly defined navigation. # If you decide not to define a navigation here then Zensical will simply # derive the navigation structure from the directory structure of your # "docs_dir". The definition below demonstrates how a navigation structure # can be defined using TOML syntax. # # Read more: https://zensical.org/docs/setup/navigation/ nav = [ { "Home" = "index.md" }, { "Install" = "install.md" }, { "Explanation" = [ "explanation/index.md", "explanation/why-composable-apps.md", "explanation/app-lifecycle.md", "explanation/flow.md", "explanation/type-system.md", "explanation/not-completed-design.md", "explanation/source-tracking.md", "explanation/customisation-hooks.md", "explanation/data-store-model.md", "explanation/mpi-backend.md", ] }, { "How-to guides" = [ "howto/index.md", "howto/write-a-function-app.md", "howto/write-a-class-app.md", "howto/handle-failures.md", "howto/use-data-stores.md", "howto/read-and-write-files.md", "howto/run-in-parallel.md", "howto/track-progress.md", "howto/log-and-cite.md", "howto/customise-display-and-ids.md", "howto/extend-type-checking.md", "howto/use-with-coding-agents.md", "howto/migrate-from-cogent3.md", ] }, { "Tutorials" = [ "tutorials/index.md", "tutorials/composing-apps.md", "tutorials/processing-a-dataset.md", ] }, { "API reference" = [ "reference/index.md", "reference/define-app.md", "reference/app-classes.md", "reference/not-completed.md", "reference/source-proxy.md", "reference/data-stores.md", "reference/io-util.md", "reference/parallel.md", "reference/progress.md", "reference/deserialise.md", "reference/utilities.md", ] }, ] # With the "extra_css" option you can add your own CSS styling to customize # your Zensical project according to your needs. You can add any number of # CSS files. # # The path provided should be relative to the "docs_dir". # # Read more: https://zensical.org/docs/customization/#additional-css # extra_css = ["stylesheets/extra.css"] # With the `extra_javascript` option you can add your own JavaScript to your # project to customize the behavior according to your needs. # # The path provided should be relative to the "docs_dir". # # Read more: https://zensical.org/docs/customization/#additional-javascript #extra_javascript = ["javascripts/extra.js"] use_directory_urls = false # ---------------------------------------------------------------------------- # Section for configuring theme options # ---------------------------------------------------------------------------- [project.theme] favicon = "images/logo-bw.png" language = "en" # Template overrides live here, relative to this file. The announcement bar # has no configuration option, it exists only as a template block. # https://zensical.org/docs/customization/ custom_dir = "overrides" # Zensical provides a number of feature toggles that change the behavior # of the documentation site. features = [ # Zensical includes an announcement bar. This feature allows users to # dismiss it when they have read the announcement. # https://zensical.org/docs/setup/header/#announcement-bar "announce.dismiss", # If you have a repository configured and turn on this feature, Zensical # will generate an edit button for the page. This works for common # repository hosting services. # https://zensical.org/docs/setup/repository/#content-actions #"content.action.edit", # If you have a repository configured and turn on this feature, Zensical # will generate a button that allows the user to view the Markdown # code for the current page. # https://zensical.org/docs/setup/repository/#content-actions #"content.action.view", # Code annotations allow you to add an icon with a tooltip to your # code blocks to provide explanations at crucial points. # https://zensical.org/docs/authoring/code-blocks/#code-annotations "content.code.annotate", # This feature turns on a button in code blocks that allow users to # copy the content to their clipboard without first selecting it. # https://zensical.org/docs/authoring/code-blocks/#code-copy-button "content.code.copy", # Code blocks can include a button to allow for the selection of line # ranges by the user. # https://zensical.org/docs/authoring/code-blocks/#code-selection-button "content.code.select", # Zensical can render footnotes as inline tooltips, so the user can read # the footnote without leaving the context of the document. # https://zensical.org/docs/authoring/footnotes/#footnote-tooltips "content.footnote.tooltips", # If you have many content tabs that have the same titles (e.g., "Python", # "JavaScript", "Cobol"), this feature causes all of them to switch to # at the same time when the user chooses their language in one. # https://zensical.org/docs/authoring/content-tabs/#linked-content-tabs "content.tabs.link", # With this feature enabled users can add tooltips to links that will be # displayed when the mouse pointer hovers the link. # https://zensical.org/docs/authoring/tooltips/#improved-tooltips "content.tooltips", # With this feature enabled, Zensical will automatically hide parts # of the header when the user scrolls past a certain point. # https://zensical.org/docs/setup/header/#automatic-hiding "header.autohide", # Turn on this feature to expand all collapsible sections in the # navigation sidebar by default. # https://zensical.org/docs/setup/navigation/#navigation-expansion # "navigation.expand", # This feature turns on navigation elements in the footer that allow the # user to navigate to a next or previous page. # https://zensical.org/docs/setup/footer/#navigation "navigation.footer", # When section index pages are enabled, documents can be directly attached # to sections, which is particularly useful for providing overview pages. # https://zensical.org/docs/setup/navigation/#section-index-pages "navigation.indexes", # When instant navigation is enabled, clicks on all internal links will be # intercepted and dispatched via XHR without fully reloading the page. # https://zensical.org/docs/setup/navigation/#instant-navigation "navigation.instant", # With instant prefetching, your site will start to fetch a page once the # user hovers over a link. This will reduce the perceived loading time # for the user. # https://zensical.org/docs/setup/navigation/#instant-prefetching "navigation.instant.prefetch", # In order to provide a better user experience on slow connections when # using instant navigation, a progress indicator can be enabled. # https://zensical.org/docs/setup/navigation/#progress-indicator #"navigation.instant.progress", # When navigation paths are activated, a breadcrumb navigation is rendered # above the title of each page # https://zensical.org/docs/setup/navigation/#navigation-path "navigation.path", # When pruning is enabled, only the visible navigation items are included # in the rendered HTML, reducing the size of the built site by 33% or more. # https://zensical.org/docs/setup/navigation/#navigation-pruning "navigation.prune", # When sections are enabled, top-level sections are rendered as groups in # the sidebar for viewports above 1220px, but remain as-is on mobile. # https://zensical.org/docs/setup/navigation/#navigation-sections "navigation.sections", # When tabs are enabled, top-level sections are rendered in a menu layer # below the header for viewports above 1220px, but remain as-is on mobile. # https://zensical.org/docs/setup/navigation/#navigation-tabs "navigation.tabs", # When sticky tabs are enabled, navigation tabs will lock below the header # and always remain visible when scrolling down. # https://zensical.org/docs/setup/navigation/#sticky-navigation-tabs "navigation.tabs.sticky", # A back-to-top button can be shown when the user, after scrolling down, # starts to scroll up again. # https://zensical.org/docs/setup/navigation/#back-to-top-button "navigation.top", # When anchor tracking is enabled, the URL in the address bar is # automatically updated with the active anchor as highlighted in the table # of contents. # https://zensical.org/docs/setup/navigation/#anchor-tracking "navigation.tracking", # When search highlighting is enabled and a user clicks on a search result, # Zensical will highlight all occurrences after following the link. # https://zensical.org/docs/setup/search/#search-highlighting "search.highlight", # When anchor following for the table of contents is enabled, the sidebar # is automatically scrolled so that the active anchor is always visible. # https://zensical.org/docs/setup/navigation/#anchor-following # "toc.follow", # When navigation integration for the table of contents is enabled, it is # always rendered as part of the navigation sidebar on the left. # https://zensical.org/docs/setup/navigation/#navigation-integration #"toc.integrate", ] # ---------------------------------------------------------------------------- # You can configure your own logo to be shown in the header using the "logo" # option in the "theme" subsection. The logo must be a relative path to a file # in your "docs_dir", e.g., to use `docs/assets/logo.png` you would set: # ---------------------------------------------------------------------------- logo = "images/logo-bw.png" # ---------------------------------------------------------------------------- # In the "font" subsection you can configure the fonts used. By default, fonts # are loaded from Google Fonts, giving you a wide range of choices from a set # of suitably licensed fonts. There are options for a normal text font and for # a monospaced font used in code blocks. # ---------------------------------------------------------------------------- #[project.theme.font] #text = "Inter" #code = "Jetbrains Mono" # ---------------------------------------------------------------------------- # In the "palette" subsection you can configure options for the color scheme. # You can configure different color schemes, e.g., to turn on dark mode, # that the user can switch between. Each color scheme can be further # customized. # # Read more: # - https://zensical.org/docs/setup/colors/ # ---------------------------------------------------------------------------- [[project.theme.palette]] scheme = "default" toggle.icon = "lucide/sun" toggle.name = "Switch to dark mode" [[project.theme.palette]] scheme = "slate" toggle.icon = "lucide/moon" toggle.name = "Switch to light mode" # ---------------------------------------------------------------------------- # The "extra" section contains miscellaneous settings. # ---------------------------------------------------------------------------- #[[project.extra.social]] icon = "fontawesome/brands/github" link = "https://github.com/cogent3/scinexus" [project.markdown_extensions.pymdownx.superfences] custom_fences = [ { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }, ] [project.markdown_extensions.pymdownx.inlinehilite] [project.markdown_extensions.admonition] [project.markdown_extensions.pymdownx.details] [project.markdown_extensions.pymdownx.emoji] emoji_index = "zensical.extensions.emoji.twemoji" emoji_generator = "zensical.extensions.emoji.to_svg" [project.plugins.mkdocstrings.handlers.python] inventories = ["https://docs.python.org/3/objects.inv"] paths = ["src"] [project.plugins.mkdocstrings.handlers.python.options] docstring_style = "numpy" inherited_members = true show_source = false [project.markdown_extensions.pymdownx.tabbed] alternate_style = true [project.markdown_extensions.footnotes]