The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── FUNDING.yml
    ├── SECURITY.md
    └── workflows
    │   ├── main.yml
    │   └── pypi-package.yml
├── .gitignore
├── CHANGELOG.md
├── Justfile
├── LICENSE
├── NOTICE
├── README.md
├── pyproject.toml
├── src
    └── aiofiles
    │   ├── __init__.py
    │   ├── base.py
    │   ├── os.py
    │   ├── ospath.py
    │   ├── tempfile
    │       ├── __init__.py
    │       └── temptypes.py
    │   └── threadpool
    │       ├── __init__.py
    │       ├── binary.py
    │       ├── text.py
    │       └── utils.py
├── tests
    ├── resources
    │   ├── multiline_file.txt
    │   └── test_file1.txt
    ├── test_os.py
    ├── test_simple.py
    ├── test_stdio.py
    ├── test_tempfile.py
    └── threadpool
    │   ├── test_binary.py
    │   ├── test_concurrency.py
    │   ├── test_open.py
    │   ├── test_text.py
    │   └── test_wrap.py
├── tox.ini
└── uv.lock


/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ---
2 | tidelift: "pypi/aiofiles"
3 | github: Tinche
4 | 


--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | ## Security contact information
2 | 
3 | To report a security vulnerability, please use the
4 | [Tidelift security contact](https://tidelift.com/security).
5 | Tidelift will coordinate the fix and disclosure.
6 | 


--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
  1 | ---
  2 | name: CI
  3 | 
  4 | on:
  5 |   push:
  6 |     branches: ["main"]
  7 |   pull_request:
  8 |     branches: ["main"]
  9 |   workflow_dispatch:
 10 | 
 11 | jobs:
 12 |   tests:
 13 |     name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}"
 14 |     runs-on: ${{ matrix.os }}
 15 |     strategy:
 16 |       matrix:
 17 |         os: [ubuntu-latest, windows-latest]
 18 |         python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.9"]
 19 | 
 20 |     steps:
 21 |       - uses: "actions/checkout@v4"
 22 | 
 23 |       - uses: hynek/setup-cached-uv@v2
 24 |       - uses: extractions/setup-just@v3
 25 | 
 26 |       - name: "Run tox targets for ${{ matrix.python-version }}"
 27 |         env:
 28 |           V: ${{ matrix.python-version }}
 29 |         run: "uv run --group tox tox -e py$(echo $V | tr -d . | sed 's/^py//')"
 30 | 
 31 |       - name: "Lint"
 32 |         if: matrix.python-version == '3.13' && runner.os == 'Linux'
 33 |         run: "uv run --group tox tox -e lint"
 34 | 
 35 |       - name: "Upload coverage data"
 36 |         uses: "actions/upload-artifact@v4"
 37 |         with:
 38 |           name: coverage-data-${{ matrix.python-version }}
 39 |           path: ".coverage.*"
 40 |           if-no-files-found: "ignore"
 41 |           include-hidden-files: true
 42 |         if: runner.os == 'Linux'
 43 | 
 44 |   coverage:
 45 |     name: "Combine & check coverage."
 46 |     needs: "tests"
 47 |     runs-on: "ubuntu-latest"
 48 | 
 49 |     steps:
 50 |       - uses: "actions/checkout@v4"
 51 |       - uses: hynek/setup-cached-uv@v2
 52 | 
 53 |       - uses: "actions/download-artifact@v4"
 54 |         with:
 55 |           pattern: "coverage-data-*"
 56 |           merge-multiple: true
 57 | 
 58 |       - name: "Combine coverage"
 59 |         run: |
 60 |           uv sync --group test --python 3.13
 61 |           uv run coverage combine
 62 |           uv run coverage html --skip-covered --skip-empty
 63 |           uv run coverage json
 64 | 
 65 |           # Report and write to summary.
 66 |           uv run coverage report | sed 's/^/    /' >> $GITHUB_STEP_SUMMARY
 67 | 
 68 |           export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])")
 69 |           echo "total=$TOTAL" >> $GITHUB_ENV
 70 | 
 71 |       - name: "Upload HTML report."
 72 |         uses: "actions/upload-artifact@v4"
 73 |         with:
 74 |           name: "html-report"
 75 |           path: "htmlcov"
 76 | 
 77 |       - name: "Make badge"
 78 |         if: github.ref == 'refs/heads/main'
 79 |         uses: "schneegans/dynamic-badges-action@v1.4.0"
 80 |         with:
 81 |           # GIST_TOKEN is a GitHub personal access token with scope "gist".
 82 |           auth: ${{ secrets.GIST_TOKEN }}
 83 |           gistID: 882f02e3df32136c847ba90d2688f06e
 84 |           filename: covbadge.json
 85 |           label: Coverage
 86 |           message: ${{ env.total }}%
 87 |           minColorRange: 50
 88 |           maxColorRange: 90
 89 |           valColorRange: ${{ env.total }}
 90 | 
 91 |   package:
 92 |     name: "Build & verify package"
 93 |     runs-on: "ubuntu-latest"
 94 | 
 95 |     steps:
 96 |       - uses: "actions/checkout@v4"
 97 |       - uses: hynek/setup-cached-uv@v2
 98 | 
 99 |       - name: "Build package"
100 |         run: "uv build"
101 |       - name: "List result"
102 |         run: "ls -l dist"
103 |       - name: "Check wheel contents"
104 |         run: "uvx check-wheel-contents dist/*.whl"
105 |       - name: "Check long_description"
106 |         run: "uvx twine check dist/*"
107 | 


--------------------------------------------------------------------------------
/.github/workflows/pypi-package.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Build & maybe upload PyPI package
 3 | 
 4 | on:
 5 |   push:
 6 |     branches: [main]
 7 |     tags: ["*"]
 8 |   release:
 9 |     types:
10 |       - published
11 |   workflow_dispatch:
12 | 
13 | permissions:
14 |   contents: read
15 |   id-token: write
16 | 
17 | jobs:
18 |   build-package:
19 |     name: Build & verify package
20 |     runs-on: ubuntu-latest
21 | 
22 |     steps:
23 |       - uses: actions/checkout@v4
24 |         with:
25 |           fetch-depth: 0
26 | 
27 |       - uses: hynek/build-and-inspect-python-package@v2
28 | 
29 |   # Upload to Test PyPI on every commit on main.
30 |   release-test-pypi:
31 |     name: Publish in-dev package to test.pypi.org
32 |     environment: release-test-pypi
33 |     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
34 |     runs-on: ubuntu-latest
35 |     needs: build-package
36 | 
37 |     steps:
38 |       - name: Download packages built by build-and-inspect-python-package
39 |         uses: actions/download-artifact@v4
40 |         with:
41 |           name: Packages
42 |           path: dist
43 | 
44 |       - name: Upload package to Test PyPI
45 |         uses: pypa/gh-action-pypi-publish@release/v1
46 |         with:
47 |           repository-url: https://test.pypi.org/legacy/
48 | 
49 |   # Upload to real PyPI on GitHub Releases.
50 |   release-pypi:
51 |     name: Publish released package to pypi.org
52 |     environment: release-pypi
53 |     if: github.event.action == 'published'
54 |     runs-on: ubuntu-latest
55 |     needs: build-package
56 | 
57 |     steps:
58 |       - name: Download packages built by build-and-inspect-python-package
59 |         uses: actions/download-artifact@v4
60 |         with:
61 |           name: Packages
62 |           path: dist
63 | 
64 |       - name: Upload package to PyPI
65 |         uses: pypa/gh-action-pypi-publish@release/v1
66 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | 
 28 | # PyInstaller
 29 | #  Usually these files are written by a python script from a template
 30 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 31 | *.manifest
 32 | *.spec
 33 | 
 34 | # Installer logs
 35 | pip-log.txt
 36 | pip-delete-this-directory.txt
 37 | 
 38 | # Unit test / coverage reports
 39 | htmlcov/
 40 | .tox/
 41 | .nox/
 42 | .coverage
 43 | .coverage.*
 44 | .cache
 45 | nosetests.xml
 46 | coverage.xml
 47 | *.cover
 48 | *.py,cover
 49 | .hypothesis/
 50 | .pytest_cache/
 51 | cover/
 52 | 
 53 | # Translations
 54 | *.mo
 55 | *.pot
 56 | 
 57 | # Sphinx documentation
 58 | docs/_build/
 59 | 
 60 | # PyBuilder
 61 | target/
 62 | 
 63 | # pyenv
 64 | #   For a library or package, you might want to ignore these files since the code is
 65 | #   intended to run in multiple environments; otherwise, check them in:
 66 | # .python-version
 67 | 
 68 | # UV
 69 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 70 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
 71 | #   commonly ignored for libraries.
 72 | # uv.lock
 73 | 
 74 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 75 | __pypackages__/
 76 | 
 77 | # Environments
 78 | .env
 79 | .venv
 80 | env/
 81 | venv/
 82 | ENV/
 83 | env.bak/
 84 | venv.bak/
 85 | 
 86 | # mypy
 87 | .mypy_cache/
 88 | .dmypy.json
 89 | dmypy.json
 90 | 
 91 | # PyCharm
 92 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 93 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 94 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
 95 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 96 | .idea/
 97 | 
 98 | # Visual Studio Code
 99 | #  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
100 | #  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
101 | #  and can be added to the global gitignore or merged into this file. However, if you prefer, 
102 | #  you could uncomment the following to ignore the entire vscode folder
103 | .vscode/
104 | 
105 | # Ruff stuff:
106 | .ruff_cache/
107 | 
108 | # PyPI configuration file
109 | .pypirc
110 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | # History
  2 | 
  3 | ## 25.1.0 (UNRELEASED)
  4 | 
  5 | - Switch to [uv](https://docs.astral.sh/uv/) + add Python v3.14 support.
  6 |   ([#219](https://github.com/Tinche/aiofiles/pull/219))
  7 | - Add `ruff` formatter and linter.
  8 |   [#216](https://github.com/Tinche/aiofiles/pull/216)
  9 | - Drop Python 3.8 support. If you require it, use version 24.1.0.
 10 |   [#204](https://github.com/Tinche/aiofiles/pull/204)
 11 | 
 12 | ## 24.1.0 (2024-06-24)
 13 | 
 14 | - Import `os.link` conditionally to fix importing on android.
 15 |   [#175](https://github.com/Tinche/aiofiles/issues/175)
 16 | - Remove spurious items from `aiofiles.os.__all__` when running on Windows.
 17 | - Switch to more modern async idioms: Remove types.coroutine and make AiofilesContextManager an awaitable instead a coroutine.
 18 | - Add `aiofiles.os.path.abspath` and `aiofiles.os.getcwd`.
 19 |   [#174](https://github.com/Tinche/aiofiles/issues/181)
 20 | - _aiofiles_ is now tested on Python 3.13 too.
 21 |   [#184](https://github.com/Tinche/aiofiles/pull/184)
 22 | - Drop Python 3.7 support. If you require it, use version 23.2.1.
 23 | 
 24 | ## 23.2.1 (2023-08-09)
 25 | 
 26 | - Import `os.statvfs` conditionally to fix importing on non-UNIX systems.
 27 |   [#171](https://github.com/Tinche/aiofiles/issues/171) [#172](https://github.com/Tinche/aiofiles/pull/172)
 28 | - aiofiles is now also tested on Windows.
 29 | 
 30 | ## 23.2.0 (2023-08-09)
 31 | 
 32 | - aiofiles is now tested on Python 3.12 too.
 33 |   [#166](https://github.com/Tinche/aiofiles/issues/166) [#168](https://github.com/Tinche/aiofiles/pull/168)
 34 | - On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` now accepts a `delete_on_close` argument, just like the stdlib version.
 35 | - On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` no longer exposes a `delete` attribute, just like the stdlib version.
 36 | - Added `aiofiles.os.statvfs` and `aiofiles.os.path.ismount`.
 37 |   [#162](https://github.com/Tinche/aiofiles/pull/162)
 38 | - Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
 39 |   [#169](https://github.com/Tinche/aiofiles/pull/169)
 40 | 
 41 | ## 23.1.0 (2023-02-09)
 42 | 
 43 | - Added `aiofiles.os.access`.
 44 |   [#146](https://github.com/Tinche/aiofiles/pull/146)
 45 | - Removed `aiofiles.tempfile.temptypes.AsyncSpooledTemporaryFile.softspace`.
 46 |   [#151](https://github.com/Tinche/aiofiles/pull/151)
 47 | - Added `aiofiles.stdin`, `aiofiles.stdin_bytes`, and other stdio streams.
 48 |   [#154](https://github.com/Tinche/aiofiles/pull/154)
 49 | - Transition to `asyncio.get_running_loop` (vs `asyncio.get_event_loop`) internally.
 50 | 
 51 | ## 22.1.0 (2022-09-04)
 52 | 
 53 | - Added `aiofiles.os.path.islink`.
 54 |   [#126](https://github.com/Tinche/aiofiles/pull/126)
 55 | - Added `aiofiles.os.readlink`.
 56 |   [#125](https://github.com/Tinche/aiofiles/pull/125)
 57 | - Added `aiofiles.os.symlink`.
 58 |   [#124](https://github.com/Tinche/aiofiles/pull/124)
 59 | - Added `aiofiles.os.unlink`.
 60 |   [#123](https://github.com/Tinche/aiofiles/pull/123)
 61 | - Added `aiofiles.os.link`.
 62 |   [#121](https://github.com/Tinche/aiofiles/pull/121)
 63 | - Added `aiofiles.os.renames`.
 64 |   [#120](https://github.com/Tinche/aiofiles/pull/120)
 65 | - Added `aiofiles.os.{listdir, scandir}`.
 66 |   [#143](https://github.com/Tinche/aiofiles/pull/143)
 67 | - Switched to CalVer.
 68 | - Dropped Python 3.6 support. If you require it, use version 0.8.0.
 69 | - aiofiles is now tested on Python 3.11.
 70 | 
 71 | ## 0.8.0 (2021-11-27)
 72 | 
 73 | - aiofiles is now tested on Python 3.10.
 74 | - Added `aiofiles.os.replace`.
 75 |   [#107](https://github.com/Tinche/aiofiles/pull/107)
 76 | - Added `aiofiles.os.{makedirs, removedirs}`.
 77 | - Added `aiofiles.os.path.{exists, isfile, isdir, getsize, getatime, getctime, samefile, sameopenfile}`.
 78 |   [#63](https://github.com/Tinche/aiofiles/pull/63)
 79 | - Added `suffix`, `prefix`, `dir` args to `aiofiles.tempfile.TemporaryDirectory`.
 80 |   [#116](https://github.com/Tinche/aiofiles/pull/116)
 81 | 
 82 | ## 0.7.0 (2021-05-17)
 83 | 
 84 | - Added the `aiofiles.tempfile` module for async temporary files.
 85 |   [#56](https://github.com/Tinche/aiofiles/pull/56)
 86 | - Switched to Poetry and GitHub actions.
 87 | - Dropped 3.5 support.
 88 | 
 89 | ## 0.6.0 (2020-10-27)
 90 | 
 91 | - `aiofiles` is now tested on ppc64le.
 92 | - Added `name` and `mode` properties to async file objects.
 93 |   [#82](https://github.com/Tinche/aiofiles/pull/82)
 94 | - Fixed a DeprecationWarning internally.
 95 |   [#75](https://github.com/Tinche/aiofiles/pull/75)
 96 | - Python 3.9 support and tests.
 97 | 
 98 | ## 0.5.0 (2020-04-12)
 99 | 
100 | - Python 3.8 support. Code base modernization (using `async/await` instead of `asyncio.coroutine`/`yield from`).
101 | - Added `aiofiles.os.remove`, `aiofiles.os.rename`, `aiofiles.os.mkdir`, `aiofiles.os.rmdir`.
102 |   [#62](https://github.com/Tinche/aiofiles/pull/62)
103 | 
104 | ## 0.4.0 (2018-08-11)
105 | 
106 | - Python 3.7 support.
107 | - Removed Python 3.3/3.4 support. If you use these versions, stick to aiofiles 0.3.x.
108 | 
109 | ## 0.3.2 (2017-09-23)
110 | 
111 | - The LICENSE is now included in the sdist.
112 |   [#31](https://github.com/Tinche/aiofiles/pull/31)
113 | 
114 | ## 0.3.1 (2017-03-10)
115 | 
116 | - Introduced a changelog.
117 | - `aiofiles.os.sendfile` will now work if the standard `os` module contains a `sendfile` function.
118 | 


--------------------------------------------------------------------------------
/Justfile:
--------------------------------------------------------------------------------
 1 | tests_dir := "tests"
 2 | code_dirs := "src" + " " + tests_dir
 3 | run_prefix := if env_var_or_default("VIRTUAL_ENV", "") == "" { "uv run " } else { "" }
 4 | 
 5 | check:
 6 | 	{{ run_prefix }}ruff format --check {{ code_dirs }}
 7 | 	{{ run_prefix }}ruff check {{ code_dirs }}
 8 | 
 9 | coverage:
10 | 	{{ run_prefix }}coverage run -m pytest {{ tests_dir }}
11 | 
12 | format:
13 | 	{{ run_prefix }}ruff format {{ code_dirs }}
14 | 
15 | lint: format
16 | 	{{ run_prefix }}ruff check --fix {{ code_dirs }}
17 | 
18 | test:
19 | 	{{ run_prefix }}pytest -x --ff {{ tests_dir }}


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 | Apache License
  2 |                            Version 2.0, January 2004
  3 |                         http://www.apache.org/licenses/
  4 | 
  5 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 | 
  7 |    1. Definitions.
  8 | 
  9 |       "License" shall mean the terms and conditions for use, reproduction,
 10 |       and distribution as defined by Sections 1 through 9 of this document.
 11 | 
 12 |       "Licensor" shall mean the copyright owner or entity authorized by
 13 |       the copyright owner that is granting the License.
 14 | 
 15 |       "Legal Entity" shall mean the union of the acting entity and all
 16 |       other entities that control, are controlled by, or are under common
 17 |       control with that entity. For the purposes of this definition,
 18 |       "control" means (i) the power, direct or indirect, to cause the
 19 |       direction or management of such entity, whether by contract or
 20 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 21 |       outstanding shares, or (iii) beneficial ownership of such entity.
 22 | 
 23 |       "You" (or "Your") shall mean an individual or Legal Entity
 24 |       exercising permissions granted by this License.
 25 | 
 26 |       "Source" form shall mean the preferred form for making modifications,
 27 |       including but not limited to software source code, documentation
 28 |       source, and configuration files.
 29 | 
 30 |       "Object" form shall mean any form resulting from mechanical
 31 |       transformation or translation of a Source form, including but
 32 |       not limited to compiled object code, generated documentation,
 33 |       and conversions to other media types.
 34 | 
 35 |       "Work" shall mean the work of authorship, whether in Source or
 36 |       Object form, made available under the License, as indicated by a
 37 |       copyright notice that is included in or attached to the work
 38 |       (an example is provided in the Appendix below).
 39 | 
 40 |       "Derivative Works" shall mean any work, whether in Source or Object
 41 |       form, that is based on (or derived from) the Work and for which the
 42 |       editorial revisions, annotations, elaborations, or other modifications
 43 |       represent, as a whole, an original work of authorship. For the purposes
 44 |       of this License, Derivative Works shall not include works that remain
 45 |       separable from, or merely link (or bind by name) to the interfaces of,
 46 |       the Work and Derivative Works thereof.
 47 | 
 48 |       "Contribution" shall mean any work of authorship, including
 49 |       the original version of the Work and any modifications or additions
 50 |       to that Work or Derivative Works thereof, that is intentionally
 51 |       submitted to Licensor for inclusion in the Work by the copyright owner
 52 |       or by an individual or Legal Entity authorized to submit on behalf of
 53 |       the copyright owner. For the purposes of this definition, "submitted"
 54 |       means any form of electronic, verbal, or written communication sent
 55 |       to the Licensor or its representatives, including but not limited to
 56 |       communication on electronic mailing lists, source code control systems,
 57 |       and issue tracking systems that are managed by, or on behalf of, the
 58 |       Licensor for the purpose of discussing and improving the Work, but
 59 |       excluding communication that is conspicuously marked or otherwise
 60 |       designated in writing by the copyright owner as "Not a Contribution."
 61 | 
 62 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 63 |       on behalf of whom a Contribution has been received by Licensor and
 64 |       subsequently incorporated within the Work.
 65 | 
 66 |    2. Grant of Copyright License. Subject to the terms and conditions of
 67 |       this License, each Contributor hereby grants to You a perpetual,
 68 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 69 |       copyright license to reproduce, prepare Derivative Works of,
 70 |       publicly display, publicly perform, sublicense, and distribute the
 71 |       Work and such Derivative Works in Source or Object form.
 72 | 
 73 |    3. Grant of Patent License. Subject to the terms and conditions of
 74 |       this License, each Contributor hereby grants to You a perpetual,
 75 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 76 |       (except as stated in this section) patent license to make, have made,
 77 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 78 |       where such license applies only to those patent claims licensable
 79 |       by such Contributor that are necessarily infringed by their
 80 |       Contribution(s) alone or by combination of their Contribution(s)
 81 |       with the Work to which such Contribution(s) was submitted. If You
 82 |       institute patent litigation against any entity (including a
 83 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 84 |       or a Contribution incorporated within the Work constitutes direct
 85 |       or contributory patent infringement, then any patent licenses
 86 |       granted to You under this License for that Work shall terminate
 87 |       as of the date such litigation is filed.
 88 | 
 89 |    4. Redistribution. You may reproduce and distribute copies of the
 90 |       Work or Derivative Works thereof in any medium, with or without
 91 |       modifications, and in Source or Object form, provided that You
 92 |       meet the following conditions:
 93 | 
 94 |       (a) You must give any other recipients of the Work or
 95 |           Derivative Works a copy of this License; and
 96 | 
 97 |       (b) You must cause any modified files to carry prominent notices
 98 |           stating that You changed the files; and
 99 | 
100 |       (c) You must retain, in the Source form of any Derivative Works
101 |           that You distribute, all copyright, patent, trademark, and
102 |           attribution notices from the Source form of the Work,
103 |           excluding those notices that do not pertain to any part of
104 |           the Derivative Works; and
105 | 
106 |       (d) If the Work includes a "NOTICE" text file as part of its
107 |           distribution, then any Derivative Works that You distribute must
108 |           include a readable copy of the attribution notices contained
109 |           within such NOTICE file, excluding those notices that do not
110 |           pertain to any part of the Derivative Works, in at least one
111 |           of the following places: within a NOTICE text file distributed
112 |           as part of the Derivative Works; within the Source form or
113 |           documentation, if provided along with the Derivative Works; or,
114 |           within a display generated by the Derivative Works, if and
115 |           wherever such third-party notices normally appear. The contents
116 |           of the NOTICE file are for informational purposes only and
117 |           do not modify the License. You may add Your own attribution
118 |           notices within Derivative Works that You distribute, alongside
119 |           or as an addendum to the NOTICE text from the Work, provided
120 |           that such additional attribution notices cannot be construed
121 |           as modifying the License.
122 | 
123 |       You may add Your own copyright statement to Your modifications and
124 |       may provide additional or different license terms and conditions
125 |       for use, reproduction, or distribution of Your modifications, or
126 |       for any such Derivative Works as a whole, provided Your use,
127 |       reproduction, and distribution of the Work otherwise complies with
128 |       the conditions stated in this License.
129 | 
130 |    5. Submission of Contributions. Unless You explicitly state otherwise,
131 |       any Contribution intentionally submitted for inclusion in the Work
132 |       by You to the Licensor shall be under the terms and conditions of
133 |       this License, without any additional terms or conditions.
134 |       Notwithstanding the above, nothing herein shall supersede or modify
135 |       the terms of any separate license agreement you may have executed
136 |       with Licensor regarding such Contributions.
137 | 
138 |    6. Trademarks. This License does not grant permission to use the trade
139 |       names, trademarks, service marks, or product names of the Licensor,
140 |       except as required for reasonable and customary use in describing the
141 |       origin of the Work and reproducing the content of the NOTICE file.
142 | 
143 |    7. Disclaimer of Warranty. Unless required by applicable law or
144 |       agreed to in writing, Licensor provides the Work (and each
145 |       Contributor provides its Contributions) on an "AS IS" BASIS,
146 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 |       implied, including, without limitation, any warranties or conditions
148 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 |       PARTICULAR PURPOSE. You are solely responsible for determining the
150 |       appropriateness of using or redistributing the Work and assume any
151 |       risks associated with Your exercise of permissions under this License.
152 | 
153 |    8. Limitation of Liability. In no event and under no legal theory,
154 |       whether in tort (including negligence), contract, or otherwise,
155 |       unless required by applicable law (such as deliberate and grossly
156 |       negligent acts) or agreed to in writing, shall any Contributor be
157 |       liable to You for damages, including any direct, indirect, special,
158 |       incidental, or consequential damages of any character arising as a
159 |       result of this License or out of the use or inability to use the
160 |       Work (including but not limited to damages for loss of goodwill,
161 |       work stoppage, computer failure or malfunction, or any and all
162 |       other commercial damages or losses), even if such Contributor
163 |       has been advised of the possibility of such damages.
164 | 
165 |    9. Accepting Warranty or Additional Liability. While redistributing
166 |       the Work or Derivative Works thereof, You may choose to offer,
167 |       and charge a fee for, acceptance of support, warranty, indemnity,
168 |       or other liability obligations and/or rights consistent with this
169 |       License. However, in accepting such obligations, You may act only
170 |       on Your own behalf and on Your sole responsibility, not on behalf
171 |       of any other Contributor, and only if You agree to indemnify,
172 |       defend, and hold each Contributor harmless for any liability
173 |       incurred by, or claims asserted against, such Contributor by reason
174 |       of your accepting any such warranty or additional liability.
175 | 
176 |    END OF TERMS AND CONDITIONS
177 | 
178 |    APPENDIX: How to apply the Apache License to your work.
179 | 
180 |       To apply the Apache License to your work, attach the following
181 |       boilerplate notice, with the fields enclosed by brackets "{}"
182 |       replaced with your own identifying information. (Don't include
183 |       the brackets!)  The text should be enclosed in the appropriate
184 |       comment syntax for the file format. We also recommend that a
185 |       file or class name and description of purpose be included on the
186 |       same "printed page" as the copyright notice for easier
187 |       identification within third-party archives.
188 | 
189 |    Copyright {yyyy} {name of copyright owner}
190 | 
191 |    Licensed under the Apache License, Version 2.0 (the "License");
192 |    you may not use this file except in compliance with the License.
193 |    You may obtain a copy of the License at
194 | 
195 |        http://www.apache.org/licenses/LICENSE-2.0
196 | 
197 |    Unless required by applicable law or agreed to in writing, software
198 |    distributed under the License is distributed on an "AS IS" BASIS,
199 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 |    See the License for the specific language governing permissions and
201 |    limitations under the License.
202 | 
203 | 


--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Asyncio support for files
2 | Copyright 2016 Tin Tvrtkovic
3 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # aiofiles: file support for asyncio
  2 | 
  3 | [![PyPI](https://img.shields.io/pypi/v/aiofiles.svg)](https://pypi.python.org/pypi/aiofiles)
  4 | [![Build](https://github.com/Tinche/aiofiles/workflows/CI/badge.svg)](https://github.com/Tinche/aiofiles/actions)
  5 | [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Tinche/882f02e3df32136c847ba90d2688f06e/raw/covbadge.json)](https://github.com/Tinche/aiofiles/actions/workflows/main.yml)
  6 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/aiofiles.svg)](https://github.com/Tinche/aiofiles)
  7 | [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
  8 | 
  9 | **aiofiles** is an Apache2 licensed library, written in Python, for handling local
 10 | disk files in asyncio applications.
 11 | 
 12 | Ordinary local file IO is blocking, and cannot easily and portably be made
 13 | asynchronous. This means doing file IO may interfere with asyncio applications,
 14 | which shouldn't block the executing thread. aiofiles helps with this by
 15 | introducing asynchronous versions of files that support delegating operations to
 16 | a separate thread pool.
 17 | 
 18 | ```python
 19 | async with aiofiles.open('filename', mode='r') as f:
 20 |     contents = await f.read()
 21 | print(contents)
 22 | 'My file contents'
 23 | ```
 24 | 
 25 | Asynchronous iteration is also supported.
 26 | 
 27 | ```python
 28 | async with aiofiles.open('filename') as f:
 29 |     async for line in f:
 30 |         ...
 31 | ```
 32 | 
 33 | Asynchronous interface to tempfile module.
 34 | 
 35 | ```python
 36 | async with aiofiles.tempfile.TemporaryFile('wb') as f:
 37 |     await f.write(b'Hello, World!')
 38 | ```
 39 | 
 40 | ## Features
 41 | 
 42 | - a file API very similar to Python's standard, blocking API
 43 | - support for buffered and unbuffered binary files, and buffered text files
 44 | - support for `async`/`await` ([PEP 492](https://peps.python.org/pep-0492/)) constructs
 45 | - async interface to tempfile module
 46 | 
 47 | ## Installation
 48 | 
 49 | To install aiofiles, simply:
 50 | 
 51 | ```shell
 52 | pip install aiofiles
 53 | ```
 54 | 
 55 | ## Usage
 56 | 
 57 | Files are opened using the `aiofiles.open()` coroutine, which in addition to
 58 | mirroring the builtin `open` accepts optional `loop` and `executor`
 59 | arguments. If `loop` is absent, the default loop will be used, as per the
 60 | set asyncio policy. If `executor` is not specified, the default event loop
 61 | executor will be used.
 62 | 
 63 | In case of success, an asynchronous file object is returned with an
 64 | API identical to an ordinary file, except the following methods are coroutines
 65 | and delegate to an executor:
 66 | 
 67 | - `close`
 68 | - `flush`
 69 | - `isatty`
 70 | - `read`
 71 | - `readall`
 72 | - `read1`
 73 | - `readinto`
 74 | - `readline`
 75 | - `readlines`
 76 | - `seek`
 77 | - `seekable`
 78 | - `tell`
 79 | - `truncate`
 80 | - `writable`
 81 | - `write`
 82 | - `writelines`
 83 | 
 84 | In case of failure, one of the usual exceptions will be raised.
 85 | 
 86 | `aiofiles.stdin`, `aiofiles.stdout`, `aiofiles.stderr`,
 87 | `aiofiles.stdin_bytes`, `aiofiles.stdout_bytes`, and
 88 | `aiofiles.stderr_bytes` provide async access to `sys.stdin`,
 89 | `sys.stdout`, `sys.stderr`, and their corresponding `.buffer` properties.
 90 | 
 91 | The `aiofiles.os` module contains executor-enabled coroutine versions of
 92 | several useful `os` functions that deal with files:
 93 | 
 94 | - `stat`
 95 | - `statvfs`
 96 | - `sendfile`
 97 | - `rename`
 98 | - `renames`
 99 | - `replace`
100 | - `remove`
101 | - `unlink`
102 | - `mkdir`
103 | - `makedirs`
104 | - `rmdir`
105 | - `removedirs`
106 | - `link`
107 | - `symlink`
108 | - `readlink`
109 | - `listdir`
110 | - `scandir`
111 | - `access`
112 | - `getcwd`
113 | - `path.abspath`
114 | - `path.exists`
115 | - `path.isfile`
116 | - `path.isdir`
117 | - `path.islink`
118 | - `path.ismount`
119 | - `path.getsize`
120 | - `path.getatime`
121 | - `path.getctime`
122 | - `path.samefile`
123 | - `path.sameopenfile`
124 | 
125 | ### Tempfile
126 | 
127 | **aiofiles.tempfile** implements the following interfaces:
128 | 
129 | - TemporaryFile
130 | - NamedTemporaryFile
131 | - SpooledTemporaryFile
132 | - TemporaryDirectory
133 | 
134 | Results return wrapped with a context manager allowing use with async with and async for.
135 | 
136 | ```python
137 | async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f:
138 |     await f.write(b'Line1\n Line2')
139 |     await f.seek(0)
140 |     async for line in f:
141 |         print(line)
142 | 
143 | async with aiofiles.tempfile.TemporaryDirectory() as d:
144 |     filename = os.path.join(d, "file.ext")
145 | ```
146 | 
147 | ### Writing tests for aiofiles
148 | 
149 | Real file IO can be mocked by patching `aiofiles.threadpool.sync_open`
150 | as desired. The return type also needs to be registered with the
151 | `aiofiles.threadpool.wrap` dispatcher:
152 | 
153 | ```python
154 | aiofiles.threadpool.wrap.register(mock.MagicMock)(
155 |     lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs)
156 | )
157 | 
158 | async def test_stuff():
159 |     write_data = 'data'
160 |     read_file_chunks = [
161 |         b'file chunks 1',
162 |         b'file chunks 2',
163 |         b'file chunks 3',
164 |         b'',
165 |     ]
166 |     file_chunks_iter = iter(read_file_chunks)
167 | 
168 |     mock_file_stream = mock.MagicMock(
169 |         read=lambda *args, **kwargs: next(file_chunks_iter)
170 |     )
171 | 
172 |     with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file_stream) as mock_open:
173 |         async with aiofiles.open('filename', 'w') as f:
174 |             await f.write(write_data)
175 |             assert await f.read() == b'file chunks 1'
176 | 
177 |         mock_file_stream.write.assert_called_once_with(write_data)
178 | ```
179 | 
180 | ### Contributing
181 | 
182 | Contributions are very welcome. Tests can be run with `tox`, please ensure
183 | the coverage at least stays the same before you submit a pull request.
184 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
  1 | [build-system]
  2 | requires = ["hatchling", "hatch-vcs"]
  3 | build-backend = "hatchling.build"
  4 | 
  5 | [project]
  6 | name = "aiofiles"
  7 | description = "File support for asyncio."
  8 | authors = [
  9 |     {name = "Tin Tvrtkovic", email = "tinchester@gmail.com"},
 10 | ]
 11 | requires-python = ">=3.9"
 12 | readme = "README.md"
 13 | license = {text = "Apache-2.0"}
 14 | classifiers = [
 15 |     "Development Status :: 5 - Production/Stable",
 16 |     "License :: OSI Approved :: Apache Software License",
 17 |     "Operating System :: OS Independent",
 18 |     "Programming Language :: Python :: 3.9",
 19 |     "Programming Language :: Python :: 3.10",
 20 |     "Programming Language :: Python :: 3.11",
 21 |     "Programming Language :: Python :: 3.12",
 22 |     "Programming Language :: Python :: 3.13",
 23 |     "Programming Language :: Python :: 3.14",
 24 |     "Programming Language :: Python :: Implementation :: CPython",
 25 |     "Programming Language :: Python :: Implementation :: PyPy",
 26 |     "Framework :: AsyncIO",
 27 | ]
 28 | dynamic = ["version"]
 29 | dependencies = []
 30 | 
 31 | [project.urls]
 32 | Changelog = "https://github.com/Tinche/aiofiles#history"
 33 | "Bug Tracker" = "https://github.com/Tinche/aiofiles/issues"
 34 | Repository = "https://github.com/Tinche/aiofiles"
 35 | 
 36 | [dependency-groups]
 37 | lint = [
 38 |     "mypy>=1.16.0",
 39 |     "ruff>=0.11.12",
 40 | ]
 41 | test = [
 42 |     "coverage>=7.8.2",
 43 |     "pytest>=8.3.5",
 44 |     "pytest-asyncio>=1.0.0",
 45 | ]
 46 | tox = [
 47 |     "tox>=4.26.0",
 48 |     "tox-uv>=1.26.0",
 49 | ]
 50 | 
 51 | [tool.uv.build-backend]
 52 | source-include = ["LICENSE"]
 53 | 
 54 | [tool.hatch.version]
 55 | source = "vcs"
 56 | raw-options = { local_scheme = "no-local-version" }
 57 | 
 58 | [tool.coverage.run]
 59 | parallel = true
 60 | source_pkgs = ["aiofiles"]
 61 | 
 62 | [tool.coverage.paths]
 63 | source = [
 64 |    "src",
 65 |    ".tox/*/lib/python*/site-packages",
 66 |    ".tox/*/lib/pypy*/site-packages",
 67 | ]
 68 | 
 69 | [tool.pytest.ini_options]
 70 | minversion = "8.3"
 71 | asyncio_mode = "auto"
 72 | asyncio_default_fixture_loop_scope = "function"
 73 | 
 74 | [tool.ruff]
 75 | indent-width = 4
 76 | line-length = 88
 77 | target-version = "py39"
 78 | 
 79 | [tool.ruff.format]
 80 | docstring-code-format = false
 81 | docstring-code-line-length = "dynamic"
 82 | indent-style = "space"
 83 | line-ending = "auto"
 84 | quote-style = "double"
 85 | skip-magic-trailing-comma = false
 86 | 
 87 | [tool.ruff.lint]
 88 | select = [
 89 |     # "A",  # flake8-builtins (A)
 90 |     # "ANN",  # flake8-annotations (ANN)
 91 |     # "ARG",  # flake8-unused-arguments (ARG)
 92 |     "ASYNC",  # flake8-async (ASYNC)
 93 |     "B",  # flake8-bugbear (B)
 94 |     "BLE",  # flake8-blind-except (BLE)
 95 |     "C4",  # flake8-comprehensions (C4)
 96 |     # "COM",  # flake8-commas (COM)
 97 |     # "D",  # pydocstyle (D)
 98 |     "E",  # Error (E)
 99 |     "EM",  # flake8-errmsg (EM)
100 |     "ERA",  # eradicate (ERA)
101 |     "F",  # Pyflakes (F)
102 |     # "FBT",  # flake8-boolean-trap (FBT)
103 |     "I",  # isort (I)
104 |     "ICN",  # flake8-import-conventions (ICN)
105 |     "ISC",  # flake8-implicit-str-concat (ISC)
106 |     # "N",  # pep8-naming (N)
107 |     "PIE",  # flake8-pie (PIE)
108 |     "PLE",  # Error (PLE)
109 |     # "PLR",  # Refactor (PLR)
110 |     "PLW",  # Warning (PLW)
111 |     # "PT",  # flake8-pytest-style (PT)
112 |     "PTH",  # flake8-use-pathlib (PTH)
113 |     "PYI",  # flake8-pyi (PYI)
114 |     "Q",  # flake8-quotes (Q)
115 |     "RET",  # flake8-return (RET)
116 |     "RSE",  # flake8-raise (RSE)
117 |     "S",  # flake8-bandit (S)
118 |     "SIM",  # flake8-simplify (SIM)
119 |     "T10",  # flake8-debugger (T10)
120 |     "T20",  # flake8-print (T20)
121 |     "TC",  # flake8-type-checking (TC)
122 |     "TID",  # flake8-tidy-imports (TID)
123 |     "TRY",  # tryceratops (TRY)
124 |     "UP",  # pyupgrade (UP)
125 |     "W",  # Warning (W)
126 |     "YTT"  # flake8-2020 (YTT)
127 | ]
128 | ignore = [
129 |     "COM812",
130 |     "ISC001",
131 | ]
132 | fixable = [
133 |     "COM",
134 |     "I"
135 | ]
136 | 
137 | [tool.ruff.lint.per-file-ignores]
138 | "__init__.py" = ["F401"]
139 | "src/**/*.py" = [
140 |     "TID252",  # https://docs.astral.sh/ruff/rules/relative-imports/
141 | ]
142 | "tests/**/*.py" = [
143 |     "ARG",
144 |     "ASYNC",
145 |     "BLE",
146 |     "PTH",
147 |     "S",
148 |     "SIM",
149 |     "T20"
150 | ]
151 | 


--------------------------------------------------------------------------------
/src/aiofiles/__init__.py:
--------------------------------------------------------------------------------
 1 | """Utilities for asyncio-friendly file handling."""
 2 | 
 3 | from . import tempfile
 4 | from .threadpool import (
 5 |     open,
 6 |     stderr,
 7 |     stderr_bytes,
 8 |     stdin,
 9 |     stdin_bytes,
10 |     stdout,
11 |     stdout_bytes,
12 | )
13 | 
14 | __all__ = [
15 |     "open",
16 |     "tempfile",
17 |     "stdin",
18 |     "stdout",
19 |     "stderr",
20 |     "stdin_bytes",
21 |     "stdout_bytes",
22 |     "stderr_bytes",
23 | ]
24 | 


--------------------------------------------------------------------------------
/src/aiofiles/base.py:
--------------------------------------------------------------------------------
 1 | from asyncio import get_running_loop
 2 | from collections.abc import Awaitable
 3 | from contextlib import AbstractAsyncContextManager
 4 | from functools import partial, wraps
 5 | 
 6 | 
 7 | def wrap(func):
 8 |     @wraps(func)
 9 |     async def run(*args, loop=None, executor=None, **kwargs):
10 |         if loop is None:
11 |             loop = get_running_loop()
12 |         pfunc = partial(func, *args, **kwargs)
13 |         return await loop.run_in_executor(executor, pfunc)
14 | 
15 |     return run
16 | 
17 | 
18 | class AsyncBase:
19 |     def __init__(self, file, loop, executor):
20 |         self._file = file
21 |         self._executor = executor
22 |         self._ref_loop = loop
23 | 
24 |     @property
25 |     def _loop(self):
26 |         return self._ref_loop or get_running_loop()
27 | 
28 |     def __aiter__(self):
29 |         """We are our own iterator."""
30 |         return self
31 | 
32 |     def __repr__(self):
33 |         return super().__repr__() + " wrapping " + repr(self._file)
34 | 
35 |     async def __anext__(self):
36 |         """Simulate normal file iteration."""
37 | 
38 |         if line := await self.readline():
39 |             return line
40 |         raise StopAsyncIteration
41 | 
42 | 
43 | class AsyncIndirectBase(AsyncBase):
44 |     def __init__(self, name, loop, executor, indirect):
45 |         self._indirect = indirect
46 |         self._name = name
47 |         super().__init__(None, loop, executor)
48 | 
49 |     @property
50 |     def _file(self):
51 |         return self._indirect()
52 | 
53 |     @_file.setter
54 |     def _file(self, v):
55 |         pass  # discard writes
56 | 
57 | 
58 | class AiofilesContextManager(Awaitable, AbstractAsyncContextManager):
59 |     """An adjusted async context manager for aiofiles."""
60 | 
61 |     __slots__ = ("_coro", "_obj")
62 | 
63 |     def __init__(self, coro):
64 |         self._coro = coro
65 |         self._obj = None
66 | 
67 |     def __await__(self):
68 |         if self._obj is None:
69 |             self._obj = yield from self._coro.__await__()
70 |         return self._obj
71 | 
72 |     async def __aenter__(self):
73 |         return await self
74 | 
75 |     async def __aexit__(self, exc_type, exc_val, exc_tb):
76 |         await get_running_loop().run_in_executor(
77 |             None, self._obj._file.__exit__, exc_type, exc_val, exc_tb
78 |         )
79 |         self._obj = None
80 | 


--------------------------------------------------------------------------------
/src/aiofiles/os.py:
--------------------------------------------------------------------------------
 1 | """Async executor versions of file functions from the os module."""
 2 | 
 3 | import os
 4 | 
 5 | from . import ospath as path
 6 | from .base import wrap
 7 | 
 8 | __all__ = [
 9 |     "path",
10 |     "stat",
11 |     "rename",
12 |     "renames",
13 |     "replace",
14 |     "remove",
15 |     "unlink",
16 |     "mkdir",
17 |     "makedirs",
18 |     "rmdir",
19 |     "removedirs",
20 |     "symlink",
21 |     "readlink",
22 |     "listdir",
23 |     "scandir",
24 |     "access",
25 |     "wrap",
26 |     "getcwd",
27 | ]
28 | 
29 | access = wrap(os.access)
30 | 
31 | getcwd = wrap(os.getcwd)
32 | 
33 | listdir = wrap(os.listdir)
34 | 
35 | makedirs = wrap(os.makedirs)
36 | mkdir = wrap(os.mkdir)
37 | 
38 | readlink = wrap(os.readlink)
39 | remove = wrap(os.remove)
40 | removedirs = wrap(os.removedirs)
41 | rename = wrap(os.rename)
42 | renames = wrap(os.renames)
43 | replace = wrap(os.replace)
44 | rmdir = wrap(os.rmdir)
45 | 
46 | scandir = wrap(os.scandir)
47 | stat = wrap(os.stat)
48 | symlink = wrap(os.symlink)
49 | 
50 | unlink = wrap(os.unlink)
51 | 
52 | 
53 | if hasattr(os, "link"):
54 |     __all__ += ["link"]
55 |     link = wrap(os.link)
56 | if hasattr(os, "sendfile"):
57 |     __all__ += ["sendfile"]
58 |     sendfile = wrap(os.sendfile)
59 | if hasattr(os, "statvfs"):
60 |     __all__ += ["statvfs"]
61 |     statvfs = wrap(os.statvfs)
62 | 


--------------------------------------------------------------------------------
/src/aiofiles/ospath.py:
--------------------------------------------------------------------------------
 1 | """Async executor versions of file functions from the os.path module."""
 2 | 
 3 | from os import path
 4 | 
 5 | from .base import wrap
 6 | 
 7 | __all__ = [
 8 |     "abspath",
 9 |     "getatime",
10 |     "getctime",
11 |     "getmtime",
12 |     "getsize",
13 |     "exists",
14 |     "isdir",
15 |     "isfile",
16 |     "islink",
17 |     "ismount",
18 |     "samefile",
19 |     "sameopenfile",
20 | ]
21 | 
22 | abspath = wrap(path.abspath)
23 | 
24 | getatime = wrap(path.getatime)
25 | getctime = wrap(path.getctime)
26 | getmtime = wrap(path.getmtime)
27 | getsize = wrap(path.getsize)
28 | 
29 | exists = wrap(path.exists)
30 | 
31 | isdir = wrap(path.isdir)
32 | isfile = wrap(path.isfile)
33 | islink = wrap(path.islink)
34 | ismount = wrap(path.ismount)
35 | 
36 | samefile = wrap(path.samefile)
37 | sameopenfile = wrap(path.sameopenfile)
38 | 


--------------------------------------------------------------------------------
/src/aiofiles/tempfile/__init__.py:
--------------------------------------------------------------------------------
  1 | import asyncio
  2 | import sys
  3 | from functools import partial, singledispatch
  4 | from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase
  5 | from tempfile import NamedTemporaryFile as syncNamedTemporaryFile
  6 | from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile
  7 | from tempfile import TemporaryDirectory as syncTemporaryDirectory
  8 | from tempfile import TemporaryFile as syncTemporaryFile
  9 | from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper
 10 | 
 11 | from ..base import AiofilesContextManager
 12 | from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
 13 | from ..threadpool.text import AsyncTextIOWrapper
 14 | from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
 15 | 
 16 | __all__ = [
 17 |     "NamedTemporaryFile",
 18 |     "TemporaryFile",
 19 |     "SpooledTemporaryFile",
 20 |     "TemporaryDirectory",
 21 | ]
 22 | 
 23 | 
 24 | # ================================================================
 25 | # Public methods for async open and return of temp file/directory
 26 | # objects with async interface
 27 | # ================================================================
 28 | if sys.version_info >= (3, 12):
 29 | 
 30 |     def NamedTemporaryFile(
 31 |         mode="w+b",
 32 |         buffering=-1,
 33 |         encoding=None,
 34 |         newline=None,
 35 |         suffix=None,
 36 |         prefix=None,
 37 |         dir=None,
 38 |         delete=True,
 39 |         delete_on_close=True,
 40 |         loop=None,
 41 |         executor=None,
 42 |     ):
 43 |         """Async open a named temporary file"""
 44 |         return AiofilesContextManager(
 45 |             _temporary_file(
 46 |                 named=True,
 47 |                 mode=mode,
 48 |                 buffering=buffering,
 49 |                 encoding=encoding,
 50 |                 newline=newline,
 51 |                 suffix=suffix,
 52 |                 prefix=prefix,
 53 |                 dir=dir,
 54 |                 delete=delete,
 55 |                 delete_on_close=delete_on_close,
 56 |                 loop=loop,
 57 |                 executor=executor,
 58 |             )
 59 |         )
 60 | 
 61 | else:
 62 | 
 63 |     def NamedTemporaryFile(
 64 |         mode="w+b",
 65 |         buffering=-1,
 66 |         encoding=None,
 67 |         newline=None,
 68 |         suffix=None,
 69 |         prefix=None,
 70 |         dir=None,
 71 |         delete=True,
 72 |         loop=None,
 73 |         executor=None,
 74 |     ):
 75 |         """Async open a named temporary file"""
 76 |         return AiofilesContextManager(
 77 |             _temporary_file(
 78 |                 named=True,
 79 |                 mode=mode,
 80 |                 buffering=buffering,
 81 |                 encoding=encoding,
 82 |                 newline=newline,
 83 |                 suffix=suffix,
 84 |                 prefix=prefix,
 85 |                 dir=dir,
 86 |                 delete=delete,
 87 |                 loop=loop,
 88 |                 executor=executor,
 89 |             )
 90 |         )
 91 | 
 92 | 
 93 | def TemporaryFile(
 94 |     mode="w+b",
 95 |     buffering=-1,
 96 |     encoding=None,
 97 |     newline=None,
 98 |     suffix=None,
 99 |     prefix=None,
100 |     dir=None,
101 |     loop=None,
102 |     executor=None,
103 | ):
104 |     """Async open an unnamed temporary file"""
105 |     return AiofilesContextManager(
106 |         _temporary_file(
107 |             named=False,
108 |             mode=mode,
109 |             buffering=buffering,
110 |             encoding=encoding,
111 |             newline=newline,
112 |             suffix=suffix,
113 |             prefix=prefix,
114 |             dir=dir,
115 |             loop=loop,
116 |             executor=executor,
117 |         )
118 |     )
119 | 
120 | 
121 | def SpooledTemporaryFile(
122 |     max_size=0,
123 |     mode="w+b",
124 |     buffering=-1,
125 |     encoding=None,
126 |     newline=None,
127 |     suffix=None,
128 |     prefix=None,
129 |     dir=None,
130 |     loop=None,
131 |     executor=None,
132 | ):
133 |     """Async open a spooled temporary file"""
134 |     return AiofilesContextManager(
135 |         _spooled_temporary_file(
136 |             max_size=max_size,
137 |             mode=mode,
138 |             buffering=buffering,
139 |             encoding=encoding,
140 |             newline=newline,
141 |             suffix=suffix,
142 |             prefix=prefix,
143 |             dir=dir,
144 |             loop=loop,
145 |             executor=executor,
146 |         )
147 |     )
148 | 
149 | 
150 | def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
151 |     """Async open a temporary directory"""
152 |     return AiofilesContextManagerTempDir(
153 |         _temporary_directory(
154 |             suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
155 |         )
156 |     )
157 | 
158 | 
159 | # =========================================================
160 | # Internal coroutines to open new temp files/directories
161 | # =========================================================
162 | if sys.version_info >= (3, 12):
163 | 
164 |     async def _temporary_file(
165 |         named=True,
166 |         mode="w+b",
167 |         buffering=-1,
168 |         encoding=None,
169 |         newline=None,
170 |         suffix=None,
171 |         prefix=None,
172 |         dir=None,
173 |         delete=True,
174 |         delete_on_close=True,
175 |         loop=None,
176 |         executor=None,
177 |         max_size=0,
178 |     ):
179 |         """Async method to open a temporary file with async interface"""
180 |         if loop is None:
181 |             loop = asyncio.get_running_loop()
182 | 
183 |         if named:
184 |             cb = partial(
185 |                 syncNamedTemporaryFile,
186 |                 mode=mode,
187 |                 buffering=buffering,
188 |                 encoding=encoding,
189 |                 newline=newline,
190 |                 suffix=suffix,
191 |                 prefix=prefix,
192 |                 dir=dir,
193 |                 delete=delete,
194 |                 delete_on_close=delete_on_close,
195 |             )
196 |         else:
197 |             cb = partial(
198 |                 syncTemporaryFile,
199 |                 mode=mode,
200 |                 buffering=buffering,
201 |                 encoding=encoding,
202 |                 newline=newline,
203 |                 suffix=suffix,
204 |                 prefix=prefix,
205 |                 dir=dir,
206 |             )
207 | 
208 |         f = await loop.run_in_executor(executor, cb)
209 | 
210 |         # Wrap based on type of underlying IO object
211 |         if type(f) is syncTemporaryFileWrapper:
212 |             # _TemporaryFileWrapper was used (named files)
213 |             result = wrap(f.file, f, loop=loop, executor=executor)
214 |             result._closer = f._closer
215 |             return result
216 |         # IO object was returned directly without wrapper
217 |         return wrap(f, f, loop=loop, executor=executor)
218 | 
219 | else:
220 | 
221 |     async def _temporary_file(
222 |         named=True,
223 |         mode="w+b",
224 |         buffering=-1,
225 |         encoding=None,
226 |         newline=None,
227 |         suffix=None,
228 |         prefix=None,
229 |         dir=None,
230 |         delete=True,
231 |         loop=None,
232 |         executor=None,
233 |         max_size=0,
234 |     ):
235 |         """Async method to open a temporary file with async interface"""
236 |         if loop is None:
237 |             loop = asyncio.get_running_loop()
238 | 
239 |         if named:
240 |             cb = partial(
241 |                 syncNamedTemporaryFile,
242 |                 mode=mode,
243 |                 buffering=buffering,
244 |                 encoding=encoding,
245 |                 newline=newline,
246 |                 suffix=suffix,
247 |                 prefix=prefix,
248 |                 dir=dir,
249 |                 delete=delete,
250 |             )
251 |         else:
252 |             cb = partial(
253 |                 syncTemporaryFile,
254 |                 mode=mode,
255 |                 buffering=buffering,
256 |                 encoding=encoding,
257 |                 newline=newline,
258 |                 suffix=suffix,
259 |                 prefix=prefix,
260 |                 dir=dir,
261 |             )
262 | 
263 |         f = await loop.run_in_executor(executor, cb)
264 | 
265 |         # Wrap based on type of underlying IO object
266 |         if type(f) is syncTemporaryFileWrapper:
267 |             # _TemporaryFileWrapper was used (named files)
268 |             result = wrap(f.file, f, loop=loop, executor=executor)
269 |             # add delete property
270 |             result.delete = f.delete
271 |             return result
272 |         # IO object was returned directly without wrapper
273 |         return wrap(f, f, loop=loop, executor=executor)
274 | 
275 | 
276 | async def _spooled_temporary_file(
277 |     max_size=0,
278 |     mode="w+b",
279 |     buffering=-1,
280 |     encoding=None,
281 |     newline=None,
282 |     suffix=None,
283 |     prefix=None,
284 |     dir=None,
285 |     loop=None,
286 |     executor=None,
287 | ):
288 |     """Open a spooled temporary file with async interface"""
289 |     if loop is None:
290 |         loop = asyncio.get_running_loop()
291 | 
292 |     cb = partial(
293 |         syncSpooledTemporaryFile,
294 |         max_size=max_size,
295 |         mode=mode,
296 |         buffering=buffering,
297 |         encoding=encoding,
298 |         newline=newline,
299 |         suffix=suffix,
300 |         prefix=prefix,
301 |         dir=dir,
302 |     )
303 | 
304 |     f = await loop.run_in_executor(executor, cb)
305 | 
306 |     # Single interface provided by SpooledTemporaryFile for all modes
307 |     return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
308 | 
309 | 
310 | async def _temporary_directory(
311 |     suffix=None, prefix=None, dir=None, loop=None, executor=None
312 | ):
313 |     """Async method to open a temporary directory with async interface"""
314 |     if loop is None:
315 |         loop = asyncio.get_running_loop()
316 | 
317 |     cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
318 |     f = await loop.run_in_executor(executor, cb)
319 | 
320 |     return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
321 | 
322 | 
323 | class AiofilesContextManagerTempDir(AiofilesContextManager):
324 |     """With returns the directory location, not the object (matching sync lib)"""
325 | 
326 |     async def __aenter__(self):
327 |         self._obj = await self._coro
328 |         return self._obj.name
329 | 
330 | 
331 | @singledispatch
332 | def wrap(base_io_obj, file, *, loop=None, executor=None):
333 |     """Wrap the object with interface based on type of underlying IO"""
334 | 
335 |     msg = f"Unsupported IO type: {base_io_obj}"
336 |     raise TypeError(msg)
337 | 
338 | 
339 | @wrap.register(TextIOBase)
340 | def _(base_io_obj, file, *, loop=None, executor=None):
341 |     return AsyncTextIOWrapper(file, loop=loop, executor=executor)
342 | 
343 | 
344 | @wrap.register(BufferedWriter)
345 | def _(base_io_obj, file, *, loop=None, executor=None):
346 |     return AsyncBufferedIOBase(file, loop=loop, executor=executor)
347 | 
348 | 
349 | @wrap.register(BufferedReader)
350 | @wrap.register(BufferedRandom)
351 | def _(base_io_obj, file, *, loop=None, executor=None):
352 |     return AsyncBufferedReader(file, loop=loop, executor=executor)
353 | 
354 | 
355 | @wrap.register(FileIO)
356 | def _(base_io_obj, file, *, loop=None, executor=None):
357 |     return AsyncFileIO(file, loop=loop, executor=executor)
358 | 


--------------------------------------------------------------------------------
/src/aiofiles/tempfile/temptypes.py:
--------------------------------------------------------------------------------
 1 | """Async wrappers for spooled temp files and temp directory objects"""
 2 | 
 3 | from functools import partial
 4 | 
 5 | from ..base import AsyncBase
 6 | from ..threadpool.utils import (
 7 |     cond_delegate_to_executor,
 8 |     delegate_to_executor,
 9 |     proxy_property_directly,
10 | )
11 | 
12 | 
13 | @delegate_to_executor("fileno", "rollover")
14 | @cond_delegate_to_executor(
15 |     "close",
16 |     "flush",
17 |     "isatty",
18 |     "read",
19 |     "readline",
20 |     "readlines",
21 |     "seek",
22 |     "tell",
23 |     "truncate",
24 | )
25 | @proxy_property_directly("closed", "encoding", "mode", "name", "newlines")
26 | class AsyncSpooledTemporaryFile(AsyncBase):
27 |     """Async wrapper for SpooledTemporaryFile class"""
28 | 
29 |     async def _check(self):
30 |         if self._file._rolled:
31 |             return
32 |         max_size = self._file._max_size
33 |         if max_size and self._file.tell() > max_size:
34 |             await self.rollover()
35 | 
36 |     async def write(self, s):
37 |         """Implementation to anticipate rollover"""
38 |         if self._file._rolled:
39 |             cb = partial(self._file.write, s)
40 |             return await self._loop.run_in_executor(self._executor, cb)
41 | 
42 |         file = self._file._file  # reference underlying base IO object
43 |         rv = file.write(s)
44 |         await self._check()
45 |         return rv
46 | 
47 |     async def writelines(self, iterable):
48 |         """Implementation to anticipate rollover"""
49 |         if self._file._rolled:
50 |             cb = partial(self._file.writelines, iterable)
51 |             return await self._loop.run_in_executor(self._executor, cb)
52 | 
53 |         file = self._file._file  # reference underlying base IO object
54 |         rv = file.writelines(iterable)
55 |         await self._check()
56 |         return rv
57 | 
58 | 
59 | @delegate_to_executor("cleanup")
60 | @proxy_property_directly("name")
61 | class AsyncTemporaryDirectory:
62 |     """Async wrapper for TemporaryDirectory class"""
63 | 
64 |     def __init__(self, file, loop, executor):
65 |         self._file = file
66 |         self._loop = loop
67 |         self._executor = executor
68 | 
69 |     async def close(self):
70 |         await self.cleanup()
71 | 


--------------------------------------------------------------------------------
/src/aiofiles/threadpool/__init__.py:
--------------------------------------------------------------------------------
  1 | """Handle files using a thread pool executor."""
  2 | 
  3 | import asyncio
  4 | import sys
  5 | from functools import partial, singledispatch
  6 | from io import (
  7 |     BufferedIOBase,
  8 |     BufferedRandom,
  9 |     BufferedReader,
 10 |     BufferedWriter,
 11 |     FileIO,
 12 |     TextIOBase,
 13 | )
 14 | 
 15 | from ..base import AiofilesContextManager
 16 | from .binary import (
 17 |     AsyncBufferedIOBase,
 18 |     AsyncBufferedReader,
 19 |     AsyncFileIO,
 20 |     AsyncIndirectBufferedIOBase,
 21 | )
 22 | from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper
 23 | 
 24 | sync_open = open
 25 | 
 26 | __all__ = (
 27 |     "open",
 28 |     "stdin",
 29 |     "stdout",
 30 |     "stderr",
 31 |     "stdin_bytes",
 32 |     "stdout_bytes",
 33 |     "stderr_bytes",
 34 | )
 35 | 
 36 | 
 37 | def open(
 38 |     file,
 39 |     mode="r",
 40 |     buffering=-1,
 41 |     encoding=None,
 42 |     errors=None,
 43 |     newline=None,
 44 |     closefd=True,
 45 |     opener=None,
 46 |     *,
 47 |     loop=None,
 48 |     executor=None,
 49 | ):
 50 |     return AiofilesContextManager(
 51 |         _open(
 52 |             file,
 53 |             mode=mode,
 54 |             buffering=buffering,
 55 |             encoding=encoding,
 56 |             errors=errors,
 57 |             newline=newline,
 58 |             closefd=closefd,
 59 |             opener=opener,
 60 |             loop=loop,
 61 |             executor=executor,
 62 |         )
 63 |     )
 64 | 
 65 | 
 66 | async def _open(
 67 |     file,
 68 |     mode="r",
 69 |     buffering=-1,
 70 |     encoding=None,
 71 |     errors=None,
 72 |     newline=None,
 73 |     closefd=True,
 74 |     opener=None,
 75 |     *,
 76 |     loop=None,
 77 |     executor=None,
 78 | ):
 79 |     """Open an asyncio file."""
 80 |     if loop is None:
 81 |         loop = asyncio.get_running_loop()
 82 |     cb = partial(
 83 |         sync_open,
 84 |         file,
 85 |         mode=mode,
 86 |         buffering=buffering,
 87 |         encoding=encoding,
 88 |         errors=errors,
 89 |         newline=newline,
 90 |         closefd=closefd,
 91 |         opener=opener,
 92 |     )
 93 |     f = await loop.run_in_executor(executor, cb)
 94 | 
 95 |     return wrap(f, loop=loop, executor=executor)
 96 | 
 97 | 
 98 | @singledispatch
 99 | def wrap(file, *, loop=None, executor=None):
100 |     msg = f"Unsupported io type: {file}."
101 |     raise TypeError(msg)
102 | 
103 | 
104 | @wrap.register(TextIOBase)
105 | def _(file, *, loop=None, executor=None):
106 |     return AsyncTextIOWrapper(file, loop=loop, executor=executor)
107 | 
108 | 
109 | @wrap.register(BufferedWriter)
110 | @wrap.register(BufferedIOBase)
111 | def _(file, *, loop=None, executor=None):
112 |     return AsyncBufferedIOBase(file, loop=loop, executor=executor)
113 | 
114 | 
115 | @wrap.register(BufferedReader)
116 | @wrap.register(BufferedRandom)
117 | def _(file, *, loop=None, executor=None):
118 |     return AsyncBufferedReader(file, loop=loop, executor=executor)
119 | 
120 | 
121 | @wrap.register(FileIO)
122 | def _(file, *, loop=None, executor=None):
123 |     return AsyncFileIO(file, loop=loop, executor=executor)
124 | 
125 | 
126 | stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin)
127 | stdout = AsyncTextIndirectIOWrapper(
128 |     "sys.stdout", None, None, indirect=lambda: sys.stdout
129 | )
130 | stderr = AsyncTextIndirectIOWrapper(
131 |     "sys.stderr", None, None, indirect=lambda: sys.stderr
132 | )
133 | stdin_bytes = AsyncIndirectBufferedIOBase(
134 |     "sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer
135 | )
136 | stdout_bytes = AsyncIndirectBufferedIOBase(
137 |     "sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer
138 | )
139 | stderr_bytes = AsyncIndirectBufferedIOBase(
140 |     "sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer
141 | )
142 | 


--------------------------------------------------------------------------------
/src/aiofiles/threadpool/binary.py:
--------------------------------------------------------------------------------
  1 | from ..base import AsyncBase, AsyncIndirectBase
  2 | from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
  3 | 
  4 | 
  5 | @delegate_to_executor(
  6 |     "close",
  7 |     "flush",
  8 |     "isatty",
  9 |     "read",
 10 |     "read1",
 11 |     "readinto",
 12 |     "readline",
 13 |     "readlines",
 14 |     "seek",
 15 |     "seekable",
 16 |     "tell",
 17 |     "truncate",
 18 |     "writable",
 19 |     "write",
 20 |     "writelines",
 21 | )
 22 | @proxy_method_directly("detach", "fileno", "readable")
 23 | @proxy_property_directly("closed", "raw", "name", "mode")
 24 | class AsyncBufferedIOBase(AsyncBase):
 25 |     """The asyncio executor version of io.BufferedWriter and BufferedIOBase."""
 26 | 
 27 | 
 28 | @delegate_to_executor("peek")
 29 | class AsyncBufferedReader(AsyncBufferedIOBase):
 30 |     """The asyncio executor version of io.BufferedReader and Random."""
 31 | 
 32 | 
 33 | @delegate_to_executor(
 34 |     "close",
 35 |     "flush",
 36 |     "isatty",
 37 |     "read",
 38 |     "readall",
 39 |     "readinto",
 40 |     "readline",
 41 |     "readlines",
 42 |     "seek",
 43 |     "seekable",
 44 |     "tell",
 45 |     "truncate",
 46 |     "writable",
 47 |     "write",
 48 |     "writelines",
 49 | )
 50 | @proxy_method_directly("fileno", "readable")
 51 | @proxy_property_directly("closed", "name", "mode")
 52 | class AsyncFileIO(AsyncBase):
 53 |     """The asyncio executor version of io.FileIO."""
 54 | 
 55 | 
 56 | @delegate_to_executor(
 57 |     "close",
 58 |     "flush",
 59 |     "isatty",
 60 |     "read",
 61 |     "read1",
 62 |     "readinto",
 63 |     "readline",
 64 |     "readlines",
 65 |     "seek",
 66 |     "seekable",
 67 |     "tell",
 68 |     "truncate",
 69 |     "writable",
 70 |     "write",
 71 |     "writelines",
 72 | )
 73 | @proxy_method_directly("detach", "fileno", "readable")
 74 | @proxy_property_directly("closed", "raw", "name", "mode")
 75 | class AsyncIndirectBufferedIOBase(AsyncIndirectBase):
 76 |     """The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase."""
 77 | 
 78 | 
 79 | @delegate_to_executor("peek")
 80 | class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase):
 81 |     """The indirect asyncio executor version of io.BufferedReader and Random."""
 82 | 
 83 | 
 84 | @delegate_to_executor(
 85 |     "close",
 86 |     "flush",
 87 |     "isatty",
 88 |     "read",
 89 |     "readall",
 90 |     "readinto",
 91 |     "readline",
 92 |     "readlines",
 93 |     "seek",
 94 |     "seekable",
 95 |     "tell",
 96 |     "truncate",
 97 |     "writable",
 98 |     "write",
 99 |     "writelines",
100 | )
101 | @proxy_method_directly("fileno", "readable")
102 | @proxy_property_directly("closed", "name", "mode")
103 | class AsyncIndirectFileIO(AsyncIndirectBase):
104 |     """The indirect asyncio executor version of io.FileIO."""
105 | 


--------------------------------------------------------------------------------
/src/aiofiles/threadpool/text.py:
--------------------------------------------------------------------------------
 1 | from ..base import AsyncBase, AsyncIndirectBase
 2 | from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
 3 | 
 4 | 
 5 | @delegate_to_executor(
 6 |     "close",
 7 |     "flush",
 8 |     "isatty",
 9 |     "read",
10 |     "readable",
11 |     "readline",
12 |     "readlines",
13 |     "seek",
14 |     "seekable",
15 |     "tell",
16 |     "truncate",
17 |     "write",
18 |     "writable",
19 |     "writelines",
20 | )
21 | @proxy_method_directly("detach", "fileno", "readable")
22 | @proxy_property_directly(
23 |     "buffer",
24 |     "closed",
25 |     "encoding",
26 |     "errors",
27 |     "line_buffering",
28 |     "newlines",
29 |     "name",
30 |     "mode",
31 | )
32 | class AsyncTextIOWrapper(AsyncBase):
33 |     """The asyncio executor version of io.TextIOWrapper."""
34 | 
35 | 
36 | @delegate_to_executor(
37 |     "close",
38 |     "flush",
39 |     "isatty",
40 |     "read",
41 |     "readable",
42 |     "readline",
43 |     "readlines",
44 |     "seek",
45 |     "seekable",
46 |     "tell",
47 |     "truncate",
48 |     "write",
49 |     "writable",
50 |     "writelines",
51 | )
52 | @proxy_method_directly("detach", "fileno", "readable")
53 | @proxy_property_directly(
54 |     "buffer",
55 |     "closed",
56 |     "encoding",
57 |     "errors",
58 |     "line_buffering",
59 |     "newlines",
60 |     "name",
61 |     "mode",
62 | )
63 | class AsyncTextIndirectIOWrapper(AsyncIndirectBase):
64 |     """The indirect asyncio executor version of io.TextIOWrapper."""
65 | 


--------------------------------------------------------------------------------
/src/aiofiles/threadpool/utils.py:
--------------------------------------------------------------------------------
 1 | import functools
 2 | 
 3 | 
 4 | def delegate_to_executor(*attrs):
 5 |     def cls_builder(cls):
 6 |         for attr_name in attrs:
 7 |             setattr(cls, attr_name, _make_delegate_method(attr_name))
 8 |         return cls
 9 | 
10 |     return cls_builder
11 | 
12 | 
13 | def proxy_method_directly(*attrs):
14 |     def cls_builder(cls):
15 |         for attr_name in attrs:
16 |             setattr(cls, attr_name, _make_proxy_method(attr_name))
17 |         return cls
18 | 
19 |     return cls_builder
20 | 
21 | 
22 | def proxy_property_directly(*attrs):
23 |     def cls_builder(cls):
24 |         for attr_name in attrs:
25 |             setattr(cls, attr_name, _make_proxy_property(attr_name))
26 |         return cls
27 | 
28 |     return cls_builder
29 | 
30 | 
31 | def cond_delegate_to_executor(*attrs):
32 |     def cls_builder(cls):
33 |         for attr_name in attrs:
34 |             setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
35 |         return cls
36 | 
37 |     return cls_builder
38 | 
39 | 
40 | def _make_delegate_method(attr_name):
41 |     async def method(self, *args, **kwargs):
42 |         cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
43 |         return await self._loop.run_in_executor(self._executor, cb)
44 | 
45 |     return method
46 | 
47 | 
48 | def _make_proxy_method(attr_name):
49 |     def method(self, *args, **kwargs):
50 |         return getattr(self._file, attr_name)(*args, **kwargs)
51 | 
52 |     return method
53 | 
54 | 
55 | def _make_proxy_property(attr_name):
56 |     def proxy_property(self):
57 |         return getattr(self._file, attr_name)
58 | 
59 |     return property(proxy_property)
60 | 
61 | 
62 | def _make_cond_delegate_method(attr_name):
63 |     """For spooled temp files, delegate only if rolled to file object"""
64 | 
65 |     async def method(self, *args, **kwargs):
66 |         if self._file._rolled:
67 |             cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
68 |             return await self._loop.run_in_executor(self._executor, cb)
69 |         return getattr(self._file, attr_name)(*args, **kwargs)
70 | 
71 |     return method
72 | 


--------------------------------------------------------------------------------
/tests/resources/multiline_file.txt:
--------------------------------------------------------------------------------
1 | line 1
2 | line 2
3 | line 3
4 | line 4


--------------------------------------------------------------------------------
/tests/resources/test_file1.txt:
--------------------------------------------------------------------------------
1 | 0123456789


--------------------------------------------------------------------------------
/tests/test_os.py:
--------------------------------------------------------------------------------
  1 | """Tests for asyncio's os module."""
  2 | 
  3 | import asyncio
  4 | import os
  5 | import platform
  6 | from os import stat
  7 | from os.path import dirname, exists, isdir, join
  8 | from pathlib import Path
  9 | 
 10 | import pytest
 11 | 
 12 | import aiofiles.os
 13 | 
 14 | 
 15 | async def test_stat():
 16 |     """Test the stat call."""
 17 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
 18 | 
 19 |     stat_res = await aiofiles.os.stat(filename)
 20 | 
 21 |     assert stat_res.st_size == 10
 22 | 
 23 | 
 24 | @pytest.mark.skipif(platform.system() == "Windows", reason="No statvfs on Windows")
 25 | async def test_statvfs():
 26 |     """Test the statvfs call."""
 27 | 
 28 |     statvfs_res = await aiofiles.os.statvfs("/")
 29 | 
 30 |     assert statvfs_res.f_bsize == os.statvfs("/").f_bsize
 31 | 
 32 | 
 33 | async def test_remove():
 34 |     """Test the remove call."""
 35 |     filename = join(dirname(__file__), "resources", "test_file2.txt")
 36 |     with open(filename, "w") as f:
 37 |         f.write("Test file for remove call")
 38 | 
 39 |     assert exists(filename)
 40 |     await aiofiles.os.remove(filename)
 41 |     assert exists(filename) is False
 42 | 
 43 | 
 44 | async def test_unlink():
 45 |     """Test the unlink call."""
 46 |     filename = join(dirname(__file__), "resources", "test_file2.txt")
 47 |     with open(filename, "w") as f:
 48 |         f.write("Test file for unlink call")
 49 | 
 50 |     assert exists(filename)
 51 |     await aiofiles.os.unlink(filename)
 52 |     assert exists(filename) is False
 53 | 
 54 | 
 55 | async def test_mkdir_and_rmdir():
 56 |     """Test the mkdir and rmdir call."""
 57 |     directory = join(dirname(__file__), "resources", "test_dir")
 58 |     await aiofiles.os.mkdir(directory)
 59 |     assert isdir(directory)
 60 |     await aiofiles.os.rmdir(directory)
 61 |     assert exists(directory) is False
 62 | 
 63 | 
 64 | async def test_rename():
 65 |     """Test the rename call."""
 66 |     old_filename = join(dirname(__file__), "resources", "test_file1.txt")
 67 |     new_filename = join(dirname(__file__), "resources", "test_file2.txt")
 68 |     await aiofiles.os.rename(old_filename, new_filename)
 69 |     assert exists(old_filename) is False and exists(new_filename)
 70 |     await aiofiles.os.rename(new_filename, old_filename)
 71 |     assert exists(old_filename) and exists(new_filename) is False
 72 | 
 73 | 
 74 | async def test_renames():
 75 |     """Test the renames call."""
 76 |     old_filename = join(dirname(__file__), "resources", "test_file1.txt")
 77 |     new_filename = join(
 78 |         dirname(__file__), "resources", "subdirectory", "test_file2.txt"
 79 |     )
 80 |     await aiofiles.os.renames(old_filename, new_filename)
 81 |     assert exists(old_filename) is False and exists(new_filename)
 82 |     await aiofiles.os.renames(new_filename, old_filename)
 83 |     assert (
 84 |         exists(old_filename)
 85 |         and exists(new_filename) is False
 86 |         and exists(dirname(new_filename)) is False
 87 |     )
 88 | 
 89 | 
 90 | async def test_replace():
 91 |     """Test the replace call."""
 92 |     old_filename = join(dirname(__file__), "resources", "test_file1.txt")
 93 |     new_filename = join(dirname(__file__), "resources", "test_file2.txt")
 94 | 
 95 |     await aiofiles.os.replace(old_filename, new_filename)
 96 |     assert exists(old_filename) is False and exists(new_filename)
 97 |     await aiofiles.os.replace(new_filename, old_filename)
 98 |     assert exists(old_filename) and exists(new_filename) is False
 99 | 
100 |     with open(new_filename, "w") as f:
101 |         f.write("Test file")
102 |     assert exists(old_filename) and exists(new_filename)
103 | 
104 |     await aiofiles.os.replace(old_filename, new_filename)
105 |     assert exists(old_filename) is False and exists(new_filename)
106 |     await aiofiles.os.replace(new_filename, old_filename)
107 |     assert exists(old_filename) and exists(new_filename) is False
108 | 
109 | 
110 | @pytest.mark.skipif(
111 |     "2.4" < platform.release() < "2.6.33",
112 |     reason="sendfile() syscall doesn't allow file->file",
113 | )
114 | @pytest.mark.skipif(
115 |     platform.system() in ("Darwin", "Windows"),
116 |     reason="sendfile() doesn't work on mac and Win",
117 | )
118 | async def test_sendfile_file(tmpdir):
119 |     """Test the sendfile functionality, file-to-file."""
120 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
121 |     tmp_filename = tmpdir.join("tmp.bin")
122 | 
123 |     with open(filename) as f:
124 |         contents = f.read()
125 | 
126 |     input_file = await aiofiles.open(filename)
127 |     output_file = await aiofiles.open(str(tmp_filename), mode="w+")
128 | 
129 |     size = (await aiofiles.os.stat(filename)).st_size
130 | 
131 |     input_fd = input_file.fileno()
132 |     output_fd = output_file.fileno()
133 | 
134 |     await aiofiles.os.sendfile(output_fd, input_fd, 0, size)
135 | 
136 |     await output_file.seek(0)
137 | 
138 |     actual_contents = await output_file.read()
139 |     actual_size = (await aiofiles.os.stat(str(tmp_filename))).st_size
140 | 
141 |     assert contents == actual_contents
142 |     assert size == actual_size
143 | 
144 | 
145 | @pytest.mark.skipif(
146 |     platform.system() in ("Windows"), reason="sendfile() doesn't work on Win"
147 | )
148 | async def test_sendfile_socket(unused_tcp_port):
149 |     """Test the sendfile functionality, file-to-socket."""
150 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
151 | 
152 |     with open(filename, mode="rb") as f:
153 |         contents = f.read()
154 | 
155 |     async def serve_file(_, writer):
156 |         out_fd = writer.transport.get_extra_info("socket").fileno()
157 |         size = (await aiofiles.os.stat(filename)).st_size
158 |         in_file = await aiofiles.open(filename)
159 |         try:
160 |             in_fd = in_file.fileno()
161 |             await aiofiles.os.sendfile(out_fd, in_fd, 0, size)
162 |         finally:
163 |             await in_file.close()
164 |             await writer.drain()
165 |             writer.close()
166 | 
167 |     server = await asyncio.start_server(serve_file, port=unused_tcp_port)
168 | 
169 |     reader, writer = await asyncio.open_connection("127.0.0.1", unused_tcp_port)
170 |     actual_contents = await reader.read()
171 |     writer.close()
172 | 
173 |     assert contents == actual_contents
174 |     server.close()
175 | 
176 |     await server.wait_closed()
177 | 
178 | 
179 | async def test_exists():
180 |     """Test path.exists call."""
181 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
182 |     result = await aiofiles.os.path.exists(filename)
183 |     assert result
184 | 
185 | 
186 | async def test_isfile():
187 |     """Test path.isfile call."""
188 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
189 |     result = await aiofiles.os.path.isfile(filename)
190 |     assert result
191 | 
192 | 
193 | async def test_isdir():
194 |     """Test path.isdir call."""
195 |     filename = join(dirname(__file__), "resources")
196 |     result = await aiofiles.os.path.isdir(filename)
197 |     assert result
198 | 
199 | 
200 | async def test_islink():
201 |     """Test the path.islink call."""
202 |     src_filename = join(dirname(__file__), "resources", "test_file1.txt")
203 |     dst_filename = join(dirname(__file__), "resources", "test_file2.txt")
204 |     await aiofiles.os.symlink(src_filename, dst_filename)
205 |     assert await aiofiles.os.path.islink(dst_filename)
206 |     await aiofiles.os.remove(dst_filename)
207 | 
208 | 
209 | async def test_ismount():
210 |     """Test the path.ismount call."""
211 |     filename = join(dirname(__file__), "resources")
212 |     assert not await aiofiles.os.path.ismount(filename)
213 |     assert await aiofiles.os.path.ismount("/")
214 | 
215 | 
216 | async def test_getsize():
217 |     """Test path.getsize call."""
218 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
219 |     result = await aiofiles.os.path.getsize(filename)
220 |     assert result == 10
221 | 
222 | 
223 | async def test_samefile():
224 |     """Test path.samefile call."""
225 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
226 |     result = await aiofiles.os.path.samefile(filename, filename)
227 |     assert result
228 | 
229 | 
230 | async def test_sameopenfile():
231 |     """Test path.samefile call."""
232 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
233 |     result = await aiofiles.os.path.samefile(filename, filename)
234 |     assert result
235 | 
236 | 
237 | async def test_getmtime():
238 |     """Test path.getmtime call."""
239 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
240 |     result = await aiofiles.os.path.getmtime(filename)
241 |     assert result
242 | 
243 | 
244 | async def test_getatime():
245 |     """Test path.getatime call."""
246 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
247 |     result = await aiofiles.os.path.getatime(filename)
248 |     assert result
249 | 
250 | 
251 | async def test_getctime():
252 |     """Test path. call."""
253 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
254 |     result = await aiofiles.os.path.getctime(filename)
255 |     assert result
256 | 
257 | 
258 | async def test_link():
259 |     """Test the link call."""
260 |     src_filename = join(dirname(__file__), "resources", "test_file1.txt")
261 |     dst_filename = join(dirname(__file__), "resources", "test_file2.txt")
262 |     initial_src_nlink = stat(src_filename).st_nlink
263 |     await aiofiles.os.link(src_filename, dst_filename)
264 |     assert (
265 |         exists(src_filename)
266 |         and exists(dst_filename)
267 |         and (stat(src_filename).st_ino == stat(dst_filename).st_ino)
268 |         and (stat(src_filename).st_nlink == initial_src_nlink + 1)
269 |         and (stat(dst_filename).st_nlink == 2)
270 |     )
271 |     await aiofiles.os.remove(dst_filename)
272 |     assert (
273 |         exists(src_filename)
274 |         and exists(dst_filename) is False
275 |         and (stat(src_filename).st_nlink == initial_src_nlink)
276 |     )
277 | 
278 | 
279 | async def test_symlink():
280 |     """Test the symlink call."""
281 |     src_filename = join(dirname(__file__), "resources", "test_file1.txt")
282 |     dst_filename = join(dirname(__file__), "resources", "test_file2.txt")
283 |     await aiofiles.os.symlink(src_filename, dst_filename)
284 |     assert (
285 |         exists(src_filename)
286 |         and exists(dst_filename)
287 |         and stat(src_filename).st_ino == stat(dst_filename).st_ino
288 |     )
289 |     await aiofiles.os.remove(dst_filename)
290 |     assert exists(src_filename) and exists(dst_filename) is False
291 | 
292 | 
293 | @pytest.mark.skipif(
294 |     platform.system() == "Windows", reason="Doesn't work on Win properly"
295 | )
296 | async def test_readlink():
297 |     """Test the readlink call."""
298 |     src_filename = join(dirname(__file__), "resources", "test_file1.txt")
299 |     dst_filename = join(dirname(__file__), "resources", "test_file2.txt")
300 |     await aiofiles.os.symlink(src_filename, dst_filename)
301 |     symlinked_path = await aiofiles.os.readlink(dst_filename)
302 |     assert src_filename == symlinked_path
303 |     await aiofiles.os.remove(dst_filename)
304 | 
305 | 
306 | async def test_listdir_empty_dir():
307 |     """Test the listdir call when the dir is empty."""
308 |     directory = join(dirname(__file__), "resources", "empty_dir")
309 |     await aiofiles.os.mkdir(directory)
310 |     dir_list = await aiofiles.os.listdir(directory)
311 |     assert dir_list == []
312 |     await aiofiles.os.rmdir(directory)
313 | 
314 | 
315 | async def test_listdir_dir_with_only_one_file():
316 |     """Test the listdir call when the dir has one file."""
317 |     some_dir = join(dirname(__file__), "resources", "some_dir")
318 |     some_file = join(some_dir, "some_file.txt")
319 |     await aiofiles.os.mkdir(some_dir)
320 |     with open(some_file, "w") as f:
321 |         f.write("Test file")
322 |     dir_list = await aiofiles.os.listdir(some_dir)
323 |     assert "some_file.txt" in dir_list
324 |     await aiofiles.os.remove(some_file)
325 |     await aiofiles.os.rmdir(some_dir)
326 | 
327 | 
328 | async def test_listdir_dir_with_only_one_dir():
329 |     """Test the listdir call when the dir has one dir."""
330 |     some_dir = join(dirname(__file__), "resources", "some_dir")
331 |     other_dir = join(some_dir, "other_dir")
332 |     await aiofiles.os.mkdir(some_dir)
333 |     await aiofiles.os.mkdir(other_dir)
334 |     dir_list = await aiofiles.os.listdir(some_dir)
335 |     assert "other_dir" in dir_list
336 |     await aiofiles.os.rmdir(other_dir)
337 |     await aiofiles.os.rmdir(some_dir)
338 | 
339 | 
340 | async def test_listdir_dir_with_multiple_files():
341 |     """Test the listdir call when the dir has multiple files."""
342 |     some_dir = join(dirname(__file__), "resources", "some_dir")
343 |     some_file = join(some_dir, "some_file.txt")
344 |     other_file = join(some_dir, "other_file.txt")
345 |     await aiofiles.os.mkdir(some_dir)
346 |     with open(some_file, "w") as f:
347 |         f.write("Test file")
348 |     with open(other_file, "w") as f:
349 |         f.write("Test file")
350 |     dir_list = await aiofiles.os.listdir(some_dir)
351 |     assert "some_file.txt" in dir_list
352 |     assert "other_file.txt" in dir_list
353 |     await aiofiles.os.remove(some_file)
354 |     await aiofiles.os.remove(other_file)
355 |     await aiofiles.os.rmdir(some_dir)
356 | 
357 | 
358 | async def test_listdir_dir_with_a_file_and_a_dir():
359 |     """Test the listdir call when the dir has files and other dirs."""
360 |     some_dir = join(dirname(__file__), "resources", "some_dir")
361 |     other_dir = join(some_dir, "other_dir")
362 |     some_file = join(some_dir, "some_file.txt")
363 |     await aiofiles.os.mkdir(some_dir)
364 |     await aiofiles.os.mkdir(other_dir)
365 |     with open(some_file, "w") as f:
366 |         f.write("Test file")
367 |     dir_list = await aiofiles.os.listdir(some_dir)
368 |     assert "some_file.txt" in dir_list
369 |     assert "other_dir" in dir_list
370 |     await aiofiles.os.remove(some_file)
371 |     await aiofiles.os.rmdir(other_dir)
372 |     await aiofiles.os.rmdir(some_dir)
373 | 
374 | 
375 | async def test_listdir_non_existing_dir():
376 |     """Test the listdir call when the dir doesn't exist."""
377 |     some_dir = join(dirname(__file__), "resources", "some_dir")
378 |     with pytest.raises(FileNotFoundError):
379 |         await aiofiles.os.listdir(some_dir)
380 | 
381 | 
382 | async def test_scantdir_empty_dir():
383 |     """Test the scandir call when the dir is empty."""
384 |     empty_dir = join(dirname(__file__), "resources", "empty_dir")
385 |     await aiofiles.os.mkdir(empty_dir)
386 |     dir_iterator = await aiofiles.os.scandir(empty_dir)
387 |     dir_list = []
388 |     for dir_entity in dir_iterator:
389 |         dir_list.append(dir_entity)
390 |     assert dir_list == []
391 |     await aiofiles.os.rmdir(empty_dir)
392 | 
393 | 
394 | async def test_scandir_dir_with_only_one_file():
395 |     """Test the scandir call when the dir has one file."""
396 |     some_dir = join(dirname(__file__), "resources", "some_dir")
397 |     some_file = join(some_dir, "some_file.txt")
398 |     await aiofiles.os.mkdir(some_dir)
399 |     with open(some_file, "w") as f:
400 |         f.write("Test file")
401 |     dir_iterator = await aiofiles.os.scandir(some_dir)
402 |     some_file_entity = next(dir_iterator)
403 |     assert some_file_entity.name == "some_file.txt"
404 |     await aiofiles.os.remove(some_file)
405 |     await aiofiles.os.rmdir(some_dir)
406 | 
407 | 
408 | async def test_scandir_dir_with_only_one_dir():
409 |     """Test the scandir call when the dir has one dir."""
410 |     some_dir = join(dirname(__file__), "resources", "some_dir")
411 |     other_dir = join(some_dir, "other_dir")
412 |     await aiofiles.os.mkdir(some_dir)
413 |     await aiofiles.os.mkdir(other_dir)
414 |     dir_iterator = await aiofiles.os.scandir(some_dir)
415 |     other_dir_entity = next(dir_iterator)
416 |     assert other_dir_entity.name == "other_dir"
417 |     await aiofiles.os.rmdir(other_dir)
418 |     await aiofiles.os.rmdir(some_dir)
419 | 
420 | 
421 | async def test_scandir_non_existing_dir():
422 |     """Test the scandir call when the dir doesn't exist."""
423 |     some_dir = join(dirname(__file__), "resources", "some_dir")
424 |     with pytest.raises(FileNotFoundError):
425 |         await aiofiles.os.scandir(some_dir)
426 | 
427 | 
428 | @pytest.mark.skipif(platform.system() == "Windows", reason="Doesn't work on Win")
429 | async def test_access():
430 |     temp_file = Path(__file__).parent.joinpath("resources", "os_access_temp.txt")
431 |     temp_dir = Path(__file__).parent.joinpath("resources", "os_access_temp")
432 | 
433 |     # prepare
434 |     if temp_file.exists():
435 |         os.remove(temp_file)
436 |     assert not temp_file.exists()
437 |     temp_file.touch()
438 | 
439 |     if temp_dir.exists():
440 |         os.rmdir(temp_dir)
441 |     assert not temp_dir.exists()
442 |     os.mkdir(temp_dir)
443 | 
444 |     data = [
445 |         # full access
446 |         [0o777, os.F_OK, True],
447 |         [0o777, os.R_OK, True],
448 |         [0o777, os.W_OK, True],
449 |         [0o777, os.X_OK, True],
450 |         # chmod -x
451 |         [0o666, os.F_OK, True],
452 |         [0o666, os.R_OK, True],
453 |         [0o666, os.W_OK, True],
454 |         [0o666, os.X_OK, False],
455 |         # chmod -w
456 |         [0o444, os.F_OK, True],
457 |         [0o444, os.R_OK, True],
458 |         [0o444, os.W_OK, False],
459 |         [0o444, os.X_OK, False],
460 |         # chmod -r
461 |         [0o000, os.F_OK, True],
462 |         [0o000, os.R_OK, False],
463 |         [0o000, os.W_OK, False],
464 |         [0o000, os.X_OK, False],
465 |     ]
466 |     for ch, mode, access in data:
467 |         print(f"mode:{mode}, access:{access}")
468 |         temp_file.chmod(ch)
469 |         temp_dir.chmod(ch)
470 |         assert await aiofiles.os.access(temp_file, mode) == access
471 |         assert await aiofiles.os.access(temp_dir, mode) == access
472 | 
473 |     # not exists
474 |     os.remove(temp_file)
475 |     os.rmdir(temp_dir)
476 |     for mode in [os.F_OK, os.R_OK, os.W_OK, os.X_OK]:
477 |         print(f"mode:{mode}")
478 |         assert not await aiofiles.os.access(temp_file, mode)
479 |         assert not await aiofiles.os.access(temp_dir, mode)
480 | 
481 | 
482 | async def test_getcwd():
483 |     """Test the getcwd call."""
484 |     cwd = await aiofiles.os.getcwd()
485 |     assert cwd == os.getcwd()
486 | 
487 | 
488 | async def test_abspath_given_abspath():
489 |     """Test the abspath call with an absolute path."""
490 |     filename = join(dirname(__file__), "resources", "test_file1.txt")
491 |     file_abs_path = await aiofiles.os.path.abspath(filename)
492 |     assert file_abs_path == filename
493 | 
494 | 
495 | async def test_abspath():
496 |     """Test the abspath call."""
497 |     relative_filename = "./tests/resources/test_file1.txt"
498 |     abs_filename = join(dirname(__file__), "resources", "test_file1.txt")
499 |     result = await aiofiles.os.path.abspath(relative_filename)
500 |     assert result == abs_filename
501 | 


--------------------------------------------------------------------------------
/tests/test_simple.py:
--------------------------------------------------------------------------------
 1 | """Simple tests verifying basic functionality."""
 2 | 
 3 | import asyncio
 4 | 
 5 | from aiofiles import threadpool
 6 | 
 7 | 
 8 | async def test_serve_small_bin_file_sync(tmpdir, unused_tcp_port):
 9 |     """Fire up a small simple file server, and fetch a file.
10 | 
11 |     The file is read into memory synchronously, so this test doesn't actually
12 |     test anything except the general test concept.
13 |     """
14 |     # First we'll write a small file.
15 |     filename = "test.bin"
16 |     file_content = b"0123456789"
17 |     file = tmpdir.join(filename)
18 |     file.write_binary(file_content)
19 | 
20 |     async def serve_file(reader, writer):
21 |         full_filename = str(file)
22 |         with open(full_filename, "rb") as f:
23 |             writer.write(f.read())
24 |         writer.close()
25 | 
26 |     server = await asyncio.start_server(serve_file, port=unused_tcp_port)
27 | 
28 |     reader, _ = await asyncio.open_connection(host="localhost", port=unused_tcp_port)
29 |     payload = await reader.read()
30 | 
31 |     assert payload == file_content
32 | 
33 |     server.close()
34 |     await server.wait_closed()
35 | 
36 | 
37 | async def test_serve_small_bin_file(tmpdir, unused_tcp_port):
38 |     """Fire up a small simple file server, and fetch a file."""
39 |     # First we'll write a small file.
40 |     filename = "test.bin"
41 |     file_content = b"0123456789"
42 |     file = tmpdir.join(filename)
43 |     file.write_binary(file_content)
44 | 
45 |     async def serve_file(reader, writer):
46 |         full_filename = str(file)
47 |         f = await threadpool.open(full_filename, mode="rb")
48 |         writer.write(await f.read())
49 |         await f.close()
50 |         writer.close()
51 | 
52 |     server = await asyncio.start_server(serve_file, port=unused_tcp_port)
53 | 
54 |     reader, _ = await asyncio.open_connection(host="localhost", port=unused_tcp_port)
55 |     payload = await reader.read()
56 | 
57 |     assert payload == file_content
58 | 
59 |     server.close()
60 |     await server.wait_closed()
61 | 


--------------------------------------------------------------------------------
/tests/test_stdio.py:
--------------------------------------------------------------------------------
 1 | import pytest
 2 | 
 3 | from aiofiles import stderr, stderr_bytes, stdin, stdin_bytes, stdout, stdout_bytes
 4 | 
 5 | 
 6 | async def test_stdio(capsys):
 7 |     await stdout.write("hello")
 8 |     await stderr.write("world")
 9 |     out, err = capsys.readouterr()
10 |     assert out == "hello"
11 |     assert err == "world"
12 |     with pytest.raises(OSError):
13 |         await stdin.read()
14 | 
15 | 
16 | async def test_stdio_bytes(capsysbinary):
17 |     await stdout_bytes.write(b"hello")
18 |     await stderr_bytes.write(b"world")
19 |     out, err = capsysbinary.readouterr()
20 |     assert out == b"hello"
21 |     assert err == b"world"
22 |     with pytest.raises(OSError):
23 |         await stdin_bytes.read()
24 | 


--------------------------------------------------------------------------------
/tests/test_tempfile.py:
--------------------------------------------------------------------------------
  1 | import io
  2 | import os
  3 | import platform
  4 | import sys
  5 | 
  6 | import pytest
  7 | 
  8 | from aiofiles import tempfile
  9 | 
 10 | 
 11 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"])
 12 | async def test_temporary_file(mode):
 13 |     """Test temporary file."""
 14 |     data = b"Hello World!\n" if "b" in mode else "Hello World!\n"
 15 | 
 16 |     async with tempfile.TemporaryFile(mode=mode) as f:
 17 |         for _ in range(3):
 18 |             await f.write(data)
 19 | 
 20 |         await f.flush()
 21 |         await f.seek(0)
 22 | 
 23 |         async for line in f:
 24 |             assert line == data
 25 | 
 26 | 
 27 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"])
 28 | @pytest.mark.skipif(
 29 |     sys.version_info >= (3, 12),
 30 |     reason=("3.12+ doesn't support tempfile.NamedTemporaryFile.delete"),
 31 | )
 32 | async def test_named_temporary_file(mode):
 33 |     data = b"Hello World!" if "b" in mode else "Hello World!"
 34 |     filename = None
 35 | 
 36 |     async with tempfile.NamedTemporaryFile(mode=mode) as f:
 37 |         await f.write(data)
 38 |         await f.flush()
 39 |         await f.seek(0)
 40 |         assert await f.read() == data
 41 | 
 42 |         filename = f.name
 43 |         assert os.path.exists(filename)
 44 |         assert os.path.isfile(filename)
 45 |         assert f.delete
 46 | 
 47 |     assert not os.path.exists(filename)
 48 | 
 49 | 
 50 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"])
 51 | @pytest.mark.skipif(
 52 |     sys.version_info < (3, 12),
 53 |     reason=("3.12+ doesn't support tempfile.NamedTemporaryFile.delete"),
 54 | )
 55 | async def test_named_temporary_file_312(mode):
 56 |     data = b"Hello World!" if "b" in mode else "Hello World!"
 57 |     filename = None
 58 | 
 59 |     async with tempfile.NamedTemporaryFile(mode=mode) as f:
 60 |         await f.write(data)
 61 |         await f.flush()
 62 |         await f.seek(0)
 63 |         assert await f.read() == data
 64 | 
 65 |         filename = f.name
 66 |         assert os.path.exists(filename)
 67 |         assert os.path.isfile(filename)
 68 | 
 69 |     assert not os.path.exists(filename)
 70 | 
 71 | 
 72 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"])
 73 | @pytest.mark.skipif(
 74 |     sys.version_info < (3, 12), reason=("3.12+ supports delete_on_close")
 75 | )
 76 | async def test_named_temporary_delete_on_close(mode):
 77 |     data = b"Hello World!" if "b" in mode else "Hello World!"
 78 |     filename = None
 79 | 
 80 |     async with tempfile.NamedTemporaryFile(mode=mode, delete_on_close=True) as f:
 81 |         await f.write(data)
 82 |         await f.flush()
 83 |         await f.close()
 84 | 
 85 |         filename = f.name
 86 |         assert not os.path.exists(filename)
 87 | 
 88 |     async with tempfile.NamedTemporaryFile(mode=mode, delete_on_close=False) as f:
 89 |         await f.write(data)
 90 |         await f.flush()
 91 |         await f.close()
 92 | 
 93 |         filename = f.name
 94 |         assert os.path.exists(filename)
 95 | 
 96 |     assert not os.path.exists(filename)
 97 | 
 98 | 
 99 | @pytest.mark.parametrize("mode", ["r+", "w+", "rb+", "wb+"])
100 | async def test_spooled_temporary_file(mode):
101 |     """Test spooled temporary file."""
102 |     data = b"Hello World!" if "b" in mode else "Hello World!"
103 | 
104 |     async with tempfile.SpooledTemporaryFile(max_size=len(data) + 1, mode=mode) as f:
105 |         await f.write(data)
106 |         await f.flush()
107 |         if "b" in mode:
108 |             assert type(f._file._file) is io.BytesIO
109 | 
110 |         await f.write(data)
111 |         await f.flush()
112 |         if "b" in mode:
113 |             assert type(f._file._file) is not io.BytesIO
114 | 
115 |         await f.seek(0)
116 |         assert await f.read() == data + data
117 | 
118 | 
119 | @pytest.mark.skipif(
120 |     platform.system() == "Windows", reason="Doesn't work on Win properly"
121 | )
122 | @pytest.mark.parametrize(
123 |     "test_string, newlines", [("LF\n", "\n"), ("CRLF\r\n", "\r\n")]
124 | )
125 | async def test_spooled_temporary_file_newlines(test_string, newlines):
126 |     """
127 |     Test `newlines` property in spooled temporary file.
128 |     issue https://github.com/Tinche/aiofiles/issues/118
129 |     """
130 | 
131 |     async with tempfile.SpooledTemporaryFile(mode="w+") as f:
132 |         await f.write(test_string)
133 |         await f.flush()
134 |         await f.seek(0)
135 | 
136 |         assert f.newlines is None
137 | 
138 |         await f.read()
139 | 
140 |         assert f.newlines == newlines
141 | 
142 | 
143 | @pytest.mark.parametrize("prefix, suffix", [("a", "b"), ("c", "d"), ("e", "f")])
144 | async def test_temporary_directory(prefix, suffix, tmp_path):
145 |     """Test temporary directory."""
146 |     dir_path = None
147 | 
148 |     async with tempfile.TemporaryDirectory(
149 |         suffix=suffix, prefix=prefix, dir=tmp_path
150 |     ) as d:
151 |         dir_path = d
152 |         assert os.path.exists(dir_path)
153 |         assert os.path.isdir(dir_path)
154 |         assert d[-1] == suffix
155 |         assert d.split(os.sep)[-1][0] == prefix
156 |     assert not os.path.exists(dir_path)
157 | 


--------------------------------------------------------------------------------
/tests/threadpool/test_binary.py:
--------------------------------------------------------------------------------
  1 | """PEP 0492/Python 3.5+ tests for binary files."""
  2 | 
  3 | import io
  4 | from os.path import dirname, join
  5 | 
  6 | import pytest
  7 | 
  8 | from aiofiles.threadpool import open as aioopen
  9 | 
 10 | 
 11 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
 12 | @pytest.mark.parametrize("buffering", [-1, 0])
 13 | async def test_simple_iteration(mode, buffering):
 14 |     """Test iterating over lines from a file."""
 15 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
 16 | 
 17 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
 18 |         # Append mode needs us to seek.
 19 |         await file.seek(0)
 20 | 
 21 |         counter = 1
 22 |         # The old iteration pattern:
 23 |         while True:
 24 |             line = await file.readline()
 25 |             if not line:
 26 |                 break
 27 |             assert line.strip() == b"line " + str(counter).encode()
 28 |             counter += 1
 29 | 
 30 |         counter = 1
 31 |         await file.seek(0)
 32 |         # The new iteration pattern:
 33 |         async for line in file:
 34 |             assert line.strip() == b"line " + str(counter).encode()
 35 |             counter += 1
 36 | 
 37 |     assert file.closed
 38 | 
 39 | 
 40 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
 41 | @pytest.mark.parametrize("buffering", [-1, 0])
 42 | async def test_simple_readlines(mode, buffering):
 43 |     """Test the readlines functionality."""
 44 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
 45 | 
 46 |     with open(filename, mode="rb") as f:
 47 |         expected = f.readlines()
 48 | 
 49 |     async with aioopen(str(filename), mode=mode) as file:
 50 |         # Append mode needs us to seek.
 51 |         await file.seek(0)
 52 | 
 53 |         actual = await file.readlines()
 54 | 
 55 |     assert actual == expected
 56 | 
 57 | 
 58 | @pytest.mark.parametrize("mode", ["rb+", "wb", "ab"])
 59 | @pytest.mark.parametrize("buffering", [-1, 0])
 60 | async def test_simple_flush(mode, buffering, tmpdir):
 61 |     """Test flushing to a file."""
 62 |     filename = "file.bin"
 63 | 
 64 |     full_file = tmpdir.join(filename)
 65 | 
 66 |     if "r" in mode:
 67 |         full_file.ensure()  # Read modes want it to already exist.
 68 | 
 69 |     async with aioopen(str(full_file), mode=mode, buffering=buffering) as file:
 70 |         await file.write(b"0")  # Shouldn't flush.
 71 | 
 72 |         if buffering == -1:
 73 |             assert b"" == full_file.read_binary()
 74 |         else:
 75 |             assert b"0" == full_file.read_binary()
 76 | 
 77 |         await file.flush()
 78 | 
 79 |         assert b"0" == full_file.read_binary()
 80 | 
 81 | 
 82 | @pytest.mark.parametrize("mode", ["rb+", "wb+", "ab+"])
 83 | async def test_simple_peek(mode, tmpdir):
 84 |     """Test flushing to a file."""
 85 |     filename = "file.bin"
 86 | 
 87 |     full_file = tmpdir.join(filename)
 88 |     full_file.write_binary(b"0123456789")
 89 | 
 90 |     async with aioopen(str(full_file), mode=mode) as file:
 91 |         if "a" in mode:
 92 |             await file.seek(0)  # Rewind for append modes.
 93 | 
 94 |         peeked = await file.peek(1)
 95 | 
 96 |         # Technically it's OK for the peek to return less bytes than requested.
 97 |         if peeked:
 98 |             assert peeked.startswith(b"0")
 99 | 
100 |             read = await file.read(1)
101 | 
102 |             assert peeked.startswith(read)
103 | 
104 | 
105 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
106 | @pytest.mark.parametrize("buffering", [-1, 0])
107 | async def test_simple_read(mode, buffering):
108 |     """Just read some bytes from a test file."""
109 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
110 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
111 |         await file.seek(0)  # Needed for the append mode.
112 | 
113 |         actual = await file.read()
114 | 
115 |         assert b"" == (await file.read())
116 |     assert actual == open(filename, mode="rb").read()
117 | 
118 | 
119 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
120 | @pytest.mark.parametrize("buffering", [-1, 0])
121 | async def test_staggered_read(mode, buffering):
122 |     """Read bytes repeatedly."""
123 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
124 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
125 |         await file.seek(0)  # Needed for the append mode.
126 | 
127 |         actual = []
128 |         while True:
129 |             byte = await file.read(1)
130 |             if byte:
131 |                 actual.append(byte)
132 |             else:
133 |                 break
134 | 
135 |         assert b"" == (await file.read())
136 | 
137 |         expected = []
138 |         with open(filename, mode="rb") as f:
139 |             while True:
140 |                 byte = f.read(1)
141 |                 if byte:
142 |                     expected.append(byte)
143 |                 else:
144 |                     break
145 | 
146 |     assert actual == expected
147 | 
148 | 
149 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
150 | @pytest.mark.parametrize("buffering", [-1, 0])
151 | async def test_simple_seek(mode, buffering, tmpdir):
152 |     """Test seeking and then reading."""
153 |     filename = "bigfile.bin"
154 |     content = b"0123456789" * 4 * io.DEFAULT_BUFFER_SIZE
155 | 
156 |     full_file = tmpdir.join(filename)
157 |     full_file.write_binary(content)
158 | 
159 |     async with aioopen(str(full_file), mode=mode, buffering=buffering) as file:
160 |         await file.seek(4)
161 | 
162 |         assert b"4" == (await file.read(1))
163 | 
164 | 
165 | @pytest.mark.parametrize("mode", ["wb", "rb", "rb+", "wb+", "ab", "ab+"])
166 | @pytest.mark.parametrize("buffering", [-1, 0])
167 | async def test_simple_close_ctx_mgr_iter(mode, buffering, tmpdir):
168 |     """Open a file, read a byte, and close it."""
169 |     filename = "bigfile.bin"
170 |     content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE
171 | 
172 |     full_file = tmpdir.join(filename)
173 |     full_file.write_binary(content)
174 | 
175 |     async with aioopen(str(full_file), mode=mode, buffering=buffering) as file:
176 |         assert not file.closed
177 |         assert not file._file.closed
178 | 
179 |     assert file.closed
180 |     assert file._file.closed
181 | 
182 | 
183 | @pytest.mark.parametrize("mode", ["wb", "rb", "rb+", "wb+", "ab", "ab+"])
184 | @pytest.mark.parametrize("buffering", [-1, 0])
185 | async def test_simple_close_ctx_mgr(mode, buffering, tmpdir):
186 |     """Open a file, read a byte, and close it."""
187 |     filename = "bigfile.bin"
188 |     content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE
189 | 
190 |     full_file = tmpdir.join(filename)
191 |     full_file.write_binary(content)
192 | 
193 |     file = await aioopen(str(full_file), mode=mode, buffering=buffering)
194 |     assert not file.closed
195 |     assert not file._file.closed
196 | 
197 |     await file.close()
198 | 
199 |     assert file.closed
200 |     assert file._file.closed
201 | 
202 | 
203 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
204 | @pytest.mark.parametrize("buffering", [-1, 0])
205 | async def test_simple_readinto(mode, buffering):
206 |     """Test the readinto functionality."""
207 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
208 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
209 |         await file.seek(0)  # Needed for the append mode.
210 | 
211 |         array = bytearray(4)
212 |         bytes_read = await file.readinto(array)
213 | 
214 |         assert bytes_read == 4
215 |         assert array == open(filename, mode="rb").read(4)
216 | 
217 | 
218 | @pytest.mark.parametrize("mode", ["rb+", "wb", "ab+"])
219 | @pytest.mark.parametrize("buffering", [-1, 0])
220 | async def test_simple_truncate(mode, buffering, tmpdir):
221 |     """Test truncating files."""
222 |     filename = "bigfile.bin"
223 |     content = b"0123456789" * 4 * io.DEFAULT_BUFFER_SIZE
224 | 
225 |     full_file = tmpdir.join(filename)
226 |     full_file.write_binary(content)
227 | 
228 |     async with aioopen(str(full_file), mode=mode, buffering=buffering) as file:
229 |         # The append modes want us to seek first.
230 |         await file.seek(0)
231 | 
232 |         if "w" in mode:
233 |             # We've just erased the entire file.
234 |             await file.write(content)
235 |             await file.flush()
236 |             await file.seek(0)
237 | 
238 |         await file.truncate()
239 | 
240 |     assert b"" == full_file.read_binary()
241 | 
242 | 
243 | @pytest.mark.parametrize("mode", ["wb", "rb+", "wb+", "ab", "ab+"])
244 | @pytest.mark.parametrize("buffering", [-1, 0])
245 | async def test_simple_write(mode, buffering, tmpdir):
246 |     """Test writing into a file."""
247 |     filename = "bigfile.bin"
248 |     content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE
249 | 
250 |     full_file = tmpdir.join(filename)
251 | 
252 |     if "r" in mode:
253 |         full_file.ensure()  # Read modes want it to already exist.
254 | 
255 |     async with aioopen(str(full_file), mode=mode, buffering=buffering) as file:
256 |         bytes_written = await file.write(content)
257 | 
258 |     assert bytes_written == len(content)
259 |     assert content == full_file.read_binary()
260 | 
261 | 
262 | async def test_simple_detach(tmpdir):
263 |     """Test detaching for buffered streams."""
264 |     filename = "file.bin"
265 | 
266 |     full_file = tmpdir.join(filename)
267 |     full_file.write_binary(b"0123456789")
268 | 
269 |     with pytest.raises(ValueError):
270 |         async with aioopen(str(full_file), mode="rb") as file:
271 |             raw_file = file.detach()
272 | 
273 |             assert raw_file
274 | 
275 |             with pytest.raises(ValueError):
276 |                 await file.read()
277 | 
278 |     assert b"0123456789" == raw_file.read(10)
279 | 
280 | 
281 | async def test_simple_readall(tmpdir):
282 |     """Test the readall function by reading a large file in.
283 | 
284 |     Only RawIOBase supports readall().
285 |     """
286 |     filename = "bigfile.bin"
287 |     content = b"0" * 4 * io.DEFAULT_BUFFER_SIZE  # Hopefully several reads.
288 | 
289 |     sync_file = tmpdir.join(filename)
290 |     sync_file.write_binary(content)
291 | 
292 |     file = await aioopen(str(sync_file), mode="rb", buffering=0)
293 | 
294 |     actual = await file.readall()
295 | 
296 |     assert actual == content
297 | 
298 |     await file.close()
299 |     assert file.closed
300 | 
301 | 
302 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
303 | @pytest.mark.parametrize("buffering", [-1, 0])
304 | async def test_name_property(mode, buffering):
305 |     """Test iterating over lines from a file."""
306 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
307 | 
308 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
309 |         assert file.name == filename
310 | 
311 |     assert file.closed
312 | 
313 | 
314 | @pytest.mark.parametrize("mode", ["rb", "rb+", "ab+"])
315 | @pytest.mark.parametrize("buffering", [-1, 0])
316 | async def test_mode_property(mode, buffering):
317 |     """Test iterating over lines from a file."""
318 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
319 | 
320 |     async with aioopen(filename, mode=mode, buffering=buffering) as file:
321 |         assert file.mode == mode
322 | 
323 |     assert file.closed
324 | 


--------------------------------------------------------------------------------
/tests/threadpool/test_concurrency.py:
--------------------------------------------------------------------------------
 1 | """Test concurrency properties of the implementation."""
 2 | 
 3 | import asyncio
 4 | import time
 5 | from os.path import dirname, join
 6 | 
 7 | import aiofiles.threadpool
 8 | 
 9 | 
10 | async def test_slow_file(monkeypatch, unused_tcp_port):
11 |     """Monkey patch open and file.read(), and assert the loop still works."""
12 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
13 | 
14 |     with open(filename, mode="rb") as f:
15 |         contents = f.read()
16 | 
17 |     def new_open(*args, **kwargs):
18 |         time.sleep(1)
19 |         return open(*args, **kwargs)
20 | 
21 |     monkeypatch.setattr(aiofiles.threadpool, "sync_open", value=new_open)
22 | 
23 |     async def serve_file(_, writer):
24 |         file = await aiofiles.threadpool.open(filename, mode="rb")
25 |         try:
26 |             while True:
27 |                 data = await file.read(1)
28 |                 if not data:
29 |                     break
30 |                 writer.write(data)
31 |                 await writer.drain()
32 |             await writer.drain()
33 |         finally:
34 |             writer.close()
35 |             await file.close()
36 | 
37 |     async def return_one(_, writer):
38 |         writer.write(b"1")
39 |         await writer.drain()
40 |         writer.close()
41 | 
42 |     counter = 0
43 | 
44 |     async def spam_client():
45 |         nonlocal counter
46 |         while True:
47 |             r, w = await asyncio.open_connection("127.0.0.1", port=30001)
48 |             assert (await r.read()) == b"1"
49 |             counter += 1
50 |             w.close()
51 |             await asyncio.sleep(0.01)
52 | 
53 |     file_server = await asyncio.start_server(serve_file, port=unused_tcp_port)
54 |     spam_server = await asyncio.start_server(return_one, port=30001)
55 | 
56 |     spam_task = asyncio.ensure_future(spam_client())
57 | 
58 |     reader, writer = await asyncio.open_connection("127.0.0.1", port=unused_tcp_port)
59 | 
60 |     actual_contents = await reader.read()
61 |     writer.close()
62 | 
63 |     await asyncio.sleep(0)
64 | 
65 |     file_server.close()
66 |     spam_server.close()
67 | 
68 |     await file_server.wait_closed()
69 |     await spam_server.wait_closed()
70 | 
71 |     spam_task.cancel()
72 | 
73 |     assert actual_contents == contents
74 |     assert counter > 30
75 | 


--------------------------------------------------------------------------------
/tests/threadpool/test_open.py:
--------------------------------------------------------------------------------
 1 | import asyncio
 2 | from pathlib import Path
 3 | 
 4 | import pytest
 5 | 
 6 | from aiofiles.threadpool import open as aioopen
 7 | 
 8 | RESOURCES_DIR = Path(__file__).parent.parent / "resources"
 9 | TEST_FILE = RESOURCES_DIR / "test_file1.txt"
10 | TEST_FILE_CONTENTS = "0123456789"
11 | 
12 | 
13 | @pytest.mark.parametrize("mode", ["r", "rb"])
14 | async def test_file_not_found(mode):
15 |     filename = "non_existent"
16 | 
17 |     try:
18 |         open(filename, mode=mode)
19 |     except Exception as e:
20 |         expected = e
21 | 
22 |     assert expected
23 | 
24 |     try:
25 |         await aioopen(filename, mode=mode)
26 |     except Exception as e:
27 |         actual = e
28 | 
29 |     assert actual
30 | 
31 |     assert actual.errno == expected.errno
32 |     assert str(actual) == str(expected)
33 | 
34 | 
35 | async def test_file_async_context_aexit():
36 |     async with aioopen(TEST_FILE) as fp:
37 |         pass
38 | 
39 |     with pytest.raises(ValueError):
40 |         line = await fp.read()
41 | 
42 |     async with aioopen(TEST_FILE) as fp:
43 |         line = await fp.read()
44 |         assert line == TEST_FILE_CONTENTS
45 | 
46 | 
47 | async def test_filetask_async_context_aexit():
48 |     async def _process_test_file(file_ctx, sleep_time: float = 1.0):
49 |         nonlocal file_ref
50 |         async with file_ctx as fp:
51 |             file_ref = file_ctx._obj
52 |             await asyncio.sleep(sleep_time)
53 |             await fp.read()
54 | 
55 |     cancel_time, sleep_time = 0.1, 10
56 |     assert cancel_time <= (sleep_time / 10)
57 | 
58 |     file_ref = None
59 |     file_ctx = aioopen(TEST_FILE)
60 | 
61 |     task = asyncio.create_task(
62 |         _process_test_file(file_ctx=file_ctx, sleep_time=sleep_time)
63 |     )
64 |     try:
65 |         await asyncio.wait_for(task, timeout=cancel_time)
66 |     except asyncio.TimeoutError:
67 |         assert task.cancelled
68 | 
69 |     assert file_ref.closed
70 | 


--------------------------------------------------------------------------------
/tests/threadpool/test_text.py:
--------------------------------------------------------------------------------
  1 | """PEP 0492/Python 3.5+ tests for text files."""
  2 | 
  3 | import io
  4 | from os.path import dirname, join
  5 | 
  6 | import pytest
  7 | 
  8 | from aiofiles.threadpool import open as aioopen
  9 | 
 10 | 
 11 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
 12 | async def test_simple_iteration(mode):
 13 |     """Test iterating over lines from a file."""
 14 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
 15 | 
 16 |     async with aioopen(filename, mode=mode) as file:
 17 |         # Append mode needs us to seek.
 18 |         await file.seek(0)
 19 | 
 20 |         counter = 1
 21 | 
 22 |         # The old iteration pattern:
 23 |         while True:
 24 |             line = await file.readline()
 25 |             if not line:
 26 |                 break
 27 |             assert line.strip() == "line " + str(counter)
 28 |             counter += 1
 29 | 
 30 |         await file.seek(0)
 31 |         counter = 1
 32 | 
 33 |         # The new iteration pattern:
 34 |         async for line in file:
 35 |             assert line.strip() == "line " + str(counter)
 36 |             counter += 1
 37 | 
 38 |     assert file.closed
 39 | 
 40 | 
 41 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
 42 | async def test_simple_readlines(mode):
 43 |     """Test the readlines functionality."""
 44 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
 45 | 
 46 |     with open(filename) as f:
 47 |         expected = f.readlines()
 48 | 
 49 |     async with aioopen(filename, mode=mode) as file:
 50 |         # Append mode needs us to seek.
 51 |         await file.seek(0)
 52 | 
 53 |         actual = await file.readlines()
 54 | 
 55 |     assert file.closed
 56 | 
 57 |     assert actual == expected
 58 | 
 59 | 
 60 | @pytest.mark.parametrize("mode", ["r+", "w", "a"])
 61 | async def test_simple_flush(mode, tmpdir):
 62 |     """Test flushing to a file."""
 63 |     filename = "file.bin"
 64 | 
 65 |     full_file = tmpdir.join(filename)
 66 | 
 67 |     if "r" in mode:
 68 |         full_file.ensure()  # Read modes want it to already exist.
 69 | 
 70 |     async with aioopen(str(full_file), mode=mode) as file:
 71 |         await file.write("0")  # Shouldn't flush.
 72 | 
 73 |         assert "" == full_file.read_text(encoding="utf8")
 74 | 
 75 |         await file.flush()
 76 | 
 77 |         assert "0" == full_file.read_text(encoding="utf8")
 78 | 
 79 |     assert file.closed
 80 | 
 81 | 
 82 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
 83 | async def test_simple_read(mode):
 84 |     """Just read some bytes from a test file."""
 85 |     filename = join(dirname(__file__), "..", "resources", "test_file1.txt")
 86 |     async with aioopen(filename, mode=mode) as file:
 87 |         await file.seek(0)  # Needed for the append mode.
 88 | 
 89 |         actual = await file.read()
 90 | 
 91 |         assert "" == (await file.read())
 92 |     assert actual == open(filename).read()
 93 | 
 94 |     assert file.closed
 95 | 
 96 | 
 97 | @pytest.mark.parametrize("mode", ["w", "a"])
 98 | async def test_simple_read_fail(mode, tmpdir):
 99 |     """Try reading some bytes and fail."""
100 |     filename = "bigfile.bin"
101 |     content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE
102 | 
103 |     full_file = tmpdir.join(filename)
104 |     full_file.write(content)
105 |     with pytest.raises(ValueError):
106 |         async with aioopen(str(full_file), mode=mode) as file:
107 |             await file.seek(0)  # Needed for the append mode.
108 | 
109 |             await file.read()
110 | 
111 |     assert file.closed
112 | 
113 | 
114 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
115 | async def test_staggered_read(mode):
116 |     """Read bytes repeatedly."""
117 |     filename = join(dirname(__file__), "..", "resources", "test_file1.txt")
118 |     async with aioopen(filename, mode=mode) as file:
119 |         await file.seek(0)  # Needed for the append mode.
120 | 
121 |         actual = []
122 |         while True:
123 |             char = await file.read(1)
124 |             if char:
125 |                 actual.append(char)
126 |             else:
127 |                 break
128 | 
129 |         assert "" == (await file.read())
130 | 
131 |     expected = []
132 |     with open(filename) as f:
133 |         while True:
134 |             char = f.read(1)
135 |             if char:
136 |                 expected.append(char)
137 |             else:
138 |                 break
139 | 
140 |     assert actual == expected
141 | 
142 |     assert file.closed
143 | 
144 | 
145 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
146 | async def test_simple_seek(mode, tmpdir):
147 |     """Test seeking and then reading."""
148 |     filename = "bigfile.bin"
149 |     content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE
150 | 
151 |     full_file = tmpdir.join(filename)
152 |     full_file.write(content)
153 | 
154 |     async with aioopen(str(full_file), mode=mode) as file:
155 |         await file.seek(4)
156 |         assert "4" == (await file.read(1))
157 | 
158 |     assert file.closed
159 | 
160 | 
161 | @pytest.mark.parametrize("mode", ["w", "r", "r+", "w+", "a", "a+"])
162 | async def test_simple_close(mode, tmpdir):
163 |     """Open a file, read a byte, and close it."""
164 |     filename = "bigfile.bin"
165 |     content = "0" * 4 * io.DEFAULT_BUFFER_SIZE
166 | 
167 |     full_file = tmpdir.join(filename)
168 |     full_file.write(content)
169 | 
170 |     async with aioopen(str(full_file), mode=mode) as file:
171 |         assert not file.closed
172 |         assert not file._file.closed
173 | 
174 |     assert file.closed
175 |     assert file._file.closed
176 | 
177 | 
178 | @pytest.mark.parametrize("mode", ["r+", "w", "a+"])
179 | async def test_simple_truncate(mode, tmpdir):
180 |     """Test truncating files."""
181 |     filename = "bigfile.bin"
182 |     content = "0123456789" * 4 * io.DEFAULT_BUFFER_SIZE
183 | 
184 |     full_file = tmpdir.join(filename)
185 |     full_file.write(content)
186 | 
187 |     async with aioopen(str(full_file), mode=mode) as file:
188 |         # The append modes want us to seek first.
189 |         await file.seek(0)
190 | 
191 |         if "w" in mode:
192 |             # We've just erased the entire file.
193 |             await file.write(content)
194 |             await file.flush()
195 |             await file.seek(0)
196 | 
197 |         await file.truncate()
198 | 
199 |     assert "" == full_file.read()
200 | 
201 | 
202 | @pytest.mark.parametrize("mode", ["w", "r+", "w+", "a", "a+"])
203 | async def test_simple_write(mode, tmpdir):
204 |     """Test writing into a file."""
205 |     filename = "bigfile.bin"
206 |     content = "0" * 4 * io.DEFAULT_BUFFER_SIZE
207 | 
208 |     full_file = tmpdir.join(filename)
209 | 
210 |     if "r" in mode:
211 |         full_file.ensure()  # Read modes want it to already exist.
212 | 
213 |     async with aioopen(str(full_file), mode=mode) as file:
214 |         bytes_written = await file.write(content)
215 | 
216 |     assert bytes_written == len(content)
217 |     assert content == full_file.read()
218 |     assert file.closed
219 | 
220 | 
221 | async def test_simple_detach(tmpdir):
222 |     """Test detaching for buffered streams."""
223 |     filename = "file.bin"
224 | 
225 |     full_file = tmpdir.join(filename)
226 |     full_file.write("0123456789")
227 | 
228 |     with pytest.raises(ValueError):  # Close will error out.
229 |         async with aioopen(str(full_file), mode="r") as file:
230 |             raw_file = file.detach()
231 | 
232 |             assert raw_file
233 | 
234 |             with pytest.raises(ValueError):
235 |                 await file.read()
236 | 
237 |             assert b"0123456789" == raw_file.read(10)
238 | 
239 | 
240 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
241 | async def test_simple_iteration_ctx_mgr(mode):
242 |     """Test iterating over lines from a file."""
243 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
244 | 
245 |     async with aioopen(filename, mode=mode) as file:
246 |         assert not file.closed
247 |         await file.seek(0)
248 | 
249 |         counter = 1
250 | 
251 |         async for line in file:
252 |             assert line.strip() == "line " + str(counter)
253 |             counter += 1
254 | 
255 |     assert file.closed
256 | 
257 | 
258 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
259 | async def test_name_property(mode):
260 |     """Test iterating over lines from a file."""
261 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
262 | 
263 |     async with aioopen(filename, mode=mode) as file:
264 |         assert file.name == filename
265 | 
266 |     assert file.closed
267 | 
268 | 
269 | @pytest.mark.parametrize("mode", ["r", "r+", "a+"])
270 | async def test_mode_property(mode):
271 |     """Test iterating over lines from a file."""
272 |     filename = join(dirname(__file__), "..", "resources", "multiline_file.txt")
273 | 
274 |     async with aioopen(filename, mode=mode) as file:
275 |         assert file.mode == mode
276 | 
277 |     assert file.closed
278 | 


--------------------------------------------------------------------------------
/tests/threadpool/test_wrap.py:
--------------------------------------------------------------------------------
 1 | from io import FileIO
 2 | 
 3 | import pytest
 4 | 
 5 | from aiofiles.threadpool import wrap
 6 | 
 7 | 
 8 | @pytest.mark.parametrize("entity", [int, [1, 2, 3], lambda x: x**x, FileIO])
 9 | def test_threadpool_wrapper_negative(entity):
10 |     """Raising TypeError when wrapping unsupported entities."""
11 | 
12 |     with pytest.raises(TypeError):
13 |         wrap(entity)
14 | 


--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
 1 | [tox]
 2 | requires = 
 3 |     tox>=4.26
 4 | min_version = 4.26
 5 | env_list = py39, py31{0,1,2,3,4}, pypy39, lint
 6 | no_package = false
 7 | 
 8 | [testenv:lint]
 9 | skip_install = true
10 | basepython = python3.13
11 | allowlist_externals = just
12 | commands =
13 |     just check
14 | dependency_groups =
15 |     lint
16 | 
17 | [testenv]
18 | runner = uv-venv-lock-runner
19 | allowlist_externals = just
20 | setenv =
21 |     PYTHONHASHSEED=0
22 | commands =
23 |     just coverage
24 | passenv = CI
25 | package = wheel
26 | wheel_build_env = .pkg
27 | uv_python_preference = only-managed
28 | dependency_groups =
29 |     test


--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
  1 | version = 1
  2 | revision = 2
  3 | requires-python = ">=3.9"
  4 | 
  5 | [[package]]
  6 | name = "aiofiles"
  7 | source = { editable = "." }
  8 | 
  9 | [package.dev-dependencies]
 10 | lint = [
 11 |     { name = "mypy" },
 12 |     { name = "ruff" },
 13 | ]
 14 | test = [
 15 |     { name = "coverage" },
 16 |     { name = "pytest" },
 17 |     { name = "pytest-asyncio" },
 18 | ]
 19 | tox = [
 20 |     { name = "tox" },
 21 |     { name = "tox-uv" },
 22 | ]
 23 | 
 24 | [package.metadata]
 25 | 
 26 | [package.metadata.requires-dev]
 27 | lint = [
 28 |     { name = "mypy", specifier = ">=1.16.0" },
 29 |     { name = "ruff", specifier = ">=0.11.12" },
 30 | ]
 31 | test = [
 32 |     { name = "coverage", specifier = ">=7.8.2" },
 33 |     { name = "pytest", specifier = ">=8.3.5" },
 34 |     { name = "pytest-asyncio", specifier = ">=1.0.0" },
 35 | ]
 36 | tox = [
 37 |     { name = "tox", specifier = ">=4.26.0" },
 38 |     { name = "tox-uv", specifier = ">=1.26.0" },
 39 | ]
 40 | 
 41 | [[package]]
 42 | name = "cachetools"
 43 | version = "6.0.0"
 44 | source = { registry = "https://pypi.org/simple" }
 45 | sdist = { url = "https://files.pythonhosted.org/packages/c0/b0/f539a1ddff36644c28a61490056e5bae43bd7386d9f9c69beae2d7e7d6d1/cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf", size = 30160, upload-time = "2025-05-23T20:01:13.076Z" }
 46 | wheels = [
 47 |     { url = "https://files.pythonhosted.org/packages/6a/c3/8bb087c903c95a570015ce84e0c23ae1d79f528c349cbc141b5c4e250293/cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e", size = 10964, upload-time = "2025-05-23T20:01:11.323Z" },
 48 | ]
 49 | 
 50 | [[package]]
 51 | name = "chardet"
 52 | version = "5.2.0"
 53 | source = { registry = "https://pypi.org/simple" }
 54 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" }
 55 | wheels = [
 56 |     { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" },
 57 | ]
 58 | 
 59 | [[package]]
 60 | name = "colorama"
 61 | version = "0.4.6"
 62 | source = { registry = "https://pypi.org/simple" }
 63 | 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" }
 64 | wheels = [
 65 |     { 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" },
 66 | ]
 67 | 
 68 | [[package]]
 69 | name = "coverage"
 70 | version = "7.8.2"
 71 | source = { registry = "https://pypi.org/simple" }
 72 | sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" }
 73 | wheels = [
 74 |     { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" },
 75 |     { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" },
 76 |     { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" },
 77 |     { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" },
 78 |     { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" },
 79 |     { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" },
 80 |     { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" },
 81 |     { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" },
 82 |     { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" },
 83 |     { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" },
 84 |     { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" },
 85 |     { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" },
 86 |     { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" },
 87 |     { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" },
 88 |     { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" },
 89 |     { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" },
 90 |     { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" },
 91 |     { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" },
 92 |     { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" },
 93 |     { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" },
 94 |     { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" },
 95 |     { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" },
 96 |     { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" },
 97 |     { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" },
 98 |     { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" },
 99 |     { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" },
100 |     { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" },
101 |     { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" },
102 |     { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" },
103 |     { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" },
104 |     { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" },
105 |     { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" },
106 |     { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" },
107 |     { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" },
108 |     { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" },
109 |     { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" },
110 |     { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" },
111 |     { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" },
112 |     { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" },
113 |     { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" },
114 |     { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" },
115 |     { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" },
116 |     { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" },
117 |     { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" },
118 |     { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" },
119 |     { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" },
120 |     { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" },
121 |     { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" },
122 |     { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" },
123 |     { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" },
124 |     { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" },
125 |     { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" },
126 |     { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" },
127 |     { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" },
128 |     { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566, upload-time = "2025-05-23T11:39:32.333Z" },
129 |     { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996, upload-time = "2025-05-23T11:39:34.512Z" },
130 |     { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741, upload-time = "2025-05-23T11:39:36.252Z" },
131 |     { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672, upload-time = "2025-05-23T11:39:38.03Z" },
132 |     { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769, upload-time = "2025-05-23T11:39:40.24Z" },
133 |     { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555, upload-time = "2025-05-23T11:39:42.3Z" },
134 |     { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768, upload-time = "2025-05-23T11:39:44.069Z" },
135 |     { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757, upload-time = "2025-05-23T11:39:46.195Z" },
136 |     { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166, upload-time = "2025-05-23T11:39:47.934Z" },
137 |     { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050, upload-time = "2025-05-23T11:39:50.252Z" },
138 |     { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" },
139 |     { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" },
140 | ]
141 | 
142 | [[package]]
143 | name = "distlib"
144 | version = "0.3.9"
145 | source = { registry = "https://pypi.org/simple" }
146 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
147 | wheels = [
148 |     { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
149 | ]
150 | 
151 | [[package]]
152 | name = "exceptiongroup"
153 | version = "1.3.0"
154 | source = { registry = "https://pypi.org/simple" }
155 | dependencies = [
156 |     { name = "typing-extensions", marker = "python_full_version < '3.13'" },
157 | ]
158 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
159 | wheels = [
160 |     { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
161 | ]
162 | 
163 | [[package]]
164 | name = "filelock"
165 | version = "3.18.0"
166 | source = { registry = "https://pypi.org/simple" }
167 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
168 | wheels = [
169 |     { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
170 | ]
171 | 
172 | [[package]]
173 | name = "iniconfig"
174 | version = "2.1.0"
175 | source = { registry = "https://pypi.org/simple" }
176 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
177 | wheels = [
178 |     { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
179 | ]
180 | 
181 | [[package]]
182 | name = "mypy"
183 | version = "1.16.0"
184 | source = { registry = "https://pypi.org/simple" }
185 | dependencies = [
186 |     { name = "mypy-extensions" },
187 |     { name = "pathspec" },
188 |     { name = "tomli", marker = "python_full_version < '3.11'" },
189 |     { name = "typing-extensions" },
190 | ]
191 | sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" }
192 | wheels = [
193 |     { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" },
194 |     { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" },
195 |     { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" },
196 |     { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" },
197 |     { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" },
198 |     { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" },
199 |     { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" },
200 |     { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" },
201 |     { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" },
202 |     { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" },
203 |     { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" },
204 |     { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" },
205 |     { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" },
206 |     { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" },
207 |     { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" },
208 |     { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" },
209 |     { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" },
210 |     { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" },
211 |     { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" },
212 |     { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" },
213 |     { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" },
214 |     { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" },
215 |     { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" },
216 |     { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" },
217 |     { url = "https://files.pythonhosted.org/packages/bd/eb/c0759617fe2159aee7a653f13cceafbf7f0b6323b4197403f2e587ca947d/mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3", size = 10956081, upload-time = "2025-05-29T13:19:32.264Z" },
218 |     { url = "https://files.pythonhosted.org/packages/70/35/df3c74a2967bdf86edea58b265feeec181d693432faed1c3b688b7c231e3/mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92", size = 10084422, upload-time = "2025-05-29T13:18:01.437Z" },
219 |     { url = "https://files.pythonhosted.org/packages/b3/07/145ffe29f4b577219943b7b1dc0a71df7ead3c5bed4898686bd87c5b5cc2/mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436", size = 11879670, upload-time = "2025-05-29T13:17:45.971Z" },
220 |     { url = "https://files.pythonhosted.org/packages/c6/94/0421562d6b046e22986758c9ae31865d10ea0ba607ae99b32c9d18b16f66/mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2", size = 12610528, upload-time = "2025-05-29T13:34:36.983Z" },
221 |     { url = "https://files.pythonhosted.org/packages/1a/f1/39a22985b78c766a594ae1e0bbb6f8bdf5f31ea8d0c52291a3c211fd3cd5/mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20", size = 12871923, upload-time = "2025-05-29T13:32:21.823Z" },
222 |     { url = "https://files.pythonhosted.org/packages/f3/8e/84db4fb0d01f43d2c82fa9072ca72a42c49e52d58f44307bbd747c977bc2/mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21", size = 9482931, upload-time = "2025-05-29T13:21:32.326Z" },
223 |     { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" },
224 | ]
225 | 
226 | [[package]]
227 | name = "mypy-extensions"
228 | version = "1.1.0"
229 | source = { registry = "https://pypi.org/simple" }
230 | 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" }
231 | wheels = [
232 |     { 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" },
233 | ]
234 | 
235 | [[package]]
236 | name = "packaging"
237 | version = "25.0"
238 | source = { registry = "https://pypi.org/simple" }
239 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
240 | wheels = [
241 |     { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
242 | ]
243 | 
244 | [[package]]
245 | name = "pathspec"
246 | version = "0.12.1"
247 | source = { registry = "https://pypi.org/simple" }
248 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
249 | wheels = [
250 |     { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
251 | ]
252 | 
253 | [[package]]
254 | name = "platformdirs"
255 | version = "4.3.8"
256 | source = { registry = "https://pypi.org/simple" }
257 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
258 | wheels = [
259 |     { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
260 | ]
261 | 
262 | [[package]]
263 | name = "pluggy"
264 | version = "1.6.0"
265 | source = { registry = "https://pypi.org/simple" }
266 | 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" }
267 | wheels = [
268 |     { 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" },
269 | ]
270 | 
271 | [[package]]
272 | name = "pyproject-api"
273 | version = "1.9.1"
274 | source = { registry = "https://pypi.org/simple" }
275 | dependencies = [
276 |     { name = "packaging" },
277 |     { name = "tomli", marker = "python_full_version < '3.11'" },
278 | ]
279 | sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" }
280 | wheels = [
281 |     { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" },
282 | ]
283 | 
284 | [[package]]
285 | name = "pytest"
286 | version = "8.3.5"
287 | source = { registry = "https://pypi.org/simple" }
288 | dependencies = [
289 |     { name = "colorama", marker = "sys_platform == 'win32'" },
290 |     { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
291 |     { name = "iniconfig" },
292 |     { name = "packaging" },
293 |     { name = "pluggy" },
294 |     { name = "tomli", marker = "python_full_version < '3.11'" },
295 | ]
296 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
297 | wheels = [
298 |     { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
299 | ]
300 | 
301 | [[package]]
302 | name = "pytest-asyncio"
303 | version = "1.0.0"
304 | source = { registry = "https://pypi.org/simple" }
305 | dependencies = [
306 |     { name = "pytest" },
307 |     { name = "typing-extensions", marker = "python_full_version < '3.10'" },
308 | ]
309 | sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" }
310 | wheels = [
311 |     { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" },
312 | ]
313 | 
314 | [[package]]
315 | name = "ruff"
316 | version = "0.11.12"
317 | source = { registry = "https://pypi.org/simple" }
318 | sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
319 | wheels = [
320 |     { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
321 |     { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
322 |     { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
323 |     { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
324 |     { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
325 |     { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
326 |     { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
327 |     { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
328 |     { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
329 |     { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
330 |     { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
331 |     { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
332 |     { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
333 |     { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
334 |     { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
335 |     { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
336 |     { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
337 | ]
338 | 
339 | [[package]]
340 | name = "tomli"
341 | version = "2.2.1"
342 | source = { registry = "https://pypi.org/simple" }
343 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
344 | wheels = [
345 |     { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
346 |     { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
347 |     { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
348 |     { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
349 |     { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
350 |     { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
351 |     { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
352 |     { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
353 |     { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
354 |     { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
355 |     { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
356 |     { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
357 |     { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
358 |     { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
359 |     { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
360 |     { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
361 |     { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
362 |     { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
363 |     { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
364 |     { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
365 |     { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
366 |     { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
367 |     { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
368 |     { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
369 |     { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
370 |     { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
371 |     { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
372 |     { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
373 |     { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
374 |     { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
375 |     { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
376 | ]
377 | 
378 | [[package]]
379 | name = "tox"
380 | version = "4.26.0"
381 | source = { registry = "https://pypi.org/simple" }
382 | dependencies = [
383 |     { name = "cachetools" },
384 |     { name = "chardet" },
385 |     { name = "colorama" },
386 |     { name = "filelock" },
387 |     { name = "packaging" },
388 |     { name = "platformdirs" },
389 |     { name = "pluggy" },
390 |     { name = "pyproject-api" },
391 |     { name = "tomli", marker = "python_full_version < '3.11'" },
392 |     { name = "typing-extensions", marker = "python_full_version < '3.11'" },
393 |     { name = "virtualenv" },
394 | ]
395 | sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/dcec0c00321a107f7f697fd00754c5112572ea6dcacb40b16d8c3eea7c37/tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca", size = 197260, upload-time = "2025-05-13T15:04:28.481Z" }
396 | wheels = [
397 |     { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761, upload-time = "2025-05-13T15:04:26.207Z" },
398 | ]
399 | 
400 | [[package]]
401 | name = "tox-uv"
402 | version = "1.26.0"
403 | source = { registry = "https://pypi.org/simple" }
404 | dependencies = [
405 |     { name = "packaging" },
406 |     { name = "tox" },
407 |     { name = "typing-extensions", marker = "python_full_version < '3.10'" },
408 |     { name = "uv" },
409 | ]
410 | sdist = { url = "https://files.pythonhosted.org/packages/7e/da/37790b4a176f05b0ec7a699f54979078fc726f743640aa5c10c551c27edb/tox_uv-1.26.0.tar.gz", hash = "sha256:5045880c467eed58a98f7eaa7fe286b7ef688e2c56f2123d53e275011495c381", size = 21523, upload-time = "2025-05-27T14:51:42.702Z" }
411 | wheels = [
412 |     { url = "https://files.pythonhosted.org/packages/46/b8/04c5cb83da072a3f96d357d68a551f5e97e162573c2011a09437df995811/tox_uv-1.26.0-py3-none-any.whl", hash = "sha256:894b2e7274fd6131c3bd1012813edc858753cad67727050c21cd973a08e691c8", size = 16562, upload-time = "2025-05-27T14:51:40.803Z" },
413 | ]
414 | 
415 | [[package]]
416 | name = "typing-extensions"
417 | version = "4.13.2"
418 | source = { registry = "https://pypi.org/simple" }
419 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
420 | wheels = [
421 |     { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
422 | ]
423 | 
424 | [[package]]
425 | name = "uv"
426 | version = "0.7.9"
427 | source = { registry = "https://pypi.org/simple" }
428 | sdist = { url = "https://files.pythonhosted.org/packages/9a/7c/8621d5928111f985196dc75c50a64147b3bad39f36164686f24d45581367/uv-0.7.9.tar.gz", hash = "sha256:baac54e49f3b0d05ee83f534fdcb27b91d2923c585bf349a1532ca25d62c216f", size = 3272882, upload-time = "2025-05-30T19:54:33.003Z" }
429 | wheels = [
430 |     { url = "https://files.pythonhosted.org/packages/c5/7a/e4d12029e16f30279ef48f387545f8f3974dc3c4c9d8ef59c381ae7e6a7d/uv-0.7.9-py3-none-linux_armv6l.whl", hash = "sha256:0f8c53d411f95cec2fa19471c23b41ec456fc0d5f2efca96480d94e0c34026c2", size = 16746809, upload-time = "2025-05-30T19:53:35.447Z" },
431 |     { url = "https://files.pythonhosted.org/packages/fc/85/8df3ca683e1a260117efa31373e91e1c03a4862b7add865662f60a967fdf/uv-0.7.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:85c1a63669e49b825923fc876b7467cc3c20d4aa010f522c0ac8b0f30ce2b18e", size = 16821006, upload-time = "2025-05-30T19:53:40.102Z" },
432 |     { url = "https://files.pythonhosted.org/packages/77/d4/c40502ec8f5575798b7ec13ac38c0d5ded84cc32129c1d74a47f8cb7bc0a/uv-0.7.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa10c61668f94515acf93f31dbb8de41b1f2e7a9c41db828f2448cef786498ff", size = 15600148, upload-time = "2025-05-30T19:53:43.513Z" },
433 |     { url = "https://files.pythonhosted.org/packages/4f/dd/4deec6d5b556f4033d6bcc35d6aad70c08acea3f5da749cb34112dced5da/uv-0.7.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9de67ca9ea97db71e5697c1320508e25679fb68d4ee2cea27bbeac499a6bad56", size = 16038119, upload-time = "2025-05-30T19:53:46.504Z" },
434 |     { url = "https://files.pythonhosted.org/packages/cb/c5/2c23763e18566a9a7767738714791203cc97a7530979f61e0fd32d8473a2/uv-0.7.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13ce63524f88228152edf8a9c1c07cecc07d69a2853b32ecc02ac73538aaa5c1", size = 16467257, upload-time = "2025-05-30T19:53:49.592Z" },
435 |     { url = "https://files.pythonhosted.org/packages/da/94/f452d0093f466f9f81a2ede3ea2d48632237b79eb1dc595c7c91be309de5/uv-0.7.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3453b7bb65eaea87c9129e27bff701007a8bd1a563249982a1ede7ec4357ced6", size = 17170719, upload-time = "2025-05-30T19:53:52.828Z" },
436 |     { url = "https://files.pythonhosted.org/packages/69/bf/e15ef77520e9bbf00d29a3b639dfaf4fe63996863d6db00c53eba19535c7/uv-0.7.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d7b1e36a8b39600d0f0333bf50c966e83beeaaee1a38380ccb0f16ab45f351c3", size = 18052903, upload-time = "2025-05-30T19:53:56.237Z" },
437 |     { url = "https://files.pythonhosted.org/packages/32/9f/ebf3f9910121ef037c0fe9e7e7fb5f1c25b77d41a65a029d5cbcd85cc886/uv-0.7.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab412ed3d415f07192805788669c8a89755086cdd6fe9f021e1ba21781728031", size = 17771828, upload-time = "2025-05-30T19:53:59.561Z" },
438 |     { url = "https://files.pythonhosted.org/packages/fe/6c/82b4cd471432e721c239ddde2ebee2e674238f3bd88e279e6c71f3cbc775/uv-0.7.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbeb229ee86f69913f5f9236ff1b8ccbae212f559d7f029f8432fa8d9abcc7e0", size = 17886161, upload-time = "2025-05-30T19:54:02.865Z" },
439 |     { url = "https://files.pythonhosted.org/packages/63/e2/922d2eed25647b50a7257a7bfea10c36d9ff910d1451f9a1ba5e31766f41/uv-0.7.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d654a14d632ecb078969ae7252d89dd98c89205df567a1eff18b5f078a6d00", size = 17442630, upload-time = "2025-05-30T19:54:06.519Z" },
440 |     { url = "https://files.pythonhosted.org/packages/96/b8/45a5598cc8d466bb1669ccf0fc4f556719babfdb7a1983edc24967cb3845/uv-0.7.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f5f47e93a5f948f431ca55d765af6e818c925500807539b976bfda7f94369aa9", size = 16299207, upload-time = "2025-05-30T19:54:09.713Z" },
441 |     { url = "https://files.pythonhosted.org/packages/14/35/7e70639cd175f340138c88290c819214a496dfc52461f30f71e51e776293/uv-0.7.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:267fe25ad3adf024e13617be9fc99bedebf96bf726c6140e48d856e844f21af4", size = 16427594, upload-time = "2025-05-30T19:54:13.318Z" },
442 |     { url = "https://files.pythonhosted.org/packages/5e/f6/90fe538370bc60509cca942b703bca06c06c160ec09816ea6946882278d1/uv-0.7.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:473d3c6ee07588cff8319079d9225fb393ed177d8d57186fce0d7c1aebff79c0", size = 16751451, upload-time = "2025-05-30T19:54:16.833Z" },
443 |     { url = "https://files.pythonhosted.org/packages/09/cb/c099aba21fb22e50713b42e874075a5b60c6b4d141cc3868ae22f505baa7/uv-0.7.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:19792c88058894c10f0370a5e5492bb4a7e6302c439fed9882e73ba2e4b231ef", size = 17594496, upload-time = "2025-05-30T19:54:20.383Z" },
444 |     { url = "https://files.pythonhosted.org/packages/c0/2e/e35b2c5669533075987e1d74da45af891890ae5faee031f90997ed81cada/uv-0.7.9-py3-none-win32.whl", hash = "sha256:298e9b3c65742edcb3097c2cf3f62ec847df174a7c62c85fe139dddaa1b9ab65", size = 17121149, upload-time = "2025-05-30T19:54:23.608Z" },
445 |     { url = "https://files.pythonhosted.org/packages/33/8e/d10425711156d0d5d9a28299950acb3ab4a3987b3150a3c871ac95ce2fdd/uv-0.7.9-py3-none-win_amd64.whl", hash = "sha256:82d76ea988ff1347158c6de46a571b1db7d344219e452bd7b3339c21ec37cfd8", size = 18622895, upload-time = "2025-05-30T19:54:27.427Z" },
446 |     { url = "https://files.pythonhosted.org/packages/72/77/cac29a8fb608b5613b7a0863ec6bd7c2517f3a80b94c419e9d890c12257e/uv-0.7.9-py3-none-win_arm64.whl", hash = "sha256:4d419bcc3138fd787ce77305f1a09e2a984766e0804c6e5a2b54adfa55d2439a", size = 17316542, upload-time = "2025-05-30T19:54:30.697Z" },
447 | ]
448 | 
449 | [[package]]
450 | name = "virtualenv"
451 | version = "20.31.2"
452 | source = { registry = "https://pypi.org/simple" }
453 | dependencies = [
454 |     { name = "distlib" },
455 |     { name = "filelock" },
456 |     { name = "platformdirs" },
457 | ]
458 | sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
459 | wheels = [
460 |     { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
461 | ]
462 | 


--------------------------------------------------------------------------------