pax_global_header00006660000000000000000000000064152542300600014507gustar00rootroot0000000000000052 comment=e2b8d84639504e0a0857dae48eb5b03b6ac530cb python-asyncmy-0.2.15/000077500000000000000000000000001525423006000145765ustar00rootroot00000000000000python-asyncmy-0.2.15/.github/000077500000000000000000000000001525423006000161365ustar00rootroot00000000000000python-asyncmy-0.2.15/.github/workflows/000077500000000000000000000000001525423006000201735ustar00rootroot00000000000000python-asyncmy-0.2.15/.github/workflows/ci.yml000066400000000000000000000044571525423006000213230ustar00rootroot00000000000000name: ci on: push: branches: - dev paths-ignore: - "**.md" - "images/**" pull_request: paths-ignore: - "**.md" - "images/**" # A new push supersedes whatever is still running for the same ref. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # Lint and type-check once, not once per matrix entry. check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.14" - uses: astral-sh/setup-uv@v7 - name: Run check run: uv run make check ci_mysql: runs-on: ubuntu-latest services: database: image: mysql:latest ports: - 3306:3306 env: MYSQL_ROOT_PASSWORD: 123456 options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 strategy: fail-fast: false matrix: # 3.14t verifies the freethreading_compatible declarations: without # them, importing asyncmy re-enables the GIL for the whole process. python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: astral-sh/setup-uv@v7 - name: Run tests run: uv run make test # MariaDB exercises server-specific paths (extended capabilities, # COM_STMT_BULK_EXECUTE), which are orthogonal to the Python version — so the # oldest and newest supported interpreters are enough here. ci_mariadb: runs-on: ubuntu-latest services: database: image: mariadb:latest ports: - 3306:3306 env: MYSQL_ROOT_PASSWORD: 123456 options: --health-cmd="mariadb-admin ping -uroot -p${MYSQL_ROOT_PASSWORD}" --health-interval 10s --health-timeout 5s --health-retries 5 strategy: fail-fast: false matrix: python-version: ["3.9", "3.14"] steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: astral-sh/setup-uv@v7 - name: Run tests run: uv run make test python-asyncmy-0.2.15/.github/workflows/pypi.yml000066400000000000000000000023511525423006000217000ustar00rootroot00000000000000name: pypi on: release: types: - created jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-15-intel, macos-latest] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Build wheels uses: pypa/cibuildwheel@v3.3.1 - uses: actions/upload-artifact@v5 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl build_sdist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: astral-sh/setup-uv@v7 - name: Build run: uv build - uses: actions/upload-artifact@v5 with: name: cibw-sdist path: dist/*.tar.gz upload: runs-on: ubuntu-latest needs: [build_wheels, build_sdist] steps: - uses: actions/download-artifact@v5 with: pattern: cibw-* path: dist merge-multiple: true - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} python-asyncmy-0.2.15/.gitignore000066400000000000000000000034461525423006000165750ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .idea asyncmy/*.c cython_debug python-asyncmy-0.2.15/CHANGELOG.md000066400000000000000000000217461525423006000164210ustar00rootroot00000000000000# ChangeLog ## 0.2 ### 0.2.15 - Fix `read_timeout` losing data, EOF, or error notifications when the transport callback runs before the read waiter is registered, notably on Python 3.9/3.10 (#155). - Close the in-flight connection when `Pool.wait_closed()` is cancelled, and serialize concurrent drainers so shutdown cannot report completion before connections close (#156). - Raise DBAPI `OperationalError` for writes to closed transports (including uvloop) so SQLAlchemy's `pool_pre_ping` can recognize the disconnect and replace the connection (#158). - Fix `Pool.wait_closed()` blocking forever when the last connection was released while inside a transaction or after disconnecting, and notify pool waiters when a pool-filling `connect()` raises (#154). - Fix `Pool.terminate()` not notifying `wait_closed()` waiters parked on the pool's condition, so a `wait_closed()` call already in flight when `terminate()` runs would hang forever even though the pool was fully drained (#157). ### 0.2.14 - Set the PEP 249 module globals `apilevel`, `threadsafety` and `paramstyle`, and export the exception classes from `asyncmy` (#77). - Expose the server's SQLSTATE on exceptions as `MySQLError.sqlstate`; `args` is unchanged (#138). - Fix `ssl=True` silently connecting in plaintext: it now builds a default TLS context, and an ssl argument that is neither `True`, a dict, nor an `ssl.SSLContext` raises `ValueError` instead of disabling TLS (#90). - `Connection.cursor()` is now awaitable, so aiomysql's `cur = await conn.cursor()` spelling works; `conn.cursor()` and `async with conn.cursor()` are unaffected (#145). - Add `Gtid.__hash__` / `GtidSet.__hash__` — defining `__eq__` had made `Gtid` unhashable, which made `GtidSet` (and therefore GTID replication) unusable (#59). - Declare the Cython modules free-threading compatible. Without this, importing asyncmy re-enables the GIL for the whole process on a free-threaded build. Module-level state is built during import and read-only afterwards; this does not make a single `Connection`/`Cursor` safe to share between threads. Requires Cython 3.1+, and CI now runs the suite on 3.14t. Thanks @honglei (#151). - Read the version from the installed package metadata instead of repeating it in `asyncmy/version.py`, so `pyproject.toml` is the single source and the `_client_version` the server sees cannot drift. Thanks @waketzheng (#149). - Ship type information (PEP 561): `py.typed` plus `.pyi` stubs for the Cython modules, so mypy and Pylance see real signatures instead of nothing. `make stubs` regenerates them and `make check` runs `stubtest`, which fails on any drift from the compiled modules (#78). - Add `connect(password_creator=...)`: a callable consulted before every connection attempt, including the ones a pool makes when it recycles or reconnects, so short-lived credentials such as AWS RDS IAM tokens can be refreshed. May return an awaitable. Thanks @DolevGabay (#139). - Fix `ping(reconnect=True)` never actually reconnecting: `_connected` stayed set when the ping failed, so `connect()` returned early and the follow-up ping ran on the dead connection. - Add `connect(query_callback=...)`, called as `callback(cursor, query, elapsed_ms)` after every statement. Lets statement logging go somewhere other than `echo`'s hardcoded INFO line — a slow-query log, a tracing span, a different level. `executemany`/`callproc` report once per call. Independent of `echo`, which is unchanged (#81, #69). - Add `connect(sock=...)`: speak MySQL over a socket the caller has already connected. Combined with `ssl` the TLS handshake runs before the MySQL handshake instead of being negotiated in-protocol, which is what connectors fronting the server with a TLS proxy need (#71). - Mark the connection secure after the TLS handshake. `_secure` was only set for unix sockets, so `caching_sha2_password` full authentication over TLS took the RSA branch instead of sending the password in the clear, and the server rejected it with `1045 Access denied` (#117). ### 0.2.13 - Add server-side prepared statements (binary protocol): `stmt = await conn.prepare(sql)`, `await stmt.execute(args)`. Parameters are sent in binary form (no client-side escaping) and results are parsed from the binary protocol — no text parsing for numeric/temporal columns. Large scans are ~35% faster than the text protocol; on the cross-language benchmark asyncmy's binary scan is now the fastest, ahead of go-sql-driver and mysql_async. - Add transparent statement cache: `connect(stmt_cache_size=N)` makes plain `cursor.execute("... %s ...", args)` run as cached server-side prepared statements (binary protocol) with silent text-protocol fallback for unpreparable queries. Opt-in because FLOAT columns return the exact stored value under the binary protocol. - Replace StreamReader with a custom `asyncio.BufferedProtocol`: incoming bytes land directly in the parse buffer (zero-copy receive, no per-read Future round-trips). - Cache prepared-statement column metadata: repeated executes skip re-parsing column definition packets entirely (C-level packet skipping). - SSCursor now bulk-parses buffered rows (unbuffered scans ~10% faster on top of 0.2.12's 2.8x). - DictCursor builds row dicts at C level. - Fix `LOAD DATA LOCAL INFILE` sending (`await` on a non-coroutine) and add write backpressure. - Build Linux aarch64 wheels. - Negotiate `CLIENT_DEPRECATE_EOF`: one packet less per result set and per prepare (both MySQL 5.7.5+ and MariaDB support it; legacy EOF path kept for older servers). - MariaDB: support `COM_STMT_BULK_EXECUTE` — `executemany` binds all rows in binary form in one round-trip when `stmt_cache_size` is enabled (automatic text fallback); also available explicitly via `stmt.execute_bulk(rows)`. - Fix `conftest.py` ignoring a non-default `MYSQL_PORT`. ### 0.2.12 - Major performance improvement: buffered packet reading, C-level bulk row parsing, pointer-based protocol reads, direct cell decoding via CPython C-API, zero-decode numeric/temporal columns, escape fast path. Large result sets are 5-15x faster; asyncmy now ranks #1 in all benchmarks, details see [benchmark/README.md](benchmark/README.md). - Fix `OKPacketWrapper.message` containing 2 stray bytes (read_struct position bug). - Fix `LoadLocalPacketWrapper` missing attribute declarations (LOAD DATA LOCAL crash). - Fix potential integer overflow of `rowcount`/`insert_id` on unbuffered cursors and Windows. - Benchmark suite now uses warmup + best-of-3 methodology. - Security: remove unsafe `escape_dict` — dict keys could reach SQL unescaped (CVE-2025-65896). (#134, #135, thanks @Cycloctane) - Fix `AttributeError: 'Connection' object has no attribute 'ssl'` in `sha256_password` auth branch. (#147, #148, thanks @shychee) - Support MySQL 8.0.19+ `INSERT ... AS alias ON DUPLICATE KEY UPDATE` syntax in `executemany`. (#116, #120, thanks @MarkReedZ) - Pool closes idle/recycled connections with QUIT instead of aborting the TCP stream. (#112, #113, thanks @Cycloctane) - Fix `OverflowError` when escaping ints outside the signed 64-bit range, e.g. unsigned BIGINT `2**64-1`. (#35, #127) - Close connection when a query is cancelled mid-read to prevent stale results leaking into pooled reuse. (#107, #108) - Use `setuptools` instead of deprecated `distutils` in build. (#106, thanks @tijuca) ### 0.2.11 - Fix `'Connection' object has no attribute '_auth_plugin_name'` (#86) ### 0.2.10 - Fix ssl context pass bool. - Fix missing `*.whl` for Python 3.12 (#94) - Fix SSL handshake error with MySQL server v8.0.34+. (#80) ### 0.2.9 - Added support for SSL context creation via `ssl` parameter using a dictionary containing `mysql_ssl_set` parameters. ( #64) - Fix bug with fallback encoder in the `escape_item()` function. (#65) ### 0.2.8 - Fix sudden loss of float precision. (#56) - Fix pool `echo` parameter not apply to create connection. (#62) - Fix replication reconnect. ### 0.2.7 - Fix `No module named 'asyncmy.connection'`. ### 0.2.6 - Fix raise_mysql_exception (#28) - Implement `read_timeout` and remove `write_timeout` parameters (#44) ### 0.2.5 - Revert `TIME` return `datetime.time` object. (#37) ### 0.2.4 - Fix `escape_string` for enum type. (#30) - `TIME` return `datetime.time` object. ### 0.2.3 - Fix `escape_sequence`. (#20) - Fix `connection.autocommit`. (#21) - Fix `_clear_result`. (#22) ### 0.2.2 - Fix bug. (#18) - Fix replication error. ### 0.2.1 - Fix `binlogstream` await. (#12) - Remove `loop` argument. (#15) - Fix `unix_socket` connect. (#17) ### 0.2.0 - Fix `cursor.close`. ## 0.1 ### 0.1.9 - Force int `pool_recycle`. - Fix `echo` option. - Fix bug replication and now don't need to connect manual. ### 0.1.8 - Fix pool recycle. (#4) - Fix async `fetchone`, `fetchall`, and `fetchmany`. (#7) ### 0.1.7 - Fix negative pk. (#2) ### 0.1.6 - Bug fix. ### 0.1.5 - Remove `byte2int` and `int2byte`. - Fix warning for sql_mode. ### 0.1.4 - Add replication support. ### 0.1.3 - Fix pool. ### 0.1.2 - Fix build error. ### 0.1.1 - Fix build error. ### 0.1.0 - Release first version. python-asyncmy-0.2.15/LICENSE000066400000000000000000000261351525423006000156120ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-asyncmy-0.2.15/Makefile000066400000000000000000000020161525423006000162350ustar00rootroot00000000000000checkfiles = asyncmy/ tests/ examples/ scripts/ conftest.py build_cython.py py_warn = PYTHONDEVMODE=1 MYSQL_PASS ?= "123456" up: @uv lock --upgrade $(MAKE) deps options=--frozen deps: uv sync --all-groups $(options) _style: @ruff format $(checkfiles) @ruff check --fix $(checkfiles) style: deps _style _stubtest: @stubtest asyncmy --mypy-config-file pyproject.toml --allowlist stubtest_allowlist.txt \ --ignore-missing-stub --ignore-disjoint-bases --ignore-positional-only _check: @ruff format --check $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false) @ruff check $(checkfiles) @mypy $(checkfiles) $(MAKE) _stubtest stubs: deps @python scripts/gen_stubs.py $(MAKE) _stubtest check: deps _check _test: $(py_warn) MYSQL_PASS=$(MYSQL_PASS) pytest test: deps _test clean: @rm -rf *.so && rm -rf build && rm -rf dist && rm -rf asyncmy/*.c && rm -rf asyncmy/*.so && rm -rf asyncmy/*.html build: clean @uv build benchmark: deps @python -m benchmark.run_all ci: deps _check _test python-asyncmy-0.2.15/README.md000066400000000000000000000247511525423006000160660ustar00rootroot00000000000000# asyncmy — The fastest asyncio MySQL/MariaDB driver [![PyPI](https://img.shields.io/pypi/v/asyncmy.svg)](https://pypi.org/pypi/asyncmy) [![License](https://img.shields.io/github/license/long2ice/asyncmy)](https://github.com/long2ice/asyncmy) [![CI](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml) [![Release](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml) `asyncmy` is the fastest asyncio MySQL/MariaDB driver for Python. It keeps the familiar [aiomysql](https://github.com/aio-libs/aiomysql) API while rewriting the entire protocol core in [Cython](https://cython.org/) — down to pointer-level packet parsing. In [our benchmarks](./benchmark/README.md) it outperforms every driver tested, including the C-based synchronous `mysqlclient`. ## Features - 🚀 **Fastest in every benchmark** — reads large result sets 2.1x faster than `mysqlclient` and 5x faster than `aiomysql`/`pymysql` ([details](./benchmark/README.md)) - 🔌 **Drop-in aiomysql replacement** — same API, same cursors (`DictCursor`, `SSCursor`), same pool semantics - 🧬 **Server-side prepared statements** (binary protocol) via `conn.prepare()` — no client-side escaping, no text parsing; large scans another ~35% faster than the text protocol - âš¡ **C-speed protocol core** — rows are parsed in bulk from the receive buffer in a single C loop, values decode straight from wire bytes via the CPython C-API - ðŸ�Š **Built-in connection pool** — `asyncmy.create_pool()`, no extra dependency, 2x aiomysql's pooled throughput - 📡 **MySQL replication protocol** over asyncio ([BinLogStream](https://github.com/long2ice/asyncmy/blob/dev/asyncmy/replication/binlogstream.py)) - ✅ **CI-tested on MySQL and MariaDB** ([workflow](https://github.com/long2ice/asyncmy/blob/dev/.github/workflows/ci.yml)) ## Benchmark asyncmy ranks **#1 in all four scenarios** against `mysqlclient`, `pymysql`, and `aiomysql` (warmup + best-of-3, see [methodology](./benchmark/README.md#methodology)): | Test | asyncmy Rank | Performance | | ---- | ------------ | ----------- | | **Large Result Set** (33k rows, all types) | ðŸ�† **#1/4** | 0.030s — 2.2x faster than mysqlclient, 5.3x faster than aiomysql | | **Connection Pool** (2k queries) | ðŸ�† **#1/2** | ~17,000 qps — 2x aiomysql's throughput | | **Concurrent Queries** (50 connections) | ðŸ�† **#1/2** | ~8,000 qps — 1.6x faster than aiomysql | | **Batch Insert** (10k rows) | ðŸ�† **#1/4** | ~107,000 rows/sec — fastest of all four drivers | The protocol core is engineered for zero waste on the hot path: - **Bulk packet parsing**: one socket read serves hundreds of row packets, parsed in a single C loop with no event-loop round-trips - **Pointer-based protocol reads**: integers and length-encoded values are read directly from raw memory, no `struct` calls - **Direct row decoding**: cell values decode straight from the receive buffer via the CPython C-API (`PyUnicode_DecodeUTF8`, `PyTuple_New`), skipping intermediate objects - **Zero-decode numeric/temporal columns**: `int`/`float`/`datetime` values parse directly from bytes, and dates are built with the C datetime API - **Escape fast path**: strings without special characters are returned as-is, no translation pass 📊 **[View detailed benchmarks →](./benchmark/README.md)** ## Install **Requirements:** Python ≥ 3.9 ```bash pip install asyncmy ``` ### Windows asyncmy uses Cython extensions; on Windows you need **Microsoft C++ Build Tools** to build them. 1. Download [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). 2. Open CMD as Administrator (recommended) and `cd` to the folder **where** the installer was downloaded. 3. Rename the installer (e.g. `vs_buildtools__XXXXXXXXX.XXXXXXXXXX.exe`) to `vs_buildtools.exe` for convenience. 4. Run (ensure ~5–6GB free disk space): ```bash vs_buildtools.exe --norestart --passive --downloadThenInstall --includeRecommended --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools ``` 5. Wait for installation to complete, then restart your computer. 6. Install asyncmy: ```bash pip install asyncmy ``` You can uninstall the Build Tools afterward if desired. ## Usage ### `connect` Use `asyncmy.connect()` for a single connection. For many concurrent connections, use a [connection pool](#pool). ```py import asyncio import os from asyncmy import connect from asyncmy.cursors import DictCursor async def main(): conn = await connect( user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD", ""), ) async with conn.cursor(cursor=DictCursor) as cursor: await cursor.execute("CREATE DATABASE IF NOT EXISTS test") await cursor.execute(""" CREATE TABLE IF NOT EXISTS test.`asyncmy` ( `id` int PRIMARY KEY AUTO_INCREMENT, `decimal` decimal(10, 2), `date` date, `datetime` datetime, `float` float, `string` varchar(200), `tinyint` tinyint ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci """.strip()) await conn.ensure_closed() if __name__ == "__main__": asyncio.run(main()) ``` ### Prepared statements (binary protocol) For repeated queries, server-side prepared statements skip client-side escaping entirely and read results in MySQL's binary protocol — numeric and temporal columns decode natively with no text parsing. Placeholders use native `?` syntax. ```py stmt = await conn.prepare("SELECT id, name FROM users WHERE id = ?") result = await stmt.execute((42,)) print(result.rows) # tuple of row tuples print(result.affected_rows) # for INSERT/UPDATE/DELETE await stmt.close() # or as a context manager async with await conn.prepare("SELECT ? + ?") as stmt: result = await stmt.execute((1, 2)) ``` **Transparent mode:** pass `stmt_cache_size=N` to `connect()`/`create_pool()` and regular `cursor.execute("... %s ...", args)` calls automatically run as cached server-side prepared statements — no code changes needed (ORMs benefit too). Queries the server can't prepare fall back to the text protocol silently. ```py pool = await asyncmy.create_pool(stmt_cache_size=128, ...) ``` Note: with the binary protocol, `FLOAT` columns return the exact stored value rather than the text protocol's decimal-rounded rendering, which is why this is opt-in. ### Pool For multiple connections, use a connection pool. Pass the same kwargs as `connect()` (e.g. `host`, `user`, `password`). ```py import asyncio import asyncmy async def main(): pool = await asyncmy.create_pool(host="localhost", user="root", password="") async with pool.acquire() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT 1") ret = await cursor.fetchone() assert ret == (1,) pool.close() await pool.wait_closed() if __name__ == "__main__": asyncio.run(main()) ``` ### Type checking asyncmy ships `py.typed` and stubs for its compiled modules, so mypy and Pylance resolve the API without extra configuration: ```py conn = await asyncmy.connect(host="localhost", user="root") # -> Connection async with conn.cursor() as cur: rows = await cur.fetchall() # -> list[Any] ``` Contributors: `make stubs` regenerates the stubs after changing a `.pyx` signature. `make check` runs `stubtest`, which compares every stub against the compiled module and fails on drift. ### Rotating credentials Some credentials expire while a pooled connection outlives them — AWS RDS IAM auth tokens last 15 minutes, for instance. Pass `password_creator` instead of `password` and it is consulted before every connection attempt, including the ones the pool makes on its own when it recycles or reconnects: ```py import boto3 client = boto3.client("rds") def rds_auth_token(): return client.generate_db_auth_token( DBHostname="mydb.cluster.amazonaws.com", Port=3306, DBUsername="dbuser", Region="us-east-1", ) pool = await asyncmy.create_pool( host="mydb.cluster.amazonaws.com", user="dbuser", password_creator=rds_auth_token, ) ``` The callable may be a plain function or return an awaitable, and must return `str` or `bytes`. If both `password` and `password_creator` are given, the creator wins. ### Statement logging `echo=True` logs every statement and its duration to the `asyncmy` logger at INFO level. Nothing appears until logging is configured — `logging.basicConfig(level=logging.INFO)` at minimum, since the root logger defaults to WARNING. For anything beyond that, pass `query_callback`. It is called as `callback(cursor, query, elapsed_ms)` after every successful statement, with the duration as a float in milliseconds: ```py import logging logger = logging.getLogger("myapp.sql") def log_slow_queries(cursor, query, elapsed_ms): if elapsed_ms > 100: logger.warning("[%sms] %s", elapsed_ms, query) conn = await asyncmy.connect(host="localhost", user="root", query_callback=log_slow_queries) ``` `executemany` and `callproc` report once for the whole call rather than once per row. The callback is independent of `echo`: set both and you get the log line and the callback. ## Replication asyncmy supports the MySQL replication protocol (like [python-mysql-replication](https://github.com/noplay/python-mysql-replication)) over asyncio. ```py import asyncio from asyncmy import connect from asyncmy.replication import BinLogStream async def main(): conn = await connect() ctl_conn = await connect() stream = BinLogStream( conn, ctl_conn, server_id=1, master_log_file="binlog.000172", master_log_position=2235312, resume_stream=True, blocking=True, ) async for event in stream: print(event) await conn.ensure_closed() await ctl_conn.ensure_closed() if __name__ == "__main__": asyncio.run(main()) ``` ## Acknowledgments asyncmy builds on these projects: - [PyMySQL](https://github.com/PyMySQL/PyMySQL) — pure Python MySQL client - [aiomysql](https://github.com/aio-libs/aiomysql) — asyncio MySQL driver - [python-mysql-replication](https://github.com/noplay/python-mysql-replication) — MySQL replication protocol (pure Python, on top of PyMySQL) ## License [Apache-2.0](./LICENSE) python-asyncmy-0.2.15/asyncmy/000077500000000000000000000000001525423006000162615ustar00rootroot00000000000000python-asyncmy-0.2.15/asyncmy/__init__.py000066400000000000000000000016111525423006000203710ustar00rootroot00000000000000from .connection import Connection, connect from .errors import ( DatabaseError, DataError, Error, IntegrityError, InterfaceError, InternalError, MySQLError, NotSupportedError, OperationalError, ProgrammingError, Warning, ) from .pool import Pool, create_pool #: Version of the DB-API 2.0 specification this module implements. apilevel = "2.0" #: Threads may share the module, but not connections. threadsafety = 1 #: Placeholders are ``%s`` (positional) and ``%(name)s`` (named). paramstyle = "pyformat" __all__ = [ "Connection", "DataError", "DatabaseError", "Error", "IntegrityError", "InterfaceError", "InternalError", "MySQLError", "NotSupportedError", "OperationalError", "Pool", "ProgrammingError", "Warning", "apilevel", "connect", "create_pool", "paramstyle", "threadsafety", ] python-asyncmy-0.2.15/asyncmy/auth.py000066400000000000000000000153101525423006000175740ustar00rootroot00000000000000""" Implements auth methods """ from .errors import OperationalError try: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding _have_cryptography = True except ImportError: _have_cryptography = False import hashlib from functools import partial SCRAMBLE_LENGTH = 20 sha1_new = partial(hashlib.new, "sha1") # mysql_native_password # https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41 def scramble_native_password(password, message): """Scramble used for mysql_native_password""" if not password: return b"" stage1 = sha1_new(password).digest() stage2 = sha1_new(stage1).digest() s = sha1_new() s.update(message[:SCRAMBLE_LENGTH]) s.update(stage2) result = s.digest() return _my_crypt(result, stage1) def _my_crypt(message1, message2): result = bytearray(message1) for i in range(len(result)): result[i] ^= message2[i] return bytes(result) # MariaDB's client_ed25519-plugin # https://mariadb.com/kb/en/library/connection/#client_ed25519-plugin _nacl_bindings = None def _init_nacl(): global _nacl_bindings try: from nacl import bindings _nacl_bindings = bindings except ImportError: raise RuntimeError("'pynacl' package is required for ed25519_password auth method") def _scalar_clamp(s32): ba = bytearray(s32) ba0 = bytes(bytearray([ba[0] & 248])) ba31 = bytes(bytearray([(ba[31] & 127) | 64])) return ba0 + bytes(s32[1:31]) + ba31 def ed25519_password(password, scramble): """Sign a random scramble with elliptic curve Ed25519. Secret and public key are derived from password. """ # variable names based on rfc8032 section-5.1.6 # if not _nacl_bindings: _init_nacl() # h = SHA512(password) h = hashlib.sha512(password).digest() # s = prune(first_half(h)) s = _scalar_clamp(h[:32]) # r = SHA512(second_half(h) || M) r = hashlib.sha512(h[32:] + scramble).digest() # R = encoded point [r]B r = _nacl_bindings.crypto_core_ed25519_scalar_reduce(r) R = _nacl_bindings.crypto_scalarmult_ed25519_base_noclamp(r) # A = encoded point [s]B A = _nacl_bindings.crypto_scalarmult_ed25519_base_noclamp(s) # k = SHA512(R || A || M) k = hashlib.sha512(R + A + scramble).digest() # S = (k * s + r) mod L k = _nacl_bindings.crypto_core_ed25519_scalar_reduce(k) ks = _nacl_bindings.crypto_core_ed25519_scalar_mul(k, s) S = _nacl_bindings.crypto_core_ed25519_scalar_add(ks, r) # signature = R || S return R + S # sha256_password async def _roundtrip(conn, send_data): conn.write_packet(send_data) pkt = await conn.read_packet() pkt.check_error() return pkt def _xor_password(password, salt): # Trailing NUL character will be added in Auth Switch Request. # See https://github.com/mysql/mysql-server/blob/7d10c82196c8e45554f27c00681474a9fb86d137/sql/auth/sha2_password.cc#L939-L945 salt = salt[:SCRAMBLE_LENGTH] password_bytes = bytearray(password) # salt = bytearray(salt) # for PY2 compat. salt_len = len(salt) for i in range(len(password_bytes)): password_bytes[i] ^= salt[i % salt_len] return bytes(password_bytes) def sha2_rsa_encrypt(password, salt, public_key): """ Encrypt password with salt and public_key. Used for sha256_password and caching_sha2_password. """ if not _have_cryptography: raise RuntimeError( "'cryptography' package is required for sha256_password " "or caching_sha2_password auth methods" ) message = _xor_password(password + b"\0", salt) rsa_key = serialization.load_pem_public_key(public_key, default_backend()) return rsa_key.encrypt( message, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA1()), # nosec:B303 algorithm=hashes.SHA1(), # nosec:B303 label=None, ), ) async def sha256_password_auth(conn, pkt): if conn._secure: data = conn._password + b"\0" return await _roundtrip(conn, data) if pkt.is_auth_switch_request(): conn.salt = pkt.read_all() if not conn._server_public_key and conn._password: pkt = await _roundtrip(conn, b"\1") if pkt.is_extra_auth_data(): conn._server_public_key = pkt.get_all_data()[1:] if conn._password: if not conn._server_public_key: raise OperationalError("Couldn't receive server's public key") data = sha2_rsa_encrypt(conn._password, conn.salt, conn._server_public_key) else: data = b"" return await _roundtrip(conn, data) def scramble_caching_sha2(password, nonce): # (bytes, bytes) -> bytes """Scramble algorithm used in cached_sha2_password fast path. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), nonce)) """ if not password: return b"" p1 = hashlib.sha256(password).digest() p2 = hashlib.sha256(p1).digest() p3 = hashlib.sha256(p2 + nonce).digest() res = bytearray(p1) for i in range(len(p3)): res[i] ^= p3[i] return bytes(res) async def caching_sha2_password_auth(conn, pkt): # No password fast path if not conn._password: return await _roundtrip(conn, b"") if pkt.is_auth_switch_request(): # Try from fast auth conn.salt = pkt.read_all() scrambled = scramble_caching_sha2(conn._password, conn.salt) pkt = await _roundtrip(conn, scrambled) # else: fast auth is tried in initial handshake if not pkt.is_extra_auth_data(): raise OperationalError( "caching sha2: Unknown packet for fast auth: %s" % pkt.get_all_data()[:1] ) # magic numbers: # 2 - request public key # 3 - fast auth succeeded # 4 - need full auth pkt.advance(1) n = pkt.read_uint8() if n == 3: pkt = await conn.read_packet() pkt.check_error() # pkt must be OK packet return pkt if n != 4: raise OperationalError("caching sha2: Unknwon result for fast auth: %s" % n) if conn._secure: return await _roundtrip(conn, conn._password + b"\0") if not conn._server_public_key: pkt = await _roundtrip(conn, b"\x02") # Request public key if not pkt.is_extra_auth_data(): raise OperationalError( "caching sha2: Unknown packet for public key: %s" % pkt.get_all_data()[:1] ) conn._server_public_key = pkt.get_all_data()[1:] data = sha2_rsa_encrypt(conn._password, conn.salt, conn._server_public_key) pkt = await _roundtrip(conn, data) python-asyncmy-0.2.15/asyncmy/charset.pxd000066400000000000000000000000621525423006000204250ustar00rootroot00000000000000cdef dict MB_LENGTH = {8: 1, 33: 3, 88: 2, 91: 2} python-asyncmy-0.2.15/asyncmy/charset.pyi000066400000000000000000000011221525423006000204310ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/charset.pyx class Charset: id: int name: str collation: str def __init__(self, id: int, name: str, collation: str, is_default: str): ... def __repr__(self): ... @property def encoding(self): ... @property def is_binary(self): ... class Charsets: def __init__(self): ... def add(self, c: Charset): ... def by_id(self, id: int): ... def by_name(self, name: str): ... def charset_by_name(name: str): ... def charset_by_id(id: int): ... python-asyncmy-0.2.15/asyncmy/charset.pyx000066400000000000000000000251741525423006000204650ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. cdef class Charset: cdef: public int id public str name, collation int is_default def __init__(self, int id, str name, str collation, str is_default): self.id, self.name, self.collation = id, name, collation self.is_default = is_default == "Yes" def __repr__(self): return "Charset(id=%s, name=%r, collation=%r)" % ( self.id, self.name, self.collation, ) @property def encoding(self): name = self.name if name in ("utf8mb4", "utf8mb3"): return "utf8" if name == "latin1": return "cp1252" if name == "koi8r": return "koi8_r" if name == "koi8u": return "koi8_u" return name @property def is_binary(self): return self.id == 63 cdef class Charsets: cdef: dict _by_id, _by_name def __init__(self): self._by_id = {} self._by_name = {} cpdef add(self, Charset c): self._by_id[c.id] = c if c.is_default: self._by_name[c.name] = c cpdef by_id(self, int id): return self._by_id[id] cpdef by_name(self, str name): return self._by_name.get(name.lower()) _charsets = Charsets() """ Generated with: mysql -N -s -e "select id, character_set_name, collation_name, is_default from information_schema.collations order by id;" | python -c "import sys for l in sys.stdin.readlines(): id, name, collation, is_default = l.split(chr(9)) print '_charsets.add(Charset(%s, \'%s\', \'%s\', \'%s\'))' \ % (id, name, collation, is_default.strip()) " """ _charsets.add(Charset(1, "big5", "big5_chinese_ci", "Yes")) _charsets.add(Charset(2, "latin2", "latin2_czech_cs", "")) _charsets.add(Charset(3, "dec8", "dec8_swedish_ci", "Yes")) _charsets.add(Charset(4, "cp850", "cp850_general_ci", "Yes")) _charsets.add(Charset(5, "latin1", "latin1_german1_ci", "")) _charsets.add(Charset(6, "hp8", "hp8_english_ci", "Yes")) _charsets.add(Charset(7, "koi8r", "koi8r_general_ci", "Yes")) _charsets.add(Charset(8, "latin1", "latin1_swedish_ci", "Yes")) _charsets.add(Charset(9, "latin2", "latin2_general_ci", "Yes")) _charsets.add(Charset(10, "swe7", "swe7_swedish_ci", "Yes")) _charsets.add(Charset(11, "ascii", "ascii_general_ci", "Yes")) _charsets.add(Charset(12, "ujis", "ujis_japanese_ci", "Yes")) _charsets.add(Charset(13, "sjis", "sjis_japanese_ci", "Yes")) _charsets.add(Charset(14, "cp1251", "cp1251_bulgarian_ci", "")) _charsets.add(Charset(15, "latin1", "latin1_danish_ci", "")) _charsets.add(Charset(16, "hebrew", "hebrew_general_ci", "Yes")) _charsets.add(Charset(18, "tis620", "tis620_thai_ci", "Yes")) _charsets.add(Charset(19, "euckr", "euckr_korean_ci", "Yes")) _charsets.add(Charset(20, "latin7", "latin7_estonian_cs", "")) _charsets.add(Charset(21, "latin2", "latin2_hungarian_ci", "")) _charsets.add(Charset(22, "koi8u", "koi8u_general_ci", "Yes")) _charsets.add(Charset(23, "cp1251", "cp1251_ukrainian_ci", "")) _charsets.add(Charset(24, "gb2312", "gb2312_chinese_ci", "Yes")) _charsets.add(Charset(25, "greek", "greek_general_ci", "Yes")) _charsets.add(Charset(26, "cp1250", "cp1250_general_ci", "Yes")) _charsets.add(Charset(27, "latin2", "latin2_croatian_ci", "")) _charsets.add(Charset(28, "gbk", "gbk_chinese_ci", "Yes")) _charsets.add(Charset(29, "cp1257", "cp1257_lithuanian_ci", "")) _charsets.add(Charset(30, "latin5", "latin5_turkish_ci", "Yes")) _charsets.add(Charset(31, "latin1", "latin1_german2_ci", "")) _charsets.add(Charset(32, "armscii8", "armscii8_general_ci", "Yes")) _charsets.add(Charset(33, "utf8", "utf8_general_ci", "Yes")) _charsets.add(Charset(34, "cp1250", "cp1250_czech_cs", "")) _charsets.add(Charset(36, "cp866", "cp866_general_ci", "Yes")) _charsets.add(Charset(37, "keybcs2", "keybcs2_general_ci", "Yes")) _charsets.add(Charset(38, "macce", "macce_general_ci", "Yes")) _charsets.add(Charset(39, "macroman", "macroman_general_ci", "Yes")) _charsets.add(Charset(40, "cp852", "cp852_general_ci", "Yes")) _charsets.add(Charset(41, "latin7", "latin7_general_ci", "Yes")) _charsets.add(Charset(42, "latin7", "latin7_general_cs", "")) _charsets.add(Charset(43, "macce", "macce_bin", "")) _charsets.add(Charset(44, "cp1250", "cp1250_croatian_ci", "")) _charsets.add(Charset(45, "utf8mb4", "utf8mb4_general_ci", "Yes")) _charsets.add(Charset(46, "utf8mb4", "utf8mb4_bin", "")) _charsets.add(Charset(47, "latin1", "latin1_bin", "")) _charsets.add(Charset(48, "latin1", "latin1_general_ci", "")) _charsets.add(Charset(49, "latin1", "latin1_general_cs", "")) _charsets.add(Charset(50, "cp1251", "cp1251_bin", "")) _charsets.add(Charset(51, "cp1251", "cp1251_general_ci", "Yes")) _charsets.add(Charset(52, "cp1251", "cp1251_general_cs", "")) _charsets.add(Charset(53, "macroman", "macroman_bin", "")) _charsets.add(Charset(57, "cp1256", "cp1256_general_ci", "Yes")) _charsets.add(Charset(58, "cp1257", "cp1257_bin", "")) _charsets.add(Charset(59, "cp1257", "cp1257_general_ci", "Yes")) _charsets.add(Charset(63, "binary", "binary", "Yes")) _charsets.add(Charset(64, "armscii8", "armscii8_bin", "")) _charsets.add(Charset(65, "ascii", "ascii_bin", "")) _charsets.add(Charset(66, "cp1250", "cp1250_bin", "")) _charsets.add(Charset(67, "cp1256", "cp1256_bin", "")) _charsets.add(Charset(68, "cp866", "cp866_bin", "")) _charsets.add(Charset(69, "dec8", "dec8_bin", "")) _charsets.add(Charset(70, "greek", "greek_bin", "")) _charsets.add(Charset(71, "hebrew", "hebrew_bin", "")) _charsets.add(Charset(72, "hp8", "hp8_bin", "")) _charsets.add(Charset(73, "keybcs2", "keybcs2_bin", "")) _charsets.add(Charset(74, "koi8r", "koi8r_bin", "")) _charsets.add(Charset(75, "koi8u", "koi8u_bin", "")) _charsets.add(Charset(76, "utf8", "utf8_tolower_ci", "")) _charsets.add(Charset(77, "latin2", "latin2_bin", "")) _charsets.add(Charset(78, "latin5", "latin5_bin", "")) _charsets.add(Charset(79, "latin7", "latin7_bin", "")) _charsets.add(Charset(80, "cp850", "cp850_bin", "")) _charsets.add(Charset(81, "cp852", "cp852_bin", "")) _charsets.add(Charset(82, "swe7", "swe7_bin", "")) _charsets.add(Charset(83, "utf8", "utf8_bin", "")) _charsets.add(Charset(84, "big5", "big5_bin", "")) _charsets.add(Charset(85, "euckr", "euckr_bin", "")) _charsets.add(Charset(86, "gb2312", "gb2312_bin", "")) _charsets.add(Charset(87, "gbk", "gbk_bin", "")) _charsets.add(Charset(88, "sjis", "sjis_bin", "")) _charsets.add(Charset(89, "tis620", "tis620_bin", "")) _charsets.add(Charset(91, "ujis", "ujis_bin", "")) _charsets.add(Charset(92, "geostd8", "geostd8_general_ci", "Yes")) _charsets.add(Charset(93, "geostd8", "geostd8_bin", "")) _charsets.add(Charset(94, "latin1", "latin1_spanish_ci", "")) _charsets.add(Charset(95, "cp932", "cp932_japanese_ci", "Yes")) _charsets.add(Charset(96, "cp932", "cp932_bin", "")) _charsets.add(Charset(97, "eucjpms", "eucjpms_japanese_ci", "Yes")) _charsets.add(Charset(98, "eucjpms", "eucjpms_bin", "")) _charsets.add(Charset(99, "cp1250", "cp1250_polish_ci", "")) _charsets.add(Charset(192, "utf8", "utf8_unicode_ci", "")) _charsets.add(Charset(193, "utf8", "utf8_icelandic_ci", "")) _charsets.add(Charset(194, "utf8", "utf8_latvian_ci", "")) _charsets.add(Charset(195, "utf8", "utf8_romanian_ci", "")) _charsets.add(Charset(196, "utf8", "utf8_slovenian_ci", "")) _charsets.add(Charset(197, "utf8", "utf8_polish_ci", "")) _charsets.add(Charset(198, "utf8", "utf8_estonian_ci", "")) _charsets.add(Charset(199, "utf8", "utf8_spanish_ci", "")) _charsets.add(Charset(200, "utf8", "utf8_swedish_ci", "")) _charsets.add(Charset(201, "utf8", "utf8_turkish_ci", "")) _charsets.add(Charset(202, "utf8", "utf8_czech_ci", "")) _charsets.add(Charset(203, "utf8", "utf8_danish_ci", "")) _charsets.add(Charset(204, "utf8", "utf8_lithuanian_ci", "")) _charsets.add(Charset(205, "utf8", "utf8_slovak_ci", "")) _charsets.add(Charset(206, "utf8", "utf8_spanish2_ci", "")) _charsets.add(Charset(207, "utf8", "utf8_roman_ci", "")) _charsets.add(Charset(208, "utf8", "utf8_persian_ci", "")) _charsets.add(Charset(209, "utf8", "utf8_esperanto_ci", "")) _charsets.add(Charset(210, "utf8", "utf8_hungarian_ci", "")) _charsets.add(Charset(211, "utf8", "utf8_sinhala_ci", "")) _charsets.add(Charset(212, "utf8", "utf8_german2_ci", "")) _charsets.add(Charset(213, "utf8", "utf8_croatian_ci", "")) _charsets.add(Charset(214, "utf8", "utf8_unicode_520_ci", "")) _charsets.add(Charset(215, "utf8", "utf8_vietnamese_ci", "")) _charsets.add(Charset(223, "utf8", "utf8_general_mysql500_ci", "")) _charsets.add(Charset(224, "utf8mb4", "utf8mb4_unicode_ci", "")) _charsets.add(Charset(225, "utf8mb4", "utf8mb4_icelandic_ci", "")) _charsets.add(Charset(226, "utf8mb4", "utf8mb4_latvian_ci", "")) _charsets.add(Charset(227, "utf8mb4", "utf8mb4_romanian_ci", "")) _charsets.add(Charset(228, "utf8mb4", "utf8mb4_slovenian_ci", "")) _charsets.add(Charset(229, "utf8mb4", "utf8mb4_polish_ci", "")) _charsets.add(Charset(230, "utf8mb4", "utf8mb4_estonian_ci", "")) _charsets.add(Charset(231, "utf8mb4", "utf8mb4_spanish_ci", "")) _charsets.add(Charset(232, "utf8mb4", "utf8mb4_swedish_ci", "")) _charsets.add(Charset(233, "utf8mb4", "utf8mb4_turkish_ci", "")) _charsets.add(Charset(234, "utf8mb4", "utf8mb4_czech_ci", "")) _charsets.add(Charset(235, "utf8mb4", "utf8mb4_danish_ci", "")) _charsets.add(Charset(236, "utf8mb4", "utf8mb4_lithuanian_ci", "")) _charsets.add(Charset(237, "utf8mb4", "utf8mb4_slovak_ci", "")) _charsets.add(Charset(238, "utf8mb4", "utf8mb4_spanish2_ci", "")) _charsets.add(Charset(239, "utf8mb4", "utf8mb4_roman_ci", "")) _charsets.add(Charset(240, "utf8mb4", "utf8mb4_persian_ci", "")) _charsets.add(Charset(241, "utf8mb4", "utf8mb4_esperanto_ci", "")) _charsets.add(Charset(242, "utf8mb4", "utf8mb4_hungarian_ci", "")) _charsets.add(Charset(243, "utf8mb4", "utf8mb4_sinhala_ci", "")) _charsets.add(Charset(244, "utf8mb4", "utf8mb4_german2_ci", "")) _charsets.add(Charset(245, "utf8mb4", "utf8mb4_croatian_ci", "")) _charsets.add(Charset(246, "utf8mb4", "utf8mb4_unicode_520_ci", "")) _charsets.add(Charset(247, "utf8mb4", "utf8mb4_vietnamese_ci", "")) _charsets.add(Charset(248, "gb18030", "gb18030_chinese_ci", "Yes")) _charsets.add(Charset(249, "gb18030", "gb18030_bin", "")) _charsets.add(Charset(250, "gb18030", "gb18030_unicode_520_ci", "")) _charsets.add(Charset(255, "utf8mb4", "utf8mb4_0900_ai_ci", "")) cpdef charset_by_name(str name): return _charsets.by_name(name) cpdef charset_by_id(int id): return _charsets.by_id(id) python-asyncmy-0.2.15/asyncmy/connection.pyi000066400000000000000000000317611525423006000211530ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/connection.pyx from typing_extensions import Self from asyncmy.contexts import _ConnectionContextManager from typing import Any, Optional, Type from asyncmy import errors from asyncmy.cursors import Cursor from asyncmy.protocol import MysqlPacket class Connection: """ Representation of a socket with a mysql server. The proper way to get an instance of this class is to call connect(). Establish a connection to the MySQL database. Accepts several arguments: :param host: Host where the database server is located. :param user: Username to log in as. :param password: Password to use. :param password_creator: Callable returning the password to authenticate with, consulted before every connection attempt — including the ones a pool makes on its own when it recycles or reconnects, which is the point: short-lived credentials such as AWS RDS IAM tokens expire while pooled connections outlive them, and nothing else gives you a hook at that moment. May be a plain function or return an awaitable. Must return ``str`` or ``bytes``. Takes precedence over ``password`` when both are given. :param database: Database to use, None to not use a particular one. :param port: MySQL port to use, default is usually OK. (default: 3306) :param unix_socket: Use a unix socket rather than TCP/IP. :param sock: An already-connected socket to speak MySQL over, instead of connecting to ``host``/``port`` ourselves. Combine it with ``ssl`` to hand the driver a connection whose TLS is set up by the caller rather than negotiated in-protocol — what connectors that front the server with a TLS proxy (e.g. Cloud SQL) need. The socket is used by a single connection and is closed with it, so a pool needs a fresh socket per connection; reconnecting one raises. Keep-alive and TCP_NODELAY are left to the caller. :param read_timeout: The timeout for reading from the connection in seconds (default: None - no timeout) :param charset: Charset to use. :param sql_mode: Default SQL_MODE to use. :param read_default_file: Specifies my.cnf file to read these parameters from under the [client] section. :param conv: Conversion dictionary to use instead of the default one. This is used to provide custom marshalling and unmarshalling of types. See converters. :param use_unicode: Whether or not to default to unicode strings. This option defaults to true. :param client_flag: Custom flags to send to MySQL. Find potential values in constants. :param cursor_cls: Custom cursor class to use. :param query_callback: Called as ``callback(cursor, query, elapsed_ms)`` after every statement, with the duration as a float in milliseconds. Use it to route statement logging wherever you want — a different level than ``echo``'s INFO, a slow-query log, a tracing span. ``executemany`` and ``callproc`` report once for the whole call rather than per row. Independent of ``echo``: setting both logs and calls back. :param init_command: Initial SQL statement to run when connection is established. :param connect_timeout: The timeout for connecting to the database in seconds. (default: 10, min: 1, max: 31536000) :param ssl: Optional dict of arguments similar to mysql_ssl_set()'s parameters or SSL Context to force SSL :param read_default_group: Group to read from in the configuration file. :param autocommit: Autocommit mode. None means use server default. (default: False) :param local_infile: Boolean to enable the use of LOAD DATA LOCAL (default: False) :param max_allowed_packet: Max size of packet sent to server in bytes. (default: 16MB) Only used to limit size of "LOAD LOCAL INFILE" data packet smaller than default (16KB). :param auth_plugin_map: A dict of plugin names to a class that processes that plugin. The class will take the Connection object as the argument to the constructor. The class needs an authenticate method taking an authentication packet as an argument. For the dialog plugin, a prompt(echo, prompt) method can be used (if no authenticate method) for returning a string from the user. (experimental) :param server_public_key: SHA256 authentication plugin public key value. (default: None) :param binary_prefix: Add _binary prefix on bytes and bytearray. (default: False) :param db: **DEPRECATED** Alias for database. See `Connection `_ in the specification. """ Warning = errors.Warning Error = errors.Error InterfaceError = errors.InterfaceError DatabaseError = errors.DatabaseError DataError = errors.DataError OperationalError = errors.OperationalError IntegrityError = errors.IntegrityError InternalError = errors.InternalError ProgrammingError = errors.ProgrammingError NotSupportedError = errors.NotSupportedError # Injected onto the instance by BinLogStream # (replication/binlogstream.py), hence declared but not defined. _get_table_information: Any def __init__( self, *, user=None, password="", password_creator=None, host=None, database=None, unix_socket=None, sock=None, port=0, charset="", sql_mode=None, read_default_file=None, conv=None, use_unicode=True, client_flag=0, cursor_cls=Cursor, init_command=None, connect_timeout=10, read_default_group=None, autocommit=False, local_infile=False, max_allowed_packet=16 * 1024 * 1024, auth_plugin_map=None, read_timeout=None, binary_prefix=False, program_name=None, server_public_key=None, echo=False, query_callback=None, ssl=None, stmt_cache_size=0, db=None, ): ... def close(self) -> None: """Close socket connection""" @property def connected(self): """Return True if the connection is open.""" @property def loop(self): ... @property def last_usage(self): """Return time() when connection was used.""" async def ensure_closed(self) -> None: """Send QUIT message and close connection.""" async def autocommit(self, value): ... def get_autocommit(self) -> bool: ... async def begin(self) -> None: """Begin transaction.""" async def commit(self) -> None: """ Commit changes to stable storage. See `Connection.commit() `_ in the specification. """ async def rollback(self) -> None: """ Roll back the current transaction. See `Connection.rollback() `_ in the specification. """ async def show_warnings(self): """Send the "SHOW WARNINGS" SQL""" async def select_db(self, db) -> None: """ Set current db. :param db: The name of the db. """ def escape(self, obj, mapping=None): """Escape whatever value is passed. Non-standard, for internal use; do not use this in your applications. """ def literal(self, obj): """Alias for escape(). Non-standard, for internal use; do not use this in your applications. """ def escape_string(self, s) -> str: ... def cursor(self, cursor: Optional[Type[Cursor]] | None = None) -> Cursor: """ Create a new cursor to execute queries with. :param cursor: The type of cursor to create. None means use Cursor. :type cursor: :py:class:`Cursor`, :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`. """ async def query(self, sql, unbuffered=False): ... async def next_result(self, unbuffered=False): ... async def prepare(self, query): """Create a server-side prepared statement (binary protocol). Returns a :class:`PreparedStatement`. Executing it skips client-side escaping entirely and reads results in the binary protocol, which is significantly faster for repeated (point) queries. :param query: SQL with ``?`` placeholders (native MySQL syntax). """ def affected_rows(self) -> int: ... async def kill(self, thread_id): ... async def ping(self, reconnect=True) -> None: """ Check if the server is alive. :param reconnect: If the connection is closed, reconnect. :type reconnect: boolean :raise Error: If the connection is closed and reconnect=False. """ async def set_charset(self, charset) -> None: ... async def connect(self) -> _ConnectionContextManager[Connection]: ... def write_packet(self, payload: bytes): """ Writes an entire "mysql packet" in its entirety to the network adding its length and sequence number. """ async def read_packet(self, packet_type=MysqlPacket): """ Read an entire "mysql packet" in its entirety from the network and return a MysqlPacket type that represents the results. :raise OperationalError: If the connection to the MySQL server is lost. :raise InternalError: If the packet sequence number is wrong. """ def insert_id(self) -> int: ... async def __aenter__(self) -> Self: ... async def __aexit__(self, exc_type, exc_val, exc_tb): ... def thread_id(self) -> int: ... def character_set_name(self): ... def get_host_info(self) -> str: ... def get_proto_info(self) -> int: ... def get_transaction_status(self) -> bool: ... def get_server_info(self) -> str: ... class PreparedStatement: """A server-side prepared statement (binary protocol). Created via :meth:`Connection.prepare`. Parameters are sent in binary form (no client-side escaping) and result rows are parsed from the binary protocol (no text parsing for numeric/temporal columns). Notes: - placeholders use native MySQL ``?`` syntax, not ``%s``; - custom ``conv`` decoders apply only to string-typed columns; numeric and temporal columns decode natively; - only the first result set is returned (extra sets are drained). """ def __init__(self, connection, statement_id, parameter_count): ... @property def parameter_count(self): ... async def execute(self, args=()): """Execute with ``args`` bound to the statement's ``?`` placeholders. Returns the result object: ``result.rows`` (tuple of row tuples, None for non-SELECT), ``result.affected_rows``, ``result.insert_id``, ``result.description``. """ async def execute_bulk(self, rows): """MariaDB COM_STMT_BULK_EXECUTE: bind many rows in one round-trip. ``rows`` is a sequence of parameter tuples. Returns the result object (``affected_rows``, ``insert_id``). Only usable when the server advertises MARIADB_CLIENT_STMT_BULK_OPERATIONS. """ async def close(self) -> None: """Deallocate the statement on the server (no server response).""" async def __aenter__(self) -> Self: ... async def __aexit__(self, exc_type, exc_val, exc_tb): ... class MySQLResult: connection: Any message: bytes warning_count: int field_count: int server_status: int unbuffered_active: int has_next: int affected_rows: object fields: list converters: list insert_id: int rows: tuple description: tuple def __init__(self, connection: Connection): ... def __del__(self): ... async def read(self): ... async def read_binary(self, stmt=None): """Read a COM_STMT_EXECUTE response (binary protocol result set).""" async def init_unbuffered_query(self): """ :raise OperationalError: If the connection to the MySQL server is lost. :raise InternalError: """ class LoadLocalFile: def __init__(self, filename: str, connection: Connection): ... async def send_data(self): """ Send data packets from the local file to the server """ def connect( user=None, password="", password_creator=None, host=None, database=None, unix_socket=None, sock=None, port=0, charset="", sql_mode=None, read_default_file=None, conv=None, use_unicode=True, client_flag=0, cursor_cls=Cursor, init_command=None, connect_timeout=10, read_default_group=None, autocommit=False, local_infile=False, max_allowed_packet=16 * 1024 * 1024, auth_plugin_map=None, read_timeout=None, binary_prefix=False, program_name=None, echo=False, query_callback=None, server_public_key=None, ssl=None, stmt_cache_size=0, db=None, ) -> _ConnectionContextManager[Connection]: ... python-asyncmy-0.2.15/asyncmy/connection.pyx000066400000000000000000002430771525423006000211770ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. # Python implementation of the MySQL client-server protocol # http://dev.mysql.com/doc/internals/en/client-server-protocol.html # Error codes: # https://dev.mysql.com/doc/refman/5.5/en/error-handling.html import asyncio import errno import inspect import os import socket import sys import warnings from typing import Optional, Type from cpython.bytearray cimport PyByteArray_AS_STRING from cpython.bytes cimport PyBytes_FromStringAndSize from asyncmy import auth, converters, errors from asyncmy.charset import charset_by_id, charset_by_name from asyncmy.cursors import Cursor from asyncmy.optionfile import Parser from asyncmy.protocol import (EOFPacketWrapper, FieldDescriptorPacket, LoadLocalPacketWrapper, MysqlPacket, OKPacketWrapper, pack_binary_params, pack_bulk_rows, parse_binary_rows_from_buffer, parse_rows_from_buffer, skip_packets_from_buffer) from .constants.CLIENT import (CAPABILITIES, CONNECT_ATTRS, CONNECT_WITH_DB, DEPRECATE_EOF, LOCAL_FILES, MULTI_RESULTS, MULTI_STATEMENTS, PLUGIN_AUTH, PLUGIN_AUTH_LENENC_CLIENT_DATA, SECURE_CONNECTION, SSL) from .constants.COMMAND import (COM_INIT_DB, COM_PING, COM_PROCESS_KILL, COM_QUERY, COM_QUIT, COM_STMT_CLOSE, COM_STMT_EXECUTE, COM_STMT_PREPARE) from .constants.CR import (CR_COMMANDS_OUT_OF_SYNC, CR_CONN_HOST_ERROR, CR_SERVER_GONE_ERROR, CR_SERVER_LOST) from .constants.ER import FILE_NOT_FOUND from .constants.FIELD_TYPE import (BIT, BLOB, GEOMETRY, JSON, LONG_BLOB, MEDIUM_BLOB, STRING, TINY_BLOB, VAR_STRING, VARCHAR) from .constants.SERVER_STATUS import (SERVER_MORE_RESULTS_EXISTS, SERVER_STATUS_AUTOCOMMIT, SERVER_STATUS_IN_TRANS, SERVER_STATUS_NO_BACKSLASH_ESCAPES) from .contexts import _ConnectionContextManager from .structs import B_, BHHB, IIB, B, H, I, Q, i, iB, iIB23s from .version import __VERSION__ try: import ssl SSL_ENABLED = True except ImportError: ssl = None SSL_ENABLED = False try: import getpass DEFAULT_USER = getpass.getuser() del getpass except (ImportError, KeyError, OSError): # When there's no entry in OS database for a current user: # KeyError is raised in Python 3.12 and below. # OSError is raised in Python 3.13+ DEFAULT_USER = None cdef set TEXT_TYPES = { BIT, BLOB, LONG_BLOB, MEDIUM_BLOB, STRING, TINY_BLOB, VAR_STRING, VARCHAR, GEOMETRY, } cdef str DEFAULT_CHARSET = "utf8mb4" cdef int MAX_PACKET_LEN = 2 ** 24 - 1 # Initial size of the protocol receive buffer. cdef int READ_CHUNK_SIZE = 2 ** 18 # MariaDB-specific protocol extensions cdef int COM_STMT_BULK_EXECUTE = 0xFA cdef unsigned int MARIADB_CLIENT_STMT_BULK_OPERATIONS = 1 << 2 # extended-caps dword cdef unsigned int CLIENT_MYSQL = 1 # bit 0: cleared to signal MariaDB awareness # Decoders that accept raw bytes input, allowing us to skip the ascii decode # step entirely for their columns. cdef set _BYTES_SAFE_DECODERS = { int, float, converters.convert_datetime, converters.convert_timedelta, converters.convert_time, converters.convert_date, } cdef inline bytes _take_bytes(bytearray buf, Py_ssize_t pos, Py_ssize_t n): """Copy buf[pos:pos+n] into a fresh bytes object with a single allocation.""" return PyBytes_FromStringAndSize(PyByteArray_AS_STRING(buf) + pos, n) cdef _pyformat_to_qmark(str query): """Convert pyformat ``%s`` placeholders to native ``?`` markers. Returns ``(converted_sql, param_count)`` or None when the query uses constructs the binary path does not support (named ``%(name)s`` placeholders or a stray ``%``). """ cdef: list out = [] Py_ssize_t i = 0, j int nparams = 0 str nxt while True: j = query.find("%", i) if j == -1: out.append(query[i:]) break out.append(query[i:j]) nxt = query[j + 1: j + 2] if nxt == "s": out.append("?") nparams += 1 i = j + 2 elif nxt == "%": out.append("%") i = j + 2 else: return None return "".join(out), nparams # https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::LengthEncodedInteger cdef _lenenc_int(int i): if i < 0: raise ValueError( "Encoding %d is less than 0 - no representation in LengthEncodedInteger" % i ) elif i < 0xFB: return bytes([i]) elif i < (1 << 16): return b"\xfc" + H.pack(i) elif i < (1 << 24): return b"\xfd" + I.pack(i)[:3] elif i < (1 << 64): return b"\xfe" + Q.pack(i) else: raise ValueError( "Encoding %x is larger than %x - no representation in LengthEncodedInteger" % (i, (1 << 64)) ) class _MySQLProtocol(asyncio.BufferedProtocol): """Receive-side transport protocol. Incoming bytes land directly in ``buffer`` (the parse buffer) via the zero-copy BufferedProtocol interface — no StreamReader, no intermediate chunk objects, no second copy. ``buffer[pos:length]`` is the unconsumed region; consumers advance ``pos`` and re-read all three attributes after every await (the buffer may be compacted or reallocated while waiting). """ def __init__(self, loop): self._loop = loop self.transport = None self.buffer = bytearray(READ_CHUNK_SIZE) self.length = 0 # valid bytes in buffer self.pos = 0 # consumed bytes self.eof = False self.exc = None self._read_waiter = None self._drain_waiter = None self._write_paused = False self._closed_waiter = loop.create_future() # -- BufferedProtocol interface -- def connection_made(self, transport): self.transport = transport def get_buffer(self, sizehint): buffer = self.buffer length = self.length pos = self.pos if pos: # compact the consumed prefix if length > pos: buffer[0: length - pos] = buffer[pos:length] length -= pos self.length = length self.pos = 0 if length == 0 and len(buffer) > (READ_CHUNK_SIZE << 2): # shrink an oversized buffer left over from a huge result set self.buffer = buffer = bytearray(READ_CHUNK_SIZE) elif len(buffer) - length < 4096: # grow geometrically so huge packets stay O(n) grow = len(buffer) if sizehint > 0 and sizehint > grow: grow = sizehint buffer.extend(bytes(grow)) return memoryview(buffer)[length:] def buffer_updated(self, nbytes): self.length += nbytes waiter = self._read_waiter if waiter is not None: self._read_waiter = None if not waiter.done(): waiter.set_result(None) def eof_received(self): self.eof = True waiter = self._read_waiter if waiter is not None: self._read_waiter = None if not waiter.done(): waiter.set_result(None) return False def connection_lost(self, exc): self.eof = True if exc is not None: self.exc = exc self.transport = None for waiter in (self._read_waiter, self._drain_waiter): if waiter is not None and not waiter.done(): waiter.set_result(None) self._read_waiter = None self._drain_waiter = None if not self._closed_waiter.done(): self._closed_waiter.set_result(None) def pause_writing(self): self._write_paused = True def resume_writing(self): self._write_paused = False waiter = self._drain_waiter if waiter is not None: self._drain_waiter = None if not waiter.done(): waiter.set_result(None) # -- consumer helpers -- async def wait_for_data(self, need): """Suspend until more bytes arrive (or EOF / connection loss).""" # wait_for() may schedule this coroutine after the transport callback. # Recheck the read predicate before registering a waiter so that data, # EOF, or errors delivered in that gap cannot lose their notification. if self.length - self.pos >= need or self.eof or self.exc is not None: return waiter = self._loop.create_future() self._read_waiter = waiter try: await waiter finally: if self._read_waiter is waiter: self._read_waiter = None async def drain(self): if self._write_paused and self.transport is not None: waiter = self._loop.create_future() self._drain_waiter = waiter await waiter async def wait_closed(self): await self._closed_waiter class Connection: """ Representation of a socket with a mysql server. The proper way to get an instance of this class is to call connect(). Establish a connection to the MySQL database. Accepts several arguments: :param host: Host where the database server is located. :param user: Username to log in as. :param password: Password to use. :param password_creator: Callable returning the password to authenticate with, consulted before every connection attempt — including the ones a pool makes on its own when it recycles or reconnects, which is the point: short-lived credentials such as AWS RDS IAM tokens expire while pooled connections outlive them, and nothing else gives you a hook at that moment. May be a plain function or return an awaitable. Must return ``str`` or ``bytes``. Takes precedence over ``password`` when both are given. :param database: Database to use, None to not use a particular one. :param port: MySQL port to use, default is usually OK. (default: 3306) :param unix_socket: Use a unix socket rather than TCP/IP. :param sock: An already-connected socket to speak MySQL over, instead of connecting to ``host``/``port`` ourselves. Combine it with ``ssl`` to hand the driver a connection whose TLS is set up by the caller rather than negotiated in-protocol — what connectors that front the server with a TLS proxy (e.g. Cloud SQL) need. The socket is used by a single connection and is closed with it, so a pool needs a fresh socket per connection; reconnecting one raises. Keep-alive and TCP_NODELAY are left to the caller. :param read_timeout: The timeout for reading from the connection in seconds (default: None - no timeout) :param charset: Charset to use. :param sql_mode: Default SQL_MODE to use. :param read_default_file: Specifies my.cnf file to read these parameters from under the [client] section. :param conv: Conversion dictionary to use instead of the default one. This is used to provide custom marshalling and unmarshalling of types. See converters. :param use_unicode: Whether or not to default to unicode strings. This option defaults to true. :param client_flag: Custom flags to send to MySQL. Find potential values in constants. :param cursor_cls: Custom cursor class to use. :param query_callback: Called as ``callback(cursor, query, elapsed_ms)`` after every statement, with the duration as a float in milliseconds. Use it to route statement logging wherever you want — a different level than ``echo``'s INFO, a slow-query log, a tracing span. ``executemany`` and ``callproc`` report once for the whole call rather than per row. Independent of ``echo``: setting both logs and calls back. :param init_command: Initial SQL statement to run when connection is established. :param connect_timeout: The timeout for connecting to the database in seconds. (default: 10, min: 1, max: 31536000) :param ssl: Optional dict of arguments similar to mysql_ssl_set()'s parameters or SSL Context to force SSL :param read_default_group: Group to read from in the configuration file. :param autocommit: Autocommit mode. None means use server default. (default: False) :param local_infile: Boolean to enable the use of LOAD DATA LOCAL (default: False) :param max_allowed_packet: Max size of packet sent to server in bytes. (default: 16MB) Only used to limit size of "LOAD LOCAL INFILE" data packet smaller than default (16KB). :param auth_plugin_map: A dict of plugin names to a class that processes that plugin. The class will take the Connection object as the argument to the constructor. The class needs an authenticate method taking an authentication packet as an argument. For the dialog plugin, a prompt(echo, prompt) method can be used (if no authenticate method) for returning a string from the user. (experimental) :param server_public_key: SHA256 authentication plugin public key value. (default: None) :param binary_prefix: Add _binary prefix on bytes and bytearray. (default: False) :param db: **DEPRECATED** Alias for database. See `Connection `_ in the specification. """ def __init__( self, *, user=None, # The first four arguments is based on DB-API 2.0 recommendation. password="", password_creator=None, host=None, database=None, unix_socket=None, sock=None, port=0, charset="", sql_mode=None, read_default_file=None, conv=None, use_unicode=True, client_flag=0, cursor_cls=Cursor, init_command=None, connect_timeout=10, read_default_group=None, autocommit=False, local_infile=False, max_allowed_packet=16 * 1024 * 1024, auth_plugin_map=None, read_timeout=None, binary_prefix=False, program_name=None, server_public_key=None, echo=False, query_callback=None, ssl=None, stmt_cache_size=0, db=None, # deprecated ): self._loop = asyncio.get_event_loop() self._last_usage = self._loop.time() if db is not None and database is None: # We will raise warining in 2022 or later. # See https://github.com/PyMySQL/PyMySQL/issues/939 # warnings.warn("'db' is deprecated, use 'database'", DeprecationWarning, 3) database = db self._local_infile = bool(local_infile) if self._local_infile: client_flag |= LOCAL_FILES if read_default_group and not read_default_file: if sys.platform.startswith("win"): read_default_file = "c:\\my.ini" else: read_default_file = "/etc/my.cnf" if read_default_file: if not read_default_group: read_default_group = "client" cfg = Parser() cfg.read(os.path.expanduser(read_default_file)) def _config(key, arg): if arg: return arg try: return cfg.get(read_default_group, key) except Exception: return arg user = _config("user", user) password = _config("password", password) host = _config("host", host) database = _config("database", database) unix_socket = _config("socket", unix_socket) port = int(_config("port", port)) charset = _config("default-character-set", charset) if not ssl: ssl = {} if isinstance(ssl, dict): for key in ["ca", "capath", "cert", "key", "cipher"]: value = _config("ssl-" + key, ssl.get(key)) if value: ssl[key] = value self._ssl_context = None if ssl: if not SSL_ENABLED: raise NotImplementedError("SSL module not found") if sock is None: # With a caller-supplied socket the TLS handshake happens # before any MySQL byte is sent, so the server must not be # asked for an in-protocol upgrade — advertising CLIENT_SSL # and then not sending the SSL request packet is a # `1043 Bad handshake`. client_flag |= SSL self._ssl_context = self._create_ssl_ctx(ssl) self._echo = echo self._query_callback = query_callback self._last_usage = self._loop.time() self._host = host or "localhost" self._port = port or 3306 if type(self._port) is not int: raise ValueError("port should be of type int") self._user = user or DEFAULT_USER self._password = password or b"" self._password_creator = password_creator if isinstance(self._password, str): self._password = self._password.encode("latin1") self._db = database self._unix_socket = unix_socket self._sock = sock self._sock_consumed = False self._tls_established = False if not (0 < connect_timeout <= 31536000): raise ValueError("connect_timeout should be >0 and <=31536000") self._connect_timeout = connect_timeout or None if read_timeout is not None and read_timeout <= 0: raise ValueError("read_timeout should be > 0") self._read_timeout = read_timeout self._secure = False self._charset = charset or DEFAULT_CHARSET self._use_unicode = use_unicode self._encoding = charset_by_name(self._charset).encoding client_flag |= CAPABILITIES client_flag |= MULTI_STATEMENTS if self._db: client_flag |= CONNECT_WITH_DB self._client_flag = client_flag self._cursor_cls = cursor_cls self._result = None self._affected_rows = 0 self.host_info = "Not connected" # specified autocommit mode. None means use server default. self.autocommit_mode = autocommit if conv is None: conv = converters.conversions # Need for MySQLdb compatibility. self._encoders = {k: v for (k, v) in conv.items() if type(k) is not int} self._decoders = {k: v for (k, v) in conv.items() if type(k) is int} self._sql_mode = sql_mode self._init_command = init_command self._max_allowed_packet = max_allowed_packet self._auth_plugin_map = auth_plugin_map or {} self._binary_prefix = binary_prefix self._server_public_key = server_public_key self._connect_attrs = { "_client_name": "asyncmy", "_pid": str(os.getpid()), "_client_version": __VERSION__, } if program_name: self._connect_attrs["program_name"] = program_name self._connected = False self._proto: Optional[_MySQLProtocol] = None self._transport = None self._close_reason = None self._deprecate_eof = False # negotiated during the handshake self._mariadb_ext_caps = 0 # MariaDB extended capabilities self._bulk_supported = False # MariaDB COM_STMT_BULK_EXECUTE # Transparent server-side prepared statement cache (binary protocol). # 0 disables it; cursor.execute() then always uses the text protocol. self._stmt_cache_size = int(stmt_cache_size) self._stmt_cache = {} # insertion-ordered; LRU via re-insertion self._unpreparable = set() self._auth_plugin_name = "" def _create_ssl_ctx(self, sslp): if isinstance(sslp, ssl.SSLContext): return sslp elif sslp is True: # `ssl=True` means "use TLS with default settings" (the form DSN # parsers produce for `?ssl=True`). Previously this returned None # while SSL stayed advertised in the client flags, so the # connection silently fell back to plaintext (#90). sslp = {} elif not isinstance(sslp, dict): raise ValueError( "ssl argument must be True, a dict of ssl options, " "or an ssl.SSLContext, got %r" % (type(sslp).__name__,) ) ca = sslp.get("ca") capath = sslp.get("capath") hasnoca = ca is None and capath is None ctx = ssl.create_default_context(cafile=ca, capath=capath) ctx.check_hostname = not hasnoca and sslp.get("check_hostname", True) verify_mode_value = sslp.get("verify_mode") if verify_mode_value is None: ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED elif isinstance(verify_mode_value, bool): ctx.verify_mode = ssl.CERT_REQUIRED if verify_mode_value else ssl.CERT_NONE else: if isinstance(verify_mode_value, str): verify_mode_value = verify_mode_value.lower() if verify_mode_value in ("none", "0", "false", "no"): ctx.verify_mode = ssl.CERT_NONE elif verify_mode_value == "optional": ctx.verify_mode = ssl.CERT_OPTIONAL elif verify_mode_value in ("required", "1", "true", "yes"): ctx.verify_mode = ssl.CERT_REQUIRED else: ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED if "cert" in sslp: ctx.load_cert_chain(sslp["cert"], keyfile=sslp.get("key")) if "cipher" in sslp: ctx.set_ciphers(sslp["cipher"]) ctx.options |= ssl.OP_NO_SSLv2 ctx.options |= ssl.OP_NO_SSLv3 return ctx def close(self): """Close socket connection""" if self._transport is not None: self._transport.close() self._transport = None @property def _stream_broken(self): """True when the underlying stream can no longer be used.""" proto = self._proto return proto is None or proto.eof or proto.exc is not None def _close_on_cancel(self): """Close the connection after a cancelled read left it desynced.""" self.close() self._close_reason = "Cancelled during execution" self._connected = False @property def connected(self): """Return True if the connection is open.""" return self._connected @property def loop(self): return self._loop @property def last_usage(self): """Return time() when connection was used.""" return self._last_usage async def ensure_closed(self): """Send QUIT message and close connection.""" if self._connected and self._transport is not None: send_data = i.pack(1) + B.pack(COM_QUIT) self._write_bytes(send_data) await self._proto.drain() self._transport.close() await self._proto.wait_closed() self.close() self._connected = False async def autocommit(self, value): self.autocommit_mode = bool(value) current = self.get_autocommit() if value != current: await self._send_autocommit_mode() def get_autocommit(self): return bool(self.server_status & SERVER_STATUS_AUTOCOMMIT) async def _read_ok_packet(self): pkt = await self.read_packet() if not pkt.is_ok_packet(): raise errors.OperationalError(CR_COMMANDS_OUT_OF_SYNC, "Command Out of Sync") ok = OKPacketWrapper(pkt) self.server_status = ok.server_status return ok async def _send_autocommit_mode(self): """Set whether or not to commit after every execute().""" await self._execute_command( COM_QUERY, "SET AUTOCOMMIT = %s" % self.escape(self.autocommit_mode) ) await self._read_ok_packet() async def begin(self): """Begin transaction.""" await self._execute_command(COM_QUERY, "BEGIN") await self._read_ok_packet() async def commit(self): """ Commit changes to stable storage. See `Connection.commit() `_ in the specification. """ await self._execute_command(COM_QUERY, "COMMIT") await self._read_ok_packet() async def rollback(self): """ Roll back the current transaction. See `Connection.rollback() `_ in the specification. """ await self._execute_command(COM_QUERY, "ROLLBACK") await self._read_ok_packet() async def show_warnings(self): """Send the "SHOW WARNINGS" SQL """ await self._execute_command(COM_QUERY, "SHOW WARNINGS") result = MySQLResult(self) await result.read() return result.rows async def select_db(self, db): """ Set current db. :param db: The name of the db. """ await self._execute_command(COM_INIT_DB, db) await self._read_ok_packet() def _set_keep_alive(self): raw_sock = self._transport.get_extra_info('socket', default=None) if raw_sock is None: raise RuntimeError("Transport does not expose socket instance") raw_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) def _set_nodelay(self, value): flag = int(bool(value)) raw_sock = self._transport.get_extra_info('socket', default=None) if raw_sock is None: raise RuntimeError("Transport does not expose socket instance") raw_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, flag) def escape(self, obj, mapping=None): """Escape whatever value is passed. Non-standard, for internal use; do not use this in your applications. """ if isinstance(obj, str): return "'" + self.escape_string(obj) + "'" if isinstance(obj, (bytes, bytearray)): return converters.escape_bytes_prefixed(obj) return converters.escape_item(obj, self._charset, mapping=mapping) def literal(self, obj): """Alias for escape(). Non-standard, for internal use; do not use this in your applications. """ return self.escape(obj, self._encoders) def escape_string(self, s): if self.server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES: return s.replace("'", "''") return converters.escape_string(s) def _quote_bytes(self, bytes s): if self.server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES: return "'%s'" % (s.replace(b"'", b"''").decode("ascii", "surrogateescape"),) return converters.escape_bytes(s) def cursor(self, cursor: Optional[Type[Cursor]] = None): """ Create a new cursor to execute queries with. :param cursor: The type of cursor to create. None means use Cursor. :type cursor: :py:class:`Cursor`, :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`. """ self._last_usage = self._loop.time() if cursor: return cursor(self, echo=self._echo, query_callback=self._query_callback) return self._cursor_cls(self, echo=self._echo, query_callback=self._query_callback) # The following methods are INTERNAL USE ONLY (called from Cursor) async def query(self, sql, unbuffered=False): if isinstance(sql, str): sql = sql.encode(self._encoding, "surrogateescape") await self._execute_command(COM_QUERY, sql) await self._read_query_result(unbuffered=unbuffered) return self._affected_rows async def next_result(self, unbuffered=False): await self._read_query_result(unbuffered=unbuffered) return self._affected_rows async def prepare(self, query): """Create a server-side prepared statement (binary protocol). Returns a :class:`PreparedStatement`. Executing it skips client-side escaping entirely and reads results in the binary protocol, which is significantly faster for repeated (point) queries. :param query: SQL with ``?`` placeholders (native MySQL syntax). """ if isinstance(query, str): query = query.encode(self._encoding) await self._execute_command(COM_STMT_PREPARE, query) # COM_STMT_PREPARE response: # https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_stmt_prepare.html pkt = await self.read_packet() pkt.read_uint8() # status: 0x00 statement_id = pkt.read_uint32() num_columns = pkt.read_uint16() num_params = pkt.read_uint16() for _ in range(num_params): await self.read_packet() # parameter definitions (unused) if num_params and not self._deprecate_eof: await self.read_packet() # EOF for _ in range(num_columns): await self.read_packet() # column definitions (re-read on execute) if num_columns and not self._deprecate_eof: await self.read_packet() # EOF return PreparedStatement(self, statement_id, num_params) async def _acquire_cached_statement(self, query, nargs): """Return a cached PreparedStatement for a pyformat query, or None. None means: fall back to the text protocol (unsupported placeholder style, parameter count mismatch, or the server refused to prepare). """ cache = self._stmt_cache stmt = cache.get(query) if stmt is not None: # LRU: move to the most-recently-used end del cache[query] cache[query] = stmt if stmt.parameter_count != nargs: return None return stmt if query in self._unpreparable: return None converted = _pyformat_to_qmark(query) if converted is None or converted[1] != nargs: return None try: stmt = await self.prepare(converted[0]) except errors.Error: # Not preparable (multi-statement, some SHOW variants, ...) or a # genuine SQL error: let the text protocol produce the canonical # behavior, and stop re-trying known-bad statements. if len(self._unpreparable) < 128: self._unpreparable.add(query) return None if stmt.parameter_count != nargs: await stmt.close() return None cache[query] = stmt if len(cache) > self._stmt_cache_size: oldest_query = next(iter(cache)) oldest = cache.pop(oldest_query) try: await oldest.close() except Exception: pass return stmt def affected_rows(self): return self._affected_rows async def kill(self, thread_id): arg = I.pack(thread_id) await self._execute_command(COM_PROCESS_KILL, arg) return await self._read_ok_packet() async def ping(self, reconnect=True): """ Check if the server is alive. :param reconnect: If the connection is closed, reconnect. :type reconnect: boolean :raise Error: If the connection is closed and reconnect=False. """ if not self._connected: if reconnect: await self.connect() reconnect = False else: raise errors.Error("Already closed") try: await self._execute_command(COM_PING, "") await self._read_ok_packet() except Exception: if reconnect: self.close() self._connected = False await self.connect() await self.ping(False) else: raise async def set_charset(self, charset): # Make sure charset is supported. encoding = charset_by_name(charset)._encoding await self._execute_command(COM_QUERY, "SET NAMES %s" % self.escape(charset)) await self.read_packet() self._charset = charset self._encoding = encoding async def _refresh_password(self): """Ask password_creator for a fresh credential before authenticating.""" password = self._password_creator() # Not iscoroutine: a creator wrapping an async client may hand back a # Task or any awaitable, and those must be awaited too. if inspect.isawaitable(password): password = await password if isinstance(password, str): password = password.encode("latin1") elif isinstance(password, (bytes, bytearray)): password = bytes(password) else: raise ValueError( "password_creator must return str or bytes, got %s" % type(password).__name__ ) self._password = password async def connect(self): if self._connected: return self._proto, self._transport try: self._close_reason = None # statement ids do not survive reconnects self._stmt_cache.clear() self._deprecate_eof = False self._mariadb_ext_caps = 0 self._bulk_supported = False loop = self._loop self._tls_established = False if self._password_creator is not None: await self._refresh_password() if self._sock is not None: if self._sock_consumed: raise errors.OperationalError( CR_SERVER_LOST, "Cannot reconnect a connection built from a user-supplied " "socket; pass a fresh socket to connect(sock=...)", ) self._sock_consumed = True proto = _MySQLProtocol(loop) # With an ssl context the TLS handshake runs now, before any # MySQL bytes, instead of the in-protocol upgrade below: the # peer here is the caller's endpoint, not necessarily a server # that speaks MySQL's STARTTLS dance. self._transport, _ = await asyncio.wait_for( loop.create_connection( lambda: proto, sock=self._sock, ssl=self._ssl_context, server_hostname=self._host if self._ssl_context else None, ), timeout=self._connect_timeout, ) self._proto = proto self.host_info = "socket %s" % (self._sock,) if self._ssl_context is not None: self._secure = True self._tls_established = True elif self._unix_socket: proto = _MySQLProtocol(loop) self._transport, _ = await asyncio.wait_for( loop.create_unix_connection(lambda: proto, self._unix_socket), timeout=self._connect_timeout, ) self._proto = proto self.host_info = "Localhost via UNIX socket" self._secure = True else: while True: try: proto = _MySQLProtocol(loop) self._transport, _ = await asyncio.wait_for(loop.create_connection( lambda: proto, self._host, self._port, ), timeout=self._connect_timeout) self._proto = proto self._set_keep_alive() break except (OSError, IOError) as e: if e.errno == errno.EINTR: continue raise self.host_info = "socket %s:%d" % (self._host, self._port) # A caller-supplied socket is left exactly as it was configured. if not self._unix_socket and self._sock is None: self._set_nodelay(True) self._next_seq_id = 0 await self._get_server_information() await self._request_authentication() self._connected = True if self._sql_mode is not None: await self.query("SET sql_mode=%s" % (self._sql_mode,)) if self._init_command is not None: await self.query(self._init_command) await self.commit() if self.autocommit_mode is not None: await self.autocommit(self.autocommit_mode) except BaseException as e: self.close() if isinstance(e, (OSError, IOError)): raise errors.OperationalError( CR_CONN_HOST_ERROR, "Can't connect to MySQL server on %r (%s)" % (self._host, e) ) from e # If e is neither DatabaseError or IOError, It's a bug. # But raising AssertionError hides original error. # So just reraise it. raise e def write_packet(self, bytes payload): """ Writes an entire "mysql packet" in its entirety to the network adding its length and sequence number. """ # Internal note: when you build packet manually and calls _write_bytes() # directly, you should set self._next_seq_id properly. cdef Py_ssize_t length = len(payload) header = bytes( (length & 0xFF, (length >> 8) & 0xFF, (length >> 16) & 0xFF, self._next_seq_id) ) if length < 8192: self._write_bytes(header + payload) else: # avoid copying large payloads just to prepend 4 header bytes self._write_bytes(header) self._write_bytes(payload) self._next_seq_id = (self._next_seq_id + 1) & 0xFF async def _fill_buffer(self, need): """Suspend until at least `need` unconsumed bytes are buffered.""" proto = self._proto read_timeout = self._read_timeout while proto.length - proto.pos < need: if proto.exc is not None: raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query (%s)" % (proto.exc,), ) if proto.eof: raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query" ) try: if read_timeout: try: await asyncio.wait_for(proto.wait_for_data(need), read_timeout) except asyncio.TimeoutError: await self.ensure_closed() raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query (read timeout)", ) else: await proto.wait_for_data(need) except asyncio.CancelledError: # Cancelled mid-read: the protocol stream is now desynced, so # the connection must not be reused (e.g. returned to a pool). self._close_on_cancel() raise except (IOError, OSError, asyncio.TimeoutError) as e: raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query (%s)" % (e,), ) async def read_packet(self, packet_type=MysqlPacket): """ Read an entire "mysql packet" in its entirety from the network and return a MysqlPacket type that represents the results. :raise OperationalError: If the connection to the MySQL server is lost. :raise InternalError: If the packet sequence number is wrong. """ proto = self._proto buff = None while True: if proto.length - proto.pos < 4: await self._fill_buffer(4) buffer = proto.buffer pos = proto.pos bytes_to_read = buffer[pos] | (buffer[pos + 1] << 8) | (buffer[pos + 2] << 16) packet_number = buffer[pos + 3] pos += 4 proto.pos = pos if packet_number != self._next_seq_id: if packet_number == 0: # MariaDB sends error packet with seqno==0 when shutdown raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query", ) raise errors.InternalError( "Packet sequence number wrong - got %d expected %d" % (packet_number, self._next_seq_id) ) self._next_seq_id = (self._next_seq_id + 1) & 0xFF if proto.length - pos < bytes_to_read: await self._fill_buffer(bytes_to_read) # the buffer may have been compacted or reallocated while waiting buffer = proto.buffer pos = proto.pos recv_data = _take_bytes(buffer, pos, bytes_to_read) proto.pos = pos + bytes_to_read # Fast path: single packet (most common case ~99%) if bytes_to_read < MAX_PACKET_LEN and buff is None: break # Slow path: payload split across 16MB wire packets # https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html if buff is None: buff = [] buff.append(recv_data) if bytes_to_read < MAX_PACKET_LEN: recv_data = b"".join(buff) break packet = packet_type(recv_data, encoding=self._encoding) if packet.is_error_packet(): if self._result is not None and self._result.unbuffered_active is True: self._result.unbuffered_active = False packet.raise_for_error() return packet async def _read_bytes(self, num_bytes: int): # Kept for backwards compatibility; packet reads go through the # protocol's receive buffer (see _fill_buffer/read_packet). proto = self._proto if proto.length - proto.pos < num_bytes: await self._fill_buffer(num_bytes) pos = proto.pos data = _take_bytes(proto.buffer, pos, num_bytes) proto.pos = pos + num_bytes return data def _write_bytes(self, bytes data): transport = self._transport if transport is None or transport.is_closing(): self.close() self._connected = False raise errors.OperationalError(CR_SERVER_GONE_ERROR, "MySQL server has gone away") try: transport.write(data) except (OSError, RuntimeError) as exc: # uvloop raises RuntimeError for a closed handle. Preserve unrelated # RuntimeErrors instead of masking application/programming errors. if isinstance(exc, RuntimeError) and not transport.is_closing(): raise self.close() self._connected = False raise errors.OperationalError( CR_SERVER_GONE_ERROR, "MySQL server has gone away (%s)" % (exc,) ) from exc async def _read_query_result(self, unbuffered=False): self._result = None if unbuffered: try: result = MySQLResult(self) await result.init_unbuffered_query() except Exception: result.unbuffered_active = False result.connection = None raise else: result = MySQLResult(self) await result.read() self._result = result self._affected_rows = result.affected_rows if result.server_status != 0: self.server_status = result.server_status def insert_id(self): if self._result: return self._result.insert_id else: return 0 async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): if exc_type: self.close() else: await self.ensure_closed() async def _execute_command(self, command, sql): """ :raise InterfaceError: If the connection is closed. :raise ValueError: If no username was specified. """ if not self._connected: raise errors.InterfaceError(0, self._close_reason or "Not connected") # If the last query was unbuffered, make sure it finishes before # sending new commands if self._result is not None: if self._result.unbuffered_active: warnings.warn("Previous unbuffered result was left incomplete") self._result._finish_unbuffered_query() while self._result.has_next: await self.next_result() self._result = None if isinstance(sql, str): sql = sql.encode(self._encoding) packet_size = min(MAX_PACKET_LEN, len(sql) + 1) # +1 is for command # tiny optimization: build first packet manually instead of # calling self..write_packet() prelude = iB.pack(packet_size, command) chunk = sql[: packet_size - 1] if packet_size < 8192: self._write_bytes(prelude + chunk) else: # avoid copying a large SQL payload just to prepend the header self._write_bytes(prelude) self._write_bytes(chunk) self._next_seq_id = 1 if packet_size < MAX_PACKET_LEN: return sql = sql[packet_size - 1:] while True: packet_size = min(MAX_PACKET_LEN, len(sql)) self.write_packet(sql[:packet_size]) sql = sql[packet_size:] if not sql and packet_size < MAX_PACKET_LEN: break async def _request_authentication(self): # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse if int(self.server_version.split(".", 1)[0]) >= 5: self._client_flag |= MULTI_RESULTS if self.server_capabilities & DEPRECATE_EOF: # Result sets then omit the EOF between metadata and rows and end # with an OK packet (0xFE header) instead of an EOF packet. self._client_flag |= DEPRECATE_EOF self._deprecate_eof = True if self._user is None: raise ValueError("Did not specify a username") charset_id = charset_by_name(self._charset).id # _tls_established means the caller handed us a socket that is already # encrypted, so there is no in-protocol upgrade left to do. if self._ssl_context and not self._tls_established: # capablities, max packet, charset data = IIB.pack(self._client_flag, MAX_PACKET_LEN, charset_id) data += b'\x00' * (32 - len(data)) self.write_packet(data) # Stop sending events to the protocol self._transport.pause_reading() # Get the raw socket from the transport raw_sock = self._transport.get_extra_info('socket', default=None) if raw_sock is None: raise RuntimeError("Transport does not expose socket instance") raw_sock = raw_sock.dup() self._transport.close() # MySQL expects TLS negotiation to happen in the middle of a # TCP connection not at start. Passing in a socket to # create_connection will cause it to negotiate TLS on an existing # connection not initiate a new one. proto = _MySQLProtocol(self._loop) self._transport, _ = await self._loop.create_connection( lambda: proto, sock=raw_sock, ssl=self._ssl_context, server_hostname=self._host, ) self._proto = proto # The channel is encrypted from here on. caching_sha2_password # full auth sends the password in the clear over a secure channel; # without this it took the RSA branch instead and the server, which # expects cleartext on a TLS connection, replied 1045 (#117). self._secure = True if isinstance(self._user, str): self._user = self._user.encode(self._encoding) if self._mariadb_ext_caps & MARIADB_CLIENT_STMT_BULK_OPERATIONS: # MariaDB: clear CLIENT_MYSQL and advertise extended capabilities # in the last 4 bytes of the 23-byte filler. self._client_flag &= ~CLIENT_MYSQL self._bulk_supported = True data_init = ( i.pack(self._client_flag) + I.pack(MAX_PACKET_LEN) + B_.pack(charset_id) + b"\x00" * 19 + I.pack(MARIADB_CLIENT_STMT_BULK_OPERATIONS) ) else: data_init = iIB23s.pack(self._client_flag, MAX_PACKET_LEN, charset_id, b"") data = data_init + self._user + b"\0" authresp = b"" plugin_name = None if self._auth_plugin_name == "": plugin_name = b"" authresp = auth.scramble_native_password(self._password, self.salt) elif self._auth_plugin_name == "mysql_native_password": plugin_name = b"mysql_native_password" authresp = auth.scramble_native_password(self._password, self.salt) elif self._auth_plugin_name == "caching_sha2_password": plugin_name = b"caching_sha2_password" if self._password: authresp = auth.scramble_caching_sha2(self._password, self.salt) elif self._auth_plugin_name == "sha256_password": plugin_name = b"sha256_password" if self._ssl_context and self.server_capabilities & SSL: authresp = self._password + b"\0" elif self._password: authresp = b"\1" # request public key else: authresp = b"\0" # empty password if self.server_capabilities & PLUGIN_AUTH_LENENC_CLIENT_DATA: data += _lenenc_int(len(authresp)) + authresp elif self.server_capabilities & SECURE_CONNECTION: data += B_.pack(len(authresp)) + authresp else: # pragma: no cover - not testing against servers without secure auth (>=5.0) data += authresp + b"\0" if self._db and self.server_capabilities & CONNECT_WITH_DB: if isinstance(self._db, str): self._db = self._db.encode(self._encoding) data += self._db + b"\0" if self.server_capabilities & PLUGIN_AUTH: data += (plugin_name or b"") + b"\0" if self.server_capabilities & CONNECT_ATTRS: connect_attrs = b"" for k, v in self._connect_attrs.items(): k = k.encode("utf-8") connect_attrs += B_.pack(len(k)) + k v = v.encode("utf-8") connect_attrs += B_.pack(len(v)) + v data += B_.pack(len(connect_attrs)) + connect_attrs self.write_packet(data) auth_packet = await self.read_packet() # if authentication method isn't accepted the first byte # will have the octet 254 if auth_packet.is_auth_switch_request(): # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest auth_packet.read_uint8() # 0xfe packet identifier plugin_name = auth_packet.read_string() if self.server_capabilities & PLUGIN_AUTH and plugin_name is not None: auth_packet = await self._process_auth(plugin_name, auth_packet) else: # send legacy handshake data = auth.scramble_old_password(self._password, self.salt) + b"\0" self.write_packet(data) auth_packet = await self.read_packet() elif auth_packet.is_extra_auth_data(): # https://dev.mysql.com/doc/internals/en/successful-authentication.html if self._auth_plugin_name == "caching_sha2_password": auth_packet = await auth.caching_sha2_password_auth(self, auth_packet) elif self._auth_plugin_name == "sha256_password": auth_packet = await auth.sha256_password_auth(self, auth_packet) else: raise errors.OperationalError( "Received extra packet for auth method %r", self._auth_plugin_name ) return auth_packet async def _process_auth(self, plugin_name, auth_packet): handler = self._get_auth_plugin_handler(plugin_name) if handler: try: return handler.authenticate(auth_packet) except AttributeError: if plugin_name != b"dialog": raise errors.OperationalError( 2059, "Authentication plugin '%s'" " not loaded: - %r missing authenticate method" % (plugin_name, type(handler)), ) if plugin_name == b"caching_sha2_password": return await auth.caching_sha2_password_auth(self, auth_packet) elif plugin_name == b"sha256_password": return await auth.sha256_password_auth(self, auth_packet) elif plugin_name == b"mysql_native_password": data = auth.scramble_native_password(self._password, auth_packet.read_all()) elif plugin_name == b"client_ed25519": data = auth.ed25519_password(self._password, auth_packet.read_all()) elif plugin_name == b"mysql_old_password": data = auth.scramble_old_password(self._password, auth_packet.read_all()) + b"\0" elif plugin_name == b"mysql_clear_password": # https://dev.mysql.com/doc/internals/en/clear-text-authentication.html data = self._password + b"\0" elif plugin_name == b"dialog": pkt = auth_packet while True: flag = pkt.read_uint8() echo = (flag & 0x06) == 0x02 last = (flag & 0x01) == 0x01 prompt = pkt.read_all() if prompt == b"Password: ": self.write_packet(self._password + b"\0") elif handler: resp = "no response - TypeError within plugin.prompt method" try: resp = handler.prompt(echo, prompt) self.write_packet(resp + b"\0") except AttributeError: raise errors.OperationalError( 2059, "Authentication plugin '%s'" " not loaded: - %r missing prompt method" % (plugin_name, handler), ) except TypeError: raise errors.OperationalError( 2061, "Authentication plugin '%s'" " %r didn't respond with string. Returned '%r' to prompt %r" % (plugin_name, handler, resp, prompt), ) else: raise errors.OperationalError( 2059, "Authentication plugin '%s' (%r) not configured" % (plugin_name, handler), ) pkt = await self.read_packet() pkt.check_error() if pkt.is_ok_packet() or last: break return pkt else: raise errors.OperationalError( 2059, "Authentication plugin '%s' not configured" % plugin_name ) self.write_packet(data) pkt = await self.read_packet() pkt.check_error() return pkt def _get_auth_plugin_handler(self, plugin_name): plugin_class = self._auth_plugin_map.get(plugin_name) if not plugin_class and isinstance(plugin_name, bytes): plugin_class = self._auth_plugin_map.get(plugin_name.decode("ascii")) if plugin_class: try: handler = plugin_class(self) except TypeError: raise errors.OperationalError( 2059, "Authentication plugin '%s'" " not loaded: - %r cannot be constructed with connection object" % (plugin_name, plugin_class), ) else: handler = None return handler # _mysql support def thread_id(self): return self.server_thread_id[0] def character_set_name(self): return self._charset def get_host_info(self): return self.host_info def get_proto_info(self): return self.protocol_version def get_transaction_status(self): return bool(self.server_status & SERVER_STATUS_IN_TRANS) async def _get_server_information(self): i = 0 packet = await self.read_packet() data = packet.get_all_data() self.protocol_version = data[i] i += 1 server_end = data.find(b"\0", i) self.server_version = data[i:server_end].decode("latin1") i = server_end + 1 self.server_thread_id = I.unpack(data[i: i + 4]) i += 4 self.salt = data[i: i + 8] i += 9 # 8 + 1(filler) self.server_capabilities = H.unpack(data[i: i + 2])[0] i += 2 if len(data) >= i + 6: lang, stat, cap_h, salt_len = BHHB.unpack(data[i: i + 6]) i += 6 # TODO: deprecate server_language and server_charset. # mysqlclient-python doesn't provide it. self.server_language = lang try: self.server_charset = charset_by_id(lang).name except KeyError: # unknown collation self.server_charset = None self.server_status = stat self.server_capabilities |= cap_h << 16 salt_len = max(12, salt_len - 9) # reserved: 6 filler bytes, then 4 bytes of MariaDB extended # capabilities (zero on MySQL servers) if "MariaDB" in self.server_version and len(data) >= i + 10: self._mariadb_ext_caps = I.unpack(data[i + 6: i + 10])[0] i += 10 if len(data) >= i + salt_len: # salt_len includes auth_plugin_data_part_1 and filler self.salt += data[i: i + salt_len] i += salt_len i += 1 # AUTH PLUGIN NAME may appear here. if self.server_capabilities & PLUGIN_AUTH and len(data) >= i: # Due to Bug#59453 the auth-plugin-name is missing the terminating # NUL-char in versions prior to 5.5.10 and 5.6.2. # ref: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake # didn't use version checks as mariadb is corrected and reports # earlier than those two. server_end = data.find(b"\0", i) if server_end < 0: # pragma: no cover - very specific upstream bug # not found \0 and last field so take it all self._auth_plugin_name = data[i:].decode("utf-8") else: self._auth_plugin_name = data[i:server_end].decode("utf-8") def get_server_info(self): return self.server_version Warning = errors.Warning Error = errors.Error InterfaceError = errors.InterfaceError DatabaseError = errors.DatabaseError DataError = errors.DataError OperationalError = errors.OperationalError IntegrityError = errors.IntegrityError InternalError = errors.InternalError ProgrammingError = errors.ProgrammingError NotSupportedError = errors.NotSupportedError class PreparedStatement: """A server-side prepared statement (binary protocol). Created via :meth:`Connection.prepare`. Parameters are sent in binary form (no client-side escaping) and result rows are parsed from the binary protocol (no text parsing for numeric/temporal columns). Notes: - placeholders use native MySQL ``?`` syntax, not ``%s``; - custom ``conv`` decoders apply only to string-typed columns; numeric and temporal columns decode natively; - only the first result set is returned (extra sets are drained). """ def __init__(self, connection, statement_id, parameter_count): self._connection = connection self._statement_id = statement_id self._parameter_count = parameter_count self._closed = False # Column metadata cached from the first execute; later executes skip # re-parsing the (identical) column definition packets. self._meta = None @property def parameter_count(self): return self._parameter_count async def execute(self, args=()): """Execute with ``args`` bound to the statement's ``?`` placeholders. Returns the result object: ``result.rows`` (tuple of row tuples, None for non-SELECT), ``result.affected_rows``, ``result.insert_id``, ``result.description``. """ conn = self._connection if self._closed or conn is None: raise errors.ProgrammingError("Prepared statement is closed") if not isinstance(args, (tuple, list)): args = (args,) if len(args) != self._parameter_count: raise errors.ProgrammingError( "Expected %d parameters, got %d" % (self._parameter_count, len(args)) ) payload = I.pack(self._statement_id) + b"\x00" + I.pack(1) if self._parameter_count: payload += pack_binary_params(tuple(args), conn._encoding) await conn._execute_command(COM_STMT_EXECUTE, payload) result = MySQLResult(conn) await result.read_binary(self) has_next = result.has_next while has_next: # drain extra result sets (e.g. from stored procedures) extra = MySQLResult(conn) await extra.read_binary() has_next = extra.has_next result.has_next = False conn._result = result conn._affected_rows = result.affected_rows if result.server_status != 0: conn.server_status = result.server_status return result async def execute_bulk(self, rows): """MariaDB COM_STMT_BULK_EXECUTE: bind many rows in one round-trip. ``rows`` is a sequence of parameter tuples. Returns the result object (``affected_rows``, ``insert_id``). Only usable when the server advertises MARIADB_CLIENT_STMT_BULK_OPERATIONS. """ conn = self._connection if self._closed or conn is None: raise errors.ProgrammingError("Prepared statement is closed") if not conn._bulk_supported: raise errors.NotSupportedError("Server does not support COM_STMT_BULK_EXECUTE") packed = pack_bulk_rows(rows, self._parameter_count, conn._encoding) if packed is None: # Raised before any I/O, so callers may safely fall back. raise ValueError( "Rows are not bulk-compatible (mixed types or all-NULL column)" ) # stmt_id(4) + flags(2): 128 = SEND_TYPES_TO_SERVER payload = I.pack(self._statement_id) + b"\x80\x00" + packed[0] + packed[1] await conn._execute_command(COM_STMT_BULK_EXECUTE, payload) result = MySQLResult(conn) await result.read() # single OK (or error) packet conn._result = result conn._affected_rows = result.affected_rows if result.server_status != 0: conn.server_status = result.server_status return result async def close(self): """Deallocate the statement on the server (no server response).""" if self._closed: return self._closed = True conn = self._connection self._connection = None if conn is not None and conn.connected: await conn._execute_command(COM_STMT_CLOSE, I.pack(self._statement_id)) async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() cdef class MySQLResult: cdef: public connection public bytes message public int warning_count, field_count, server_status, unbuffered_active, has_next public object affected_rows public list fields, converters public unsigned long long insert_id public tuple rows, description tuple _row_converters tuple _binary_colspecs list _pending_rows Py_ssize_t _pending_idx bint _deprecate_eof def __init__(self, connection: Connection): self.connection = connection self._deprecate_eof = connection._deprecate_eof self.affected_rows = 0 self.insert_id = 0 self.server_status = 0 self.warning_count = 0 self.message = None self.field_count = 0 self.description = None self.rows = None self.has_next = False self.unbuffered_active = False def __del__(self): if self.unbuffered_active: self._finish_unbuffered_query() async def read(self): try: first_packet = await self.connection.read_packet() if first_packet.is_ok_packet(): self._read_ok_packet(first_packet) elif first_packet.is_load_local_packet(): await self._read_load_local_packet(first_packet) else: await self._read_result_packet(first_packet) finally: self.connection = None async def read_binary(self, stmt=None): """Read a COM_STMT_EXECUTE response (binary protocol result set).""" try: first_packet = await self.connection.read_packet() if first_packet.is_ok_packet(): self._read_ok_packet(first_packet) else: self.field_count = first_packet.read_length_encoded_integer() meta = stmt._meta if stmt is not None else None if meta is not None and meta[0] == self.field_count: await self._consume_descriptions(meta) else: await self._get_descriptions() if stmt is not None: stmt._meta = ( self.field_count, self.fields, self.converters, self._row_converters, self._binary_colspecs, self.description, ) await self._read_binary_rowdata_packet() finally: self.connection = None async def _consume_descriptions(self, tuple meta): """Skip the column definition packets, reusing cached metadata.""" # column definitions, plus a trailing EOF unless DEPRECATE_EOF is on cdef Py_ssize_t remaining = self.field_count + (0 if self._deprecate_eof else 1) conn = self.connection proto = conn._proto while remaining: new_pos, new_seq, skipped = skip_packets_from_buffer( proto.buffer, proto.pos, proto.length, conn._next_seq_id, remaining ) proto.pos = new_pos conn._next_seq_id = new_seq remaining -= skipped if remaining: # incomplete or error packet: take the generic path for one await conn.read_packet() remaining -= 1 self.fields = meta[1] self.converters = meta[2] self._row_converters = meta[3] self._binary_colspecs = meta[4] self.description = meta[5] async def _read_binary_rowdata_packet(self): cdef list rows = [] cdef tuple specs = self._binary_colspecs conn = self.connection proto = conn._proto while True: new_pos, new_seq = parse_binary_rows_from_buffer( proto.buffer, proto.pos, proto.length, specs, conn._next_seq_id, rows, self._deprecate_eof, ) proto.pos = new_pos conn._next_seq_id = new_seq packet = await conn.read_packet() if self._check_packet_is_eof(packet): self.connection = None break rows.append(packet.read_binary_row(specs)) self.affected_rows = len(rows) self.rows = tuple(rows) async def init_unbuffered_query(self): """ :raise OperationalError: If the connection to the MySQL server is lost. :raise InternalError: """ self.unbuffered_active = True first_packet = await self.connection.read_packet() if first_packet.is_ok_packet(): self._read_ok_packet(first_packet) self.unbuffered_active = False self.connection = None elif first_packet.is_load_local_packet(): await self._read_load_local_packet(first_packet) self.unbuffered_active = False self.connection = None else: self.field_count = first_packet.read_length_encoded_integer() await self._get_descriptions() # Apparently, MySQLdb picks this number because it's the maximum # value of a 64bit unsigned integer. Since we're emulating MySQLdb, # we set it to this instead of None, which would be preferred. self.affected_rows = 18446744073709551615 def _read_ok_packet(self, first_packet): ok_packet = OKPacketWrapper(first_packet) self.affected_rows = ok_packet.affected_rows self.insert_id = ok_packet.insert_id self.server_status = ok_packet.server_status self.warning_count = ok_packet.warning_count self.message = ok_packet.message self.has_next = ok_packet.has_next async def _read_load_local_packet(self, first_packet): if not self.connection._local_infile: raise RuntimeError( "**WARN**: Received LOAD_LOCAL packet but local_infile option is false." ) load_packet = LoadLocalPacketWrapper(first_packet) sender = LoadLocalFile(load_packet.filename, self.connection) try: await sender.send_data() except Exception: await self.connection.read_packet() # skip ok packet raise ok_packet = await self.connection.read_packet() if not ok_packet.is_ok_packet(): # pragma: no cover - upstream induced protocol error raise errors.OperationalError(CR_COMMANDS_OUT_OF_SYNC, "Commands Out of Sync") self._read_ok_packet(ok_packet) def _check_packet_is_eof(self, packet): if self._deprecate_eof: # Result sets end with an OK packet carrying a 0xFE header. # A row packet can never start with 0xFE at payload < 16MB (that # first byte would be an 8-byte length-encoded integer marker, # implying a >=16MB packet). if not packet.is_auth_switch_request(): # first byte != 0xFE return False packet.advance(1) packet.read_length_encoded_integer() # affected rows packet.read_length_encoded_integer() # insert id server_status = packet.read_uint16() self.warning_count = packet.read_uint16() self.server_status = server_status self.has_next = server_status & SERVER_MORE_RESULTS_EXISTS return True if not packet.is_eof_packet(): return False wp = EOFPacketWrapper(packet) self.warning_count = wp.warning_count self.has_next = wp.has_next return True async def _read_result_packet(self, first_packet): self.field_count = first_packet.read_length_encoded_integer() await self._get_descriptions() await self._read_rowdata_packet() async def _read_rowdata_packet_unbuffered(self): # Check if in an active query if not self.unbuffered_active: return cdef list pending = self._pending_rows # Serve rows already bulk-parsed from the receive buffer if pending is not None and self._pending_idx < len(pending): row = pending[self._pending_idx] self._pending_idx += 1 self.affected_rows = 1 self.rows = (row,) return row # Bulk-parse whatever complete row packets are sitting in the buffer conn = self.connection proto = conn._proto cdef list rows = [] new_pos, new_seq = parse_rows_from_buffer( proto.buffer, proto.pos, proto.length, self._row_converters, conn._next_seq_id, rows, self._deprecate_eof, ) proto.pos = new_pos conn._next_seq_id = new_seq if rows: self._pending_rows = rows self._pending_idx = 1 row = rows[0] self.affected_rows = 1 self.rows = (row,) return row self._pending_rows = None # Slow path: await the next packet (row, EOF, or error) packet = await conn.read_packet() if self._check_packet_is_eof(packet): self.unbuffered_active = False self.connection = None self.rows = None return row = self._read_row_from_packet(packet) self.affected_rows = 1 self.rows = (row,) # rows should tuple of row for MySQL-python compatibility. return row async def _finish_unbuffered_query(self): # After much reading on the MySQL protocol, it appears that there is, # in fact, no way to stop MySQL from sending all the data after # executing a query, so we just spin, and wait for an EOF packet. while self.unbuffered_active: packet = await self.connection.read_packet() if self._check_packet_is_eof(packet): self.unbuffered_active = False self.connection = None # release reference to kill cyclic reference. async def _read_rowdata_packet(self): """Read a rowdata packet for each data row in the result set.""" cdef list rows = [] cdef tuple convs = self._row_converters conn = self.connection proto = conn._proto while True: # Bulk-parse every complete row packet already sitting in the # receive buffer without touching the event loop. new_pos, new_seq = parse_rows_from_buffer( proto.buffer, proto.pos, proto.length, convs, conn._next_seq_id, rows, self._deprecate_eof, ) proto.pos = new_pos conn._next_seq_id = new_seq # Whatever stopped the bulk parser (incomplete data, EOF, error, # jumbo packet) goes through the generic packet reader. packet = await conn.read_packet() if self._check_packet_is_eof(packet): self.connection = None # release reference to kill cyclic reference. break rows.append(packet.read_row(convs)) self.affected_rows = len(rows) self.rows = tuple(rows) cdef _read_row_from_packet(self, packet: MysqlPacket): return packet.read_row(self._row_converters) async def _get_descriptions(self): """Read a column descriptor packet for each column in the result.""" self.fields = [] self.converters = [] cdef list binary_specs = [] use_unicode = self.connection._use_unicode conn_encoding = self.connection._encoding description = [] for i in range(self.field_count): field = await self.connection.read_packet(FieldDescriptorPacket) self.fields.append(field) description.append(field.description()) field_type = field.type_code if use_unicode: if field_type == JSON: # When SELECT from JSON column: charset = binary # When SELECT CAST(... AS JSON): charset = connection encoding # This behavior is different from TEXT / BLOB. # We should decode result by connection encoding regardless charsetnr. # See https://github.com/PyMySQL/PyMySQL/issues/488 encoding = conn_encoding # SELECT CAST(... AS JSON) elif field_type in TEXT_TYPES: if field.charsetnr == 63: # binary # TEXTs with charset=binary means BINARY types. encoding = None else: encoding = conn_encoding else: # Integers, Dates and Times, and other basic data is encoded in ascii encoding = "ascii" else: encoding = None converter = self.connection._decoders.get(field_type) if converter is converters.through: converter = None if encoding == "ascii" and converter in _BYTES_SAFE_DECODERS: # int()/float()/date-time parsers consume bytes directly; # skip the intermediate str allocation entirely. encoding = None if encoding is None: code = 0 elif encoding == "utf8": code = 1 elif encoding == "ascii": code = 2 else: code = 3 self.converters.append((code, encoding, converter)) binary_specs.append( (field_type, bool(field.flags & 32), code, encoding, converter) # 32: UNSIGNED ) self._row_converters = tuple(self.converters) self._binary_colspecs = tuple(binary_specs) if not self._deprecate_eof: eof_packet = await self.connection.read_packet() assert eof_packet.is_eof_packet(), "Protocol error, expecting EOF" self.description = tuple(description) class LoadLocalFile: def __init__(self, filename: str, connection: Connection): self.filename = filename self.connection = connection self._loop = connection.loop async def send_data(self): """ Send data packets from the local file to the server """ if not self.connection.connected: raise errors.InterfaceError(0, "") conn = self.connection try: with open(self.filename, "rb") as open_file: packet_size = min(conn._max_allowed_packet, 16 * 1024) # 16KB is efficient enough while True: chunk = open_file.read(packet_size) if not chunk: break conn.write_packet(chunk) await conn._proto.drain() except IOError: raise errors.OperationalError(FILE_NOT_FOUND, f"Can't find file '{self.filename}'") finally: # send the empty packet to signify we are done sending data conn.write_packet(b"") await conn._proto.drain() def connect(user=None, password="", password_creator=None, host=None, database=None, unix_socket=None, sock=None, port=0, charset="", sql_mode=None, read_default_file=None, conv=None, use_unicode=True, client_flag=0, cursor_cls=Cursor, init_command=None, connect_timeout=10, read_default_group=None, autocommit=False, local_infile=False, max_allowed_packet=16 * 1024 * 1024, auth_plugin_map=None, read_timeout=None, binary_prefix=False, program_name=None, echo=False, query_callback=None, server_public_key=None, ssl=None, stmt_cache_size=0, db=None, # deprecated ): coro = _connect( user=user, password=password, password_creator=password_creator, host=host, database=database, unix_socket=unix_socket, sock=sock, port=port, charset=charset, sql_mode=sql_mode, read_default_file=read_default_file, conv=conv, use_unicode=use_unicode, client_flag=client_flag, cursor_cls=cursor_cls, init_command=init_command, connect_timeout=connect_timeout, read_default_group=read_default_group, autocommit=autocommit, local_infile=local_infile, max_allowed_packet=max_allowed_packet, auth_plugin_map=auth_plugin_map, read_timeout=read_timeout, binary_prefix=binary_prefix, program_name=program_name, server_public_key=server_public_key, echo=echo, query_callback=query_callback, ssl=ssl, stmt_cache_size=stmt_cache_size, db=db, # deprecated ) return _ConnectionContextManager(coro) async def _connect( **kwargs, ) -> Connection: conn = Connection( **kwargs, ) await conn.connect() return conn python-asyncmy-0.2.15/asyncmy/constants/000077500000000000000000000000001525423006000202755ustar00rootroot00000000000000python-asyncmy-0.2.15/asyncmy/constants/CLIENT.py000066400000000000000000000015561525423006000216340ustar00rootroot00000000000000# https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags LONG_PASSWORD = 1 FOUND_ROWS = 1 << 1 LONG_FLAG = 1 << 2 CONNECT_WITH_DB = 1 << 3 NO_SCHEMA = 1 << 4 COMPRESS = 1 << 5 ODBC = 1 << 6 LOCAL_FILES = 1 << 7 IGNORE_SPACE = 1 << 8 PROTOCOL_41 = 1 << 9 INTERACTIVE = 1 << 10 SSL = 1 << 11 IGNORE_SIGPIPE = 1 << 12 TRANSACTIONS = 1 << 13 SECURE_CONNECTION = 1 << 15 MULTI_STATEMENTS = 1 << 16 MULTI_RESULTS = 1 << 17 PS_MULTI_RESULTS = 1 << 18 PLUGIN_AUTH = 1 << 19 CONNECT_ATTRS = 1 << 20 PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21 CAPABILITIES = ( LONG_PASSWORD | LONG_FLAG | PROTOCOL_41 | TRANSACTIONS | SECURE_CONNECTION | MULTI_RESULTS | PLUGIN_AUTH | PLUGIN_AUTH_LENENC_CLIENT_DATA | CONNECT_ATTRS ) # Not done yet HANDLE_EXPIRED_PASSWORDS = 1 << 22 SESSION_TRACK = 1 << 23 DEPRECATE_EOF = 1 << 24 python-asyncmy-0.2.15/asyncmy/constants/COLUMN.py000066400000000000000000000002011525423006000216350ustar00rootroot00000000000000NULL_COLUMN = 251 UNSIGNED_CHAR_COLUMN = 251 UNSIGNED_SHORT_COLUMN = 252 UNSIGNED_INT24_COLUMN = 253 UNSIGNED_INT64_COLUMN = 254 python-asyncmy-0.2.15/asyncmy/constants/COMMAND.py000066400000000000000000000012471525423006000217310ustar00rootroot00000000000000COM_SLEEP = 0x00 COM_QUIT = 0x01 COM_INIT_DB = 0x02 COM_QUERY = 0x03 COM_FIELD_LIST = 0x04 COM_CREATE_DB = 0x05 COM_DROP_DB = 0x06 COM_REFRESH = 0x07 COM_SHUTDOWN = 0x08 COM_STATISTICS = 0x09 COM_PROCESS_INFO = 0x0A COM_CONNECT = 0x0B COM_PROCESS_KILL = 0x0C COM_DEBUG = 0x0D COM_PING = 0x0E COM_TIME = 0x0F COM_DELAYED_INSERT = 0x10 COM_CHANGE_USER = 0x11 COM_BINLOG_DUMP = 0x12 COM_TABLE_DUMP = 0x13 COM_CONNECT_OUT = 0x14 COM_REGISTER_SLAVE = 0x15 COM_STMT_PREPARE = 0x16 COM_STMT_EXECUTE = 0x17 COM_STMT_SEND_LONG_DATA = 0x18 COM_STMT_CLOSE = 0x19 COM_STMT_RESET = 0x1A COM_SET_OPTION = 0x1B COM_STMT_FETCH = 0x1C COM_DAEMON = 0x1D COM_BINLOG_DUMP_GTID = 0x1E COM_END = 0x1F python-asyncmy-0.2.15/asyncmy/constants/CR.py000066400000000000000000000035701525423006000211600ustar00rootroot00000000000000# errmsg.h CR_ERROR_FIRST = 2000 CR_UNKNOWN_ERROR = 2000 CR_SOCKET_CREATE_ERROR = 2001 CR_CONNECTION_ERROR = 2002 CR_CONN_HOST_ERROR = 2003 CR_IPSOCK_ERROR = 2004 CR_UNKNOWN_HOST = 2005 CR_SERVER_GONE_ERROR = 2006 CR_VERSION_ERROR = 2007 CR_OUT_OF_MEMORY = 2008 CR_WRONG_HOST_INFO = 2009 CR_LOCALHOST_CONNECTION = 2010 CR_TCP_CONNECTION = 2011 CR_SERVER_HANDSHAKE_ERR = 2012 CR_SERVER_LOST = 2013 CR_COMMANDS_OUT_OF_SYNC = 2014 CR_NAMEDPIPE_CONNECTION = 2015 CR_NAMEDPIPEWAIT_ERROR = 2016 CR_NAMEDPIPEOPEN_ERROR = 2017 CR_NAMEDPIPESETSTATE_ERROR = 2018 CR_CANT_READ_CHARSET = 2019 CR_NET_PACKET_TOO_LARGE = 2020 CR_EMBEDDED_CONNECTION = 2021 CR_PROBE_SLAVE_STATUS = 2022 CR_PROBE_SLAVE_HOSTS = 2023 CR_PROBE_SLAVE_CONNECT = 2024 CR_PROBE_MASTER_CONNECT = 2025 CR_SSL_CONNECTION_ERROR = 2026 CR_MALFORMED_PACKET = 2027 CR_WRONG_LICENSE = 2028 CR_NULL_POINTER = 2029 CR_NO_PREPARE_STMT = 2030 CR_PARAMS_NOT_BOUND = 2031 CR_DATA_TRUNCATED = 2032 CR_NO_PARAMETERS_EXISTS = 2033 CR_INVALID_PARAMETER_NO = 2034 CR_INVALID_BUFFER_USE = 2035 CR_UNSUPPORTED_PARAM_TYPE = 2036 CR_SHARED_MEMORY_CONNECTION = 2037 CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038 CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039 CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040 CR_SHARED_MEMORY_CONNECT_MAP_ERROR = 2041 CR_SHARED_MEMORY_FILE_MAP_ERROR = 2042 CR_SHARED_MEMORY_MAP_ERROR = 2043 CR_SHARED_MEMORY_EVENT_ERROR = 2044 CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045 CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046 CR_CONN_UNKNOW_PROTOCOL = 2047 CR_INVALID_CONN_HANDLE = 2048 CR_SECURE_AUTH = 2049 CR_FETCH_CANCELED = 2050 CR_NO_DATA = 2051 CR_NO_STMT_METADATA = 2052 CR_NO_RESULT_SET = 2053 CR_NOT_IMPLEMENTED = 2054 CR_SERVER_LOST_EXTENDED = 2055 CR_STMT_CLOSED = 2056 CR_NEW_STMT_METADATA = 2057 CR_ALREADY_CONNECTED = 2058 CR_AUTH_PLUGIN_CANNOT_LOAD = 2059 CR_DUPLICATE_CONNECTION_ATTR = 2060 CR_AUTH_PLUGIN_ERR = 2061 CR_ERROR_LAST = 2061 python-asyncmy-0.2.15/asyncmy/constants/ER.py000066400000000000000000000300101525423006000211470ustar00rootroot00000000000000ERROR_FIRST = 1000 HASHCHK = 1000 NISAMCHK = 1001 NO = 1002 YES = 1003 CANT_CREATE_FILE = 1004 CANT_CREATE_TABLE = 1005 CANT_CREATE_DB = 1006 DB_CREATE_EXISTS = 1007 DB_DROP_EXISTS = 1008 DB_DROP_DELETE = 1009 DB_DROP_RMDIR = 1010 CANT_DELETE_FILE = 1011 CANT_FIND_SYSTEM_REC = 1012 CANT_GET_STAT = 1013 CANT_GET_WD = 1014 CANT_LOCK = 1015 CANT_OPEN_FILE = 1016 FILE_NOT_FOUND = 1017 CANT_READ_DIR = 1018 CANT_SET_WD = 1019 CHECKREAD = 1020 DISK_FULL = 1021 DUP_KEY = 1022 ERROR_ON_CLOSE = 1023 ERROR_ON_READ = 1024 ERROR_ON_RENAME = 1025 ERROR_ON_WRITE = 1026 FILE_USED = 1027 FILSORT_ABORT = 1028 FORM_NOT_FOUND = 1029 GET_ERRNO = 1030 ILLEGAL_HA = 1031 KEY_NOT_FOUND = 1032 NOT_FORM_FILE = 1033 NOT_KEYFILE = 1034 OLD_KEYFILE = 1035 OPEN_AS_READONLY = 1036 OUTOFMEMORY = 1037 OUT_OF_SORTMEMORY = 1038 UNEXPECTED_EOF = 1039 CON_COUNT_ERROR = 1040 OUT_OF_RESOURCES = 1041 BAD_HOST_ERROR = 1042 HANDSHAKE_ERROR = 1043 DBACCESS_DENIED_ERROR = 1044 ACCESS_DENIED_ERROR = 1045 NO_DB_ERROR = 1046 UNKNOWN_COM_ERROR = 1047 BAD_NULL_ERROR = 1048 BAD_DB_ERROR = 1049 TABLE_EXISTS_ERROR = 1050 BAD_TABLE_ERROR = 1051 NON_UNIQ_ERROR = 1052 SERVER_SHUTDOWN = 1053 BAD_FIELD_ERROR = 1054 WRONG_FIELD_WITH_GROUP = 1055 WRONG_GROUP_FIELD = 1056 WRONG_SUM_SELECT = 1057 WRONG_VALUE_COUNT = 1058 TOO_LONG_IDENT = 1059 DUP_FIELDNAME = 1060 DUP_KEYNAME = 1061 DUP_ENTRY = 1062 WRONG_FIELD_SPEC = 1063 PARSE_ERROR = 1064 EMPTY_QUERY = 1065 NONUNIQ_TABLE = 1066 INVALID_DEFAULT = 1067 MULTIPLE_PRI_KEY = 1068 TOO_MANY_KEYS = 1069 TOO_MANY_KEY_PARTS = 1070 TOO_LONG_KEY = 1071 KEY_COLUMN_DOES_NOT_EXITS = 1072 BLOB_USED_AS_KEY = 1073 TOO_BIG_FIELDLENGTH = 1074 WRONG_AUTO_KEY = 1075 READY = 1076 NORMAL_SHUTDOWN = 1077 GOT_SIGNAL = 1078 SHUTDOWN_COMPLETE = 1079 FORCING_CLOSE = 1080 IPSOCK_ERROR = 1081 NO_SUCH_INDEX = 1082 WRONG_FIELD_TERMINATORS = 1083 BLOBS_AND_NO_TERMINATED = 1084 TEXTFILE_NOT_READABLE = 1085 FILE_EXISTS_ERROR = 1086 LOAD_INFO = 1087 ALTER_INFO = 1088 WRONG_SUB_KEY = 1089 CANT_REMOVE_ALL_FIELDS = 1090 CANT_DROP_FIELD_OR_KEY = 1091 INSERT_INFO = 1092 UPDATE_TABLE_USED = 1093 NO_SUCH_THREAD = 1094 KILL_DENIED_ERROR = 1095 NO_TABLES_USED = 1096 TOO_BIG_SET = 1097 NO_UNIQUE_LOGFILE = 1098 TABLE_NOT_LOCKED_FOR_WRITE = 1099 TABLE_NOT_LOCKED = 1100 BLOB_CANT_HAVE_DEFAULT = 1101 WRONG_DB_NAME = 1102 WRONG_TABLE_NAME = 1103 TOO_BIG_SELECT = 1104 UNKNOWN_ERROR = 1105 UNKNOWN_PROCEDURE = 1106 WRONG_PARAMCOUNT_TO_PROCEDURE = 1107 WRONG_PARAMETERS_TO_PROCEDURE = 1108 UNKNOWN_TABLE = 1109 FIELD_SPECIFIED_TWICE = 1110 INVALID_GROUP_FUNC_USE = 1111 UNSUPPORTED_EXTENSION = 1112 TABLE_MUST_HAVE_COLUMNS = 1113 RECORD_FILE_FULL = 1114 UNKNOWN_CHARACTER_SET = 1115 TOO_MANY_TABLES = 1116 TOO_MANY_FIELDS = 1117 TOO_BIG_ROWSIZE = 1118 STACK_OVERRUN = 1119 WRONG_OUTER_JOIN = 1120 NULL_COLUMN_IN_INDEX = 1121 CANT_FIND_UDF = 1122 CANT_INITIALIZE_UDF = 1123 UDF_NO_PATHS = 1124 UDF_EXISTS = 1125 CANT_OPEN_LIBRARY = 1126 CANT_FIND_DL_ENTRY = 1127 FUNCTION_NOT_DEFINED = 1128 HOST_IS_BLOCKED = 1129 HOST_NOT_PRIVILEGED = 1130 PASSWORD_ANONYMOUS_USER = 1131 PASSWORD_NOT_ALLOWED = 1132 PASSWORD_NO_MATCH = 1133 UPDATE_INFO = 1134 CANT_CREATE_THREAD = 1135 WRONG_VALUE_COUNT_ON_ROW = 1136 CANT_REOPEN_TABLE = 1137 INVALID_USE_OF_NULL = 1138 REGEXP_ERROR = 1139 MIX_OF_GROUP_FUNC_AND_FIELDS = 1140 NONEXISTING_GRANT = 1141 TABLEACCESS_DENIED_ERROR = 1142 COLUMNACCESS_DENIED_ERROR = 1143 ILLEGAL_GRANT_FOR_TABLE = 1144 GRANT_WRONG_HOST_OR_USER = 1145 NO_SUCH_TABLE = 1146 NONEXISTING_TABLE_GRANT = 1147 NOT_ALLOWED_COMMAND = 1148 SYNTAX_ERROR = 1149 DELAYED_CANT_CHANGE_LOCK = 1150 TOO_MANY_DELAYED_THREADS = 1151 ABORTING_CONNECTION = 1152 NET_PACKET_TOO_LARGE = 1153 NET_READ_ERROR_FROM_PIPE = 1154 NET_FCNTL_ERROR = 1155 NET_PACKETS_OUT_OF_ORDER = 1156 NET_UNCOMPRESS_ERROR = 1157 NET_READ_ERROR = 1158 NET_READ_INTERRUPTED = 1159 NET_ERROR_ON_WRITE = 1160 NET_WRITE_INTERRUPTED = 1161 TOO_LONG_STRING = 1162 TABLE_CANT_HANDLE_BLOB = 1163 TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164 DELAYED_INSERT_TABLE_LOCKED = 1165 WRONG_COLUMN_NAME = 1166 WRONG_KEY_COLUMN = 1167 WRONG_MRG_TABLE = 1168 DUP_UNIQUE = 1169 BLOB_KEY_WITHOUT_LENGTH = 1170 PRIMARY_CANT_HAVE_NULL = 1171 TOO_MANY_ROWS = 1172 REQUIRES_PRIMARY_KEY = 1173 NO_RAID_COMPILED = 1174 UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175 KEY_DOES_NOT_EXITS = 1176 CHECK_NO_SUCH_TABLE = 1177 CHECK_NOT_IMPLEMENTED = 1178 CANT_DO_THIS_DURING_AN_TRANSACTION = 1179 ERROR_DURING_COMMIT = 1180 ERROR_DURING_ROLLBACK = 1181 ERROR_DURING_FLUSH_LOGS = 1182 ERROR_DURING_CHECKPOINT = 1183 NEW_ABORTING_CONNECTION = 1184 DUMP_NOT_IMPLEMENTED = 1185 FLUSH_MASTER_BINLOG_CLOSED = 1186 INDEX_REBUILD = 1187 MASTER = 1188 MASTER_NET_READ = 1189 MASTER_NET_WRITE = 1190 FT_MATCHING_KEY_NOT_FOUND = 1191 LOCK_OR_ACTIVE_TRANSACTION = 1192 UNKNOWN_SYSTEM_VARIABLE = 1193 CRASHED_ON_USAGE = 1194 CRASHED_ON_REPAIR = 1195 WARNING_NOT_COMPLETE_ROLLBACK = 1196 TRANS_CACHE_FULL = 1197 SLAVE_MUST_STOP = 1198 SLAVE_NOT_RUNNING = 1199 BAD_SLAVE = 1200 MASTER_INFO = 1201 SLAVE_THREAD = 1202 TOO_MANY_USER_CONNECTIONS = 1203 SET_CONSTANTS_ONLY = 1204 LOCK_WAIT_TIMEOUT = 1205 LOCK_TABLE_FULL = 1206 READ_ONLY_TRANSACTION = 1207 DROP_DB_WITH_READ_LOCK = 1208 CREATE_DB_WITH_READ_LOCK = 1209 WRONG_ARGUMENTS = 1210 NO_PERMISSION_TO_CREATE_USER = 1211 UNION_TABLES_IN_DIFFERENT_DIR = 1212 LOCK_DEADLOCK = 1213 TABLE_CANT_HANDLE_FT = 1214 CANNOT_ADD_FOREIGN = 1215 NO_REFERENCED_ROW = 1216 ROW_IS_REFERENCED = 1217 CONNECT_TO_MASTER = 1218 QUERY_ON_MASTER = 1219 ERROR_WHEN_EXECUTING_COMMAND = 1220 WRONG_USAGE = 1221 WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222 CANT_UPDATE_WITH_READLOCK = 1223 MIXING_NOT_ALLOWED = 1224 DUP_ARGUMENT = 1225 USER_LIMIT_REACHED = 1226 SPECIFIC_ACCESS_DENIED_ERROR = 1227 LOCAL_VARIABLE = 1228 GLOBAL_VARIABLE = 1229 NO_DEFAULT = 1230 WRONG_VALUE_FOR_VAR = 1231 WRONG_TYPE_FOR_VAR = 1232 VAR_CANT_BE_READ = 1233 CANT_USE_OPTION_HERE = 1234 NOT_SUPPORTED_YET = 1235 MASTER_FATAL_ERROR_READING_BINLOG = 1236 SLAVE_IGNORED_TABLE = 1237 INCORRECT_GLOBAL_LOCAL_VAR = 1238 WRONG_FK_DEF = 1239 KEY_REF_DO_NOT_MATCH_TABLE_REF = 1240 OPERAND_COLUMNS = 1241 SUBQUERY_NO_1_ROW = 1242 UNKNOWN_STMT_HANDLER = 1243 CORRUPT_HELP_DB = 1244 CYCLIC_REFERENCE = 1245 AUTO_CONVERT = 1246 ILLEGAL_REFERENCE = 1247 DERIVED_MUST_HAVE_ALIAS = 1248 SELECT_REDUCED = 1249 TABLENAME_NOT_ALLOWED_HERE = 1250 NOT_SUPPORTED_AUTH_MODE = 1251 SPATIAL_CANT_HAVE_NULL = 1252 COLLATION_CHARSET_MISMATCH = 1253 SLAVE_WAS_RUNNING = 1254 SLAVE_WAS_NOT_RUNNING = 1255 TOO_BIG_FOR_UNCOMPRESS = 1256 ZLIB_Z_MEM_ERROR = 1257 ZLIB_Z_BUF_ERROR = 1258 ZLIB_Z_DATA_ERROR = 1259 CUT_VALUE_GROUP_CONCAT = 1260 WARN_TOO_FEW_RECORDS = 1261 WARN_TOO_MANY_RECORDS = 1262 WARN_NULL_TO_NOTNULL = 1263 WARN_DATA_OUT_OF_RANGE = 1264 WARN_DATA_TRUNCATED = 1265 WARN_USING_OTHER_HANDLER = 1266 CANT_AGGREGATE_2COLLATIONS = 1267 DROP_USER = 1268 REVOKE_GRANTS = 1269 CANT_AGGREGATE_3COLLATIONS = 1270 CANT_AGGREGATE_NCOLLATIONS = 1271 VARIABLE_IS_NOT_STRUCT = 1272 UNKNOWN_COLLATION = 1273 SLAVE_IGNORED_SSL_PARAMS = 1274 SERVER_IS_IN_SECURE_AUTH_MODE = 1275 WARN_FIELD_RESOLVED = 1276 BAD_SLAVE_UNTIL_COND = 1277 MISSING_SKIP_SLAVE = 1278 UNTIL_COND_IGNORED = 1279 WRONG_NAME_FOR_INDEX = 1280 WRONG_NAME_FOR_CATALOG = 1281 WARN_QC_RESIZE = 1282 BAD_FT_COLUMN = 1283 UNKNOWN_KEY_CACHE = 1284 WARN_HOSTNAME_WONT_WORK = 1285 UNKNOWN_STORAGE_ENGINE = 1286 WARN_DEPRECATED_SYNTAX = 1287 NON_UPDATABLE_TABLE = 1288 FEATURE_DISABLED = 1289 OPTION_PREVENTS_STATEMENT = 1290 DUPLICATED_VALUE_IN_TYPE = 1291 TRUNCATED_WRONG_VALUE = 1292 TOO_MUCH_AUTO_TIMESTAMP_COLS = 1293 INVALID_ON_UPDATE = 1294 UNSUPPORTED_PS = 1295 GET_ERRMSG = 1296 GET_TEMPORARY_ERRMSG = 1297 UNKNOWN_TIME_ZONE = 1298 WARN_INVALID_TIMESTAMP = 1299 INVALID_CHARACTER_STRING = 1300 WARN_ALLOWED_PACKET_OVERFLOWED = 1301 CONFLICTING_DECLARATIONS = 1302 SP_NO_RECURSIVE_CREATE = 1303 SP_ALREADY_EXISTS = 1304 SP_DOES_NOT_EXIST = 1305 SP_DROP_FAILED = 1306 SP_STORE_FAILED = 1307 SP_LILABEL_MISMATCH = 1308 SP_LABEL_REDEFINE = 1309 SP_LABEL_MISMATCH = 1310 SP_UNINIT_VAR = 1311 SP_BADSELECT = 1312 SP_BADRETURN = 1313 SP_BADSTATEMENT = 1314 UPDATE_LOG_DEPRECATED_IGNORED = 1315 UPDATE_LOG_DEPRECATED_TRANSLATED = 1316 QUERY_INTERRUPTED = 1317 SP_WRONG_NO_OF_ARGS = 1318 SP_COND_MISMATCH = 1319 SP_NORETURN = 1320 SP_NORETURNEND = 1321 SP_BAD_CURSOR_QUERY = 1322 SP_BAD_CURSOR_SELECT = 1323 SP_CURSOR_MISMATCH = 1324 SP_CURSOR_ALREADY_OPEN = 1325 SP_CURSOR_NOT_OPEN = 1326 SP_UNDECLARED_VAR = 1327 SP_WRONG_NO_OF_FETCH_ARGS = 1328 SP_FETCH_NO_DATA = 1329 SP_DUP_PARAM = 1330 SP_DUP_VAR = 1331 SP_DUP_COND = 1332 SP_DUP_CURS = 1333 SP_CANT_ALTER = 1334 SP_SUBSELECT_NYI = 1335 STMT_NOT_ALLOWED_IN_SF_OR_TRG = 1336 SP_VARCOND_AFTER_CURSHNDLR = 1337 SP_CURSOR_AFTER_HANDLER = 1338 SP_CASE_NOT_FOUND = 1339 FPARSER_TOO_BIG_FILE = 1340 FPARSER_BAD_HEADER = 1341 FPARSER_EOF_IN_COMMENT = 1342 FPARSER_ERROR_IN_PARAMETER = 1343 FPARSER_EOF_IN_UNKNOWN_PARAMETER = 1344 VIEW_NO_EXPLAIN = 1345 FRM_UNKNOWN_TYPE = 1346 WRONG_OBJECT = 1347 NONUPDATEABLE_COLUMN = 1348 VIEW_SELECT_DERIVED = 1349 VIEW_SELECT_CLAUSE = 1350 VIEW_SELECT_VARIABLE = 1351 VIEW_SELECT_TMPTABLE = 1352 VIEW_WRONG_LIST = 1353 WARN_VIEW_MERGE = 1354 WARN_VIEW_WITHOUT_KEY = 1355 VIEW_INVALID = 1356 SP_NO_DROP_SP = 1357 SP_GOTO_IN_HNDLR = 1358 TRG_ALREADY_EXISTS = 1359 TRG_DOES_NOT_EXIST = 1360 TRG_ON_VIEW_OR_TEMP_TABLE = 1361 TRG_CANT_CHANGE_ROW = 1362 TRG_NO_SUCH_ROW_IN_TRG = 1363 NO_DEFAULT_FOR_FIELD = 1364 DIVISION_BY_ZERO = 1365 TRUNCATED_WRONG_VALUE_FOR_FIELD = 1366 ILLEGAL_VALUE_FOR_TYPE = 1367 VIEW_NONUPD_CHECK = 1368 VIEW_CHECK_FAILED = 1369 PROCACCESS_DENIED_ERROR = 1370 RELAY_LOG_FAIL = 1371 PASSWD_LENGTH = 1372 UNKNOWN_TARGET_BINLOG = 1373 IO_ERR_LOG_INDEX_READ = 1374 BINLOG_PURGE_PROHIBITED = 1375 FSEEK_FAIL = 1376 BINLOG_PURGE_FATAL_ERR = 1377 LOG_IN_USE = 1378 LOG_PURGE_UNKNOWN_ERR = 1379 RELAY_LOG_INIT = 1380 NO_BINARY_LOGGING = 1381 RESERVED_SYNTAX = 1382 WSAS_FAILED = 1383 DIFF_GROUPS_PROC = 1384 NO_GROUP_FOR_PROC = 1385 ORDER_WITH_PROC = 1386 LOGGING_PROHIBIT_CHANGING_OF = 1387 NO_FILE_MAPPING = 1388 WRONG_MAGIC = 1389 PS_MANY_PARAM = 1390 KEY_PART_0 = 1391 VIEW_CHECKSUM = 1392 VIEW_MULTIUPDATE = 1393 VIEW_NO_INSERT_FIELD_LIST = 1394 VIEW_DELETE_MERGE_VIEW = 1395 CANNOT_USER = 1396 XAER_NOTA = 1397 XAER_INVAL = 1398 XAER_RMFAIL = 1399 XAER_OUTSIDE = 1400 XAER_RMERR = 1401 XA_RBROLLBACK = 1402 NONEXISTING_PROC_GRANT = 1403 PROC_AUTO_GRANT_FAIL = 1404 PROC_AUTO_REVOKE_FAIL = 1405 DATA_TOO_LONG = 1406 SP_BAD_SQLSTATE = 1407 STARTUP = 1408 LOAD_FROM_FIXED_SIZE_ROWS_TO_VAR = 1409 CANT_CREATE_USER_WITH_GRANT = 1410 WRONG_VALUE_FOR_TYPE = 1411 TABLE_DEF_CHANGED = 1412 SP_DUP_HANDLER = 1413 SP_NOT_VAR_ARG = 1414 SP_NO_RETSET = 1415 CANT_CREATE_GEOMETRY_OBJECT = 1416 FAILED_ROUTINE_BREAK_BINLOG = 1417 BINLOG_UNSAFE_ROUTINE = 1418 BINLOG_CREATE_ROUTINE_NEED_SUPER = 1419 EXEC_STMT_WITH_OPEN_CURSOR = 1420 STMT_HAS_NO_OPEN_CURSOR = 1421 COMMIT_NOT_ALLOWED_IN_SF_OR_TRG = 1422 NO_DEFAULT_FOR_VIEW_FIELD = 1423 SP_NO_RECURSION = 1424 TOO_BIG_SCALE = 1425 TOO_BIG_PRECISION = 1426 M_BIGGER_THAN_D = 1427 WRONG_LOCK_OF_SYSTEM_TABLE = 1428 CONNECT_TO_FOREIGN_DATA_SOURCE = 1429 QUERY_ON_FOREIGN_DATA_SOURCE = 1430 FOREIGN_DATA_SOURCE_DOESNT_EXIST = 1431 FOREIGN_DATA_STRING_INVALID_CANT_CREATE = 1432 FOREIGN_DATA_STRING_INVALID = 1433 CANT_CREATE_FEDERATED_TABLE = 1434 TRG_IN_WRONG_SCHEMA = 1435 STACK_OVERRUN_NEED_MORE = 1436 TOO_LONG_BODY = 1437 WARN_CANT_DROP_DEFAULT_KEYCACHE = 1438 TOO_BIG_DISPLAYWIDTH = 1439 XAER_DUPID = 1440 DATETIME_FUNCTION_OVERFLOW = 1441 CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG = 1442 VIEW_PREVENT_UPDATE = 1443 PS_NO_RECURSION = 1444 SP_CANT_SET_AUTOCOMMIT = 1445 MALFORMED_DEFINER = 1446 VIEW_FRM_NO_USER = 1447 VIEW_OTHER_USER = 1448 NO_SUCH_USER = 1449 FORBID_SCHEMA_CHANGE = 1450 ROW_IS_REFERENCED_2 = 1451 NO_REFERENCED_ROW_2 = 1452 SP_BAD_VAR_SHADOW = 1453 TRG_NO_DEFINER = 1454 OLD_FILE_FORMAT = 1455 SP_RECURSION_LIMIT = 1456 SP_PROC_TABLE_CORRUPT = 1457 SP_WRONG_NAME = 1458 TABLE_NEEDS_UPGRADE = 1459 SP_NO_AGGREGATE = 1460 MAX_PREPARED_STMT_COUNT_REACHED = 1461 VIEW_RECURSIVE = 1462 NON_GROUPING_FIELD_USED = 1463 TABLE_CANT_HANDLE_SPKEYS = 1464 NO_TRIGGERS_ON_SYSTEM_SCHEMA = 1465 USERNAME = 1466 HOSTNAME = 1467 WRONG_STRING_LENGTH = 1468 ERROR_LAST = 1468 # https://github.com/PyMySQL/PyMySQL/issues/607 CONSTRAINT_FAILED = 4025 python-asyncmy-0.2.15/asyncmy/constants/FIELD_TYPE.py000066400000000000000000000005621525423006000223360ustar00rootroot00000000000000DECIMAL = 0 TINY = 1 SHORT = 2 LONG = 3 FLOAT = 4 DOUBLE = 5 NULL = 6 TIMESTAMP = 7 LONGLONG = 8 INT24 = 9 DATE = 10 TIME = 11 DATETIME = 12 YEAR = 13 NEWDATE = 14 VARCHAR = 15 BIT = 16 JSON = 245 NEWDECIMAL = 246 ENUM = 247 SET = 248 TINY_BLOB = 249 MEDIUM_BLOB = 250 LONG_BLOB = 251 BLOB = 252 VAR_STRING = 253 STRING = 254 GEOMETRY = 255 CHAR = TINY INTERVAL = ENUM python-asyncmy-0.2.15/asyncmy/constants/FLAG.py000066400000000000000000000003261525423006000213610ustar00rootroot00000000000000NOT_NULL = 1 PRI_KEY = 2 UNIQUE_KEY = 4 MULTIPLE_KEY = 8 BLOB = 16 UNSIGNED = 32 ZEROFILL = 64 BINARY = 128 ENUM = 256 AUTO_INCREMENT = 512 TIMESTAMP = 1024 SET = 2048 PART_KEY = 16384 GROUP = 32767 UNIQUE = 65536 python-asyncmy-0.2.15/asyncmy/constants/SERVER_STATUS.py000066400000000000000000000005151525423006000227610ustar00rootroot00000000000000SERVER_STATUS_IN_TRANS = 1 SERVER_STATUS_AUTOCOMMIT = 2 SERVER_MORE_RESULTS_EXISTS = 8 SERVER_QUERY_NO_GOOD_INDEX_USED = 16 SERVER_QUERY_NO_INDEX_USED = 32 SERVER_STATUS_CURSOR_EXISTS = 64 SERVER_STATUS_LAST_ROW_SENT = 128 SERVER_STATUS_DB_DROPPED = 256 SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512 SERVER_STATUS_METADATA_CHANGED = 1024 python-asyncmy-0.2.15/asyncmy/constants/__init__.py000066400000000000000000000000001525423006000223740ustar00rootroot00000000000000python-asyncmy-0.2.15/asyncmy/contexts.py000066400000000000000000000054171525423006000205110ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Coroutine from typing import Any, Generic, TypeVar from collections.abc import Generator, Iterator _T = TypeVar("_T") class _ContextManager(Coroutine[Any, Any, _T], Generic[_T]): """Awaitable that is also an async context manager yielding ``_T``. ``await connect(...)`` and ``async with connect(...)`` both hand back the same object, so the parameter is what lets callers get a real type out of either spelling. """ __slots__ = ("_coro", "_obj") def __init__(self, coro: Coroutine) -> None: self._coro = coro self._obj: Any = None def send(self, value) -> Any: return self._coro.send(value) def throw(self, typ, val=None, tb=None) -> Any: if val is None: return self._coro.throw(typ) elif tb is None: return self._coro.throw(typ, val) else: return self._coro.throw(typ, val, tb) def close(self) -> None: return self._coro.close() @property def gi_frame(self) -> Any: return self._coro.gi_frame # type:ignore[attr-defined] @property def gi_running(self) -> Any: return self._coro.gi_running # type:ignore[attr-defined] @property def gi_code(self) -> Any: return self._coro.gi_code # type:ignore[attr-defined] def __next__(self) -> Any: return self.send(None) def __iter__(self) -> Iterator: return self._coro.__await__() def __await__(self) -> Generator[Any, None, _T]: return self._coro.__await__() async def __aenter__(self) -> _T: self._obj = await self._coro return self._obj async def __aexit__(self, exc_type, exc, tb) -> None: await self._obj.close() self._obj = None class _PoolContextManager(_ContextManager[_T]): async def __aexit__(self, exc_type, exc, tb) -> None: self._obj.close() await self._obj.wait_closed() self._obj = None class _PoolAcquireContextManager(_ContextManager[_T]): __slots__ = ("_coro", "_conn", "_pool") def __init__(self, coro, pool) -> None: super().__init__(coro) self._coro = coro self._conn: Any = None self._pool = pool async def __aenter__(self) -> _T: self._conn = await self._coro return self._conn async def __aexit__(self, exc_type, exc, tb) -> None: try: await self._pool.release(self._conn) finally: self._pool = None self._conn = None class _ConnectionContextManager(_ContextManager[_T]): async def __aexit__(self, exc_type, exc, tb) -> None: if exc_type is not None: self._obj.close() else: await self._obj.ensure_closed() self._obj = None python-asyncmy-0.2.15/asyncmy/converters.pyi000066400000000000000000000076221525423006000212050ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/converters.pyx import re import time import datetime from .constants.FIELD_TYPE import * DATETIME_RE = re.compile( "(\\d{1,4})-(\\d{1,2})-(\\d{1,2})[T ](\\d{1,2}):(\\d{1,2}):(\\d{1,2})(?:.(\\d{1,6}))?" ) TIMEDELTA_RE = re.compile("(-)?(\\d{1,3}):(\\d{1,2}):(\\d{1,2})(?:.(\\d{1,6}))?") TIME_RE = re.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})(?:.(\\d{1,6}))?") convert_bit = through conversions: dict def escape_item(val, charset: str, mapping: dict | None = None): ... def escape_sequence(val: tuple, charset: str, mapping: dict | None = None) -> str: ... def escape_set(val: set, charset: str, mapping: dict | None = None) -> str: ... def escape_bool(value: int, mapping: dict | None = None) -> str: ... def escape_int(value, mapping: dict | None = None) -> str: ... def escape_float(value: float, mapping: dict | None = None) -> str: ... def escape_string(value, mapping: dict | None = None) -> str: """ escapes *value* without adding quote. Value should be unicode """ def escape_bytes_prefixed(value: bytes, mapping: dict | None = None) -> str: ... def escape_bytes(value: bytes, mapping: dict | None = None) -> str: ... def escape_str(value, mapping: dict | None = None) -> str: ... def escape_None(value, mapping: dict | None = None) -> str: ... def escape_timedelta(obj: datetime.timedelta, mapping: dict | None = None) -> str: ... def escape_time(obj, mapping: dict | None = None) -> str: ... def escape_datetime(obj: datetime.datetime, mapping: dict | None = None) -> str: ... def escape_date(obj: datetime.date, mapping: dict | None = None) -> str: ... def escape_struct_time(obj: time.struct_time, mapping: dict | None = None) -> str: ... def decimal2literal(o, d) -> str: ... def convert_datetime(obj: object) -> object: """Returns a DATETIME or TIMESTAMP column value as a datetime object: >>> convert_datetime('2007-02-25 23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) >>> convert_datetime('2007-02-25T23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) Illegal values are returned as str: >>> convert_datetime('2007-02-31T23:06:20') '2007-02-31T23:06:20' >>> convert_datetime('0000-00-00 00:00:00') '0000-00-00 00:00:00' """ def convert_timedelta(obj: object) -> object: """Returns a TIME column as a timedelta object: >>> convert_timedelta('25:06:17') datetime.timedelta(1, 3977) >>> convert_timedelta('-25:06:17') datetime.timedelta(-2, 83177) Illegal values are returned as str: >>> convert_timedelta('random crap') 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. """ def convert_time(obj: object) -> object: """Returns a TIME column as a time object: >>> convert_time('15:06:17') datetime.time(15, 6, 17) Illegal values are returned as str: >>> convert_time('-25:06:17') '-25:06:17' >>> convert_time('random crap') 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. Also note that MySQL's TIME column corresponds more closely to Python's timedelta and not time. However if you want TIME columns to be treated as time-of-day and not a time offset, then you can use set this function as the converter for TIME. """ def convert_date(obj) -> object: """Returns a DATE column as a date object: >>> convert_date('2007-02-26') datetime.date(2007, 2, 26) Illegal values are returned as str: >>> convert_date('2007-02-31') '2007-02-31' >>> convert_date('0000-00-00') '0000-00-00' """ def through(x): ... python-asyncmy-0.2.15/asyncmy/converters.pyx000066400000000000000000000450351525423006000212240ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. import re import time from decimal import Decimal from cpython cimport datetime from cpython.bytes cimport PyBytes_AS_STRING, PyBytes_GET_SIZE from .constants.FIELD_TYPE import * from .errors import ProgrammingError datetime.import_datetime() cdef inline int _p2(const unsigned char *s) noexcept: """Parse exactly two ASCII digits; -1 if not digits.""" if s[0] < 48 or s[0] > 57 or s[1] < 48 or s[1] > 57: return -1 return (s[0] - 48) * 10 + (s[1] - 48) cdef inline int _p4(const unsigned char *s) noexcept: """Parse exactly four ASCII digits; -1 if not digits.""" cdef int i, v = 0 for i in range(4): if s[i] < 48 or s[i] > 57: return -1 v = v * 10 + (s[i] - 48) return v cdef inline int _pfrac(const unsigned char *s, Py_ssize_t n) noexcept: """Parse up to six fractional-second digits, right-padded to microseconds.""" cdef: int v = 0 Py_ssize_t i = 0 while i < n and i < 6: if s[i] < 48 or s[i] > 57: return -1 v = v * 10 + (s[i] - 48) i += 1 while i < 6: v *= 10 i += 1 return v cdef inline int _days_in_month(int year, int month) noexcept: if month == 2: if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0: return 29 return 28 if month == 4 or month == 6 or month == 9 or month == 11: return 30 return 31 cpdef escape_item(val, str charset, mapping: dict = None): if mapping is None: mapping = encoders encoder = mapping.get(type(val)) # Fallback to default when no encoder found if not encoder: try: encoder = mapping[str] except KeyError: raise TypeError("no default type converter defined") if encoder is escape_sequence: val = encoder(val, charset, mapping) else: val = encoder(val, mapping) return val cpdef str escape_sequence(tuple val, str charset, mapping: dict = None): n = [] for item in val: quoted = escape_item(item, charset, mapping) n.append(quoted) return "(" + ",".join(n) + ")" cpdef str escape_set(set val, str charset, mapping: dict = None): return ",".join([escape_item(x, charset, mapping) for x in val]) cpdef str escape_bool(int value, mapping: dict = None): return str(int(value)) cpdef str escape_int(value, mapping: dict = None): # Accept arbitrary Python ints: a C integer parameter would overflow on # values outside [LLONG_MIN, LLONG_MAX], e.g. unsigned BIGINT 2**64-1 (#35). return str(value) cpdef str escape_float(double value, mapping: dict = None): s = repr(value) if s in ("inf", "nan"): raise ProgrammingError("%s can not be used with MySQL" % s) if "e" not in s: s += "e0" return s cdef list _escape_table = [chr(x) for x in range(128)] _escape_table[0] = "\\0" _escape_table[ord("\\")] = "\\\\" _escape_table[ord("\n")] = "\\n" _escape_table[ord("\r")] = "\\r" _escape_table[ord("\032")] = "\\Z" _escape_table[ord('"')] = '\\"' _escape_table[ord("'")] = "\\'" cdef inline bint _str_needs_escape(str s): cdef Py_UCS4 ch for ch in s: if ch == 0 or ch == 10 or ch == 13 or ch == 26 or ch == 34 or ch == 39 or ch == 92: return True return False cdef inline bint _bytes_need_escape(bytes value): cdef: const unsigned char *p = PyBytes_AS_STRING(value) Py_ssize_t i, n = PyBytes_GET_SIZE(value) unsigned char c for i in range(n): c = p[i] if c == 0 or c == 10 or c == 13 or c == 26 or c == 34 or c == 39 or c == 92: return True return False cpdef str escape_string(value, mapping: dict = None): """ escapes *value* without adding quote. Value should be unicode """ # Fast path: most strings contain nothing that needs escaping. if type(value) is str and not _str_needs_escape( value): return value return value.translate(_escape_table) cpdef str escape_bytes_prefixed(bytes value, mapping: dict = None): if not _bytes_need_escape(value): return "_binary'%s'" % value.decode("ascii", "surrogateescape") return "_binary'%s'" % value.decode("ascii", "surrogateescape").translate(_escape_table) cpdef str escape_bytes(bytes value, mapping: dict = None): if not _bytes_need_escape(value): return "'%s'" % value.decode("ascii", "surrogateescape") return "'%s'" % value.decode("ascii", "surrogateescape").translate(_escape_table) cpdef str escape_str(value, mapping: dict = None): return "'%s'" % escape_string(str(value), mapping) cpdef str escape_None(value, mapping: dict = None): return "NULL" cpdef str escape_timedelta(obj: datetime.timedelta, mapping: dict = None): seconds = int(obj.seconds) % 60 minutes = int(obj.seconds // 60) % 60 hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24 if obj.microseconds: fmt = "'{0:02d}:{1:02d}:{2:02d}.{3:06d}'" else: fmt = "'{0:02d}:{1:02d}:{2:02d}'" return fmt.format(hours, minutes, seconds, obj.microseconds) cpdef str escape_time(obj, mapping: dict = None): if obj.microsecond: fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" else: fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}'" return fmt.format(obj) cpdef str escape_datetime(datetime.datetime obj, mapping: dict = None): if obj.microsecond: fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" else: fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'" return fmt.format(obj) cpdef str escape_date(datetime.date obj, mapping: dict = None): fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'" return fmt.format(obj) cpdef str escape_struct_time(obj: time.struct_time, mapping: dict = None): return escape_datetime(datetime.datetime(*obj[:6])) cpdef str decimal2literal(o, d): return format(o, "f") cpdef int _convert_second_fraction(s): if not s: return 0 # Pad zeros to ensure the fraction length in microseconds s = s.ljust(6, "0") return int(s[:6]) DATETIME_RE = re.compile( r"(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?" ) cpdef object convert_datetime(object obj): """Returns a DATETIME or TIMESTAMP column value as a datetime object: >>> convert_datetime('2007-02-25 23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) >>> convert_datetime('2007-02-25T23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) Illegal values are returned as str: >>> convert_datetime('2007-02-31T23:06:20') '2007-02-31T23:06:20' >>> convert_datetime('0000-00-00 00:00:00') '0000-00-00 00:00:00' """ cdef: const unsigned char *s Py_ssize_t n int b_year, b_month, b_day, b_hour, b_minute, b_second, b_usec if type(obj) is bytes: # Fast path: parse "YYYY-MM-DD HH:MM:SS[.ffffff]" straight from bytes, # skipping the intermediate str allocation. s = PyBytes_AS_STRING( obj) n = PyBytes_GET_SIZE( obj) if ( n >= 19 and s[4] == 45 and s[7] == 45 # '-' and (s[10] == 32 or s[10] == 84) # ' ' or 'T' and s[13] == 58 and s[16] == 58 # ':' ): b_year = _p4(s) b_month = _p2(s + 5) b_day = _p2(s + 8) b_hour = _p2(s + 11) b_minute = _p2(s + 14) b_second = _p2(s + 17) if n > 20 and s[19] == 46: # '.' b_usec = _pfrac(s + 20, n - 20) elif n == 19: b_usec = 0 else: b_usec = -1 if ( b_year >= 1 and 1 <= b_month <= 12 and 1 <= b_day <= _days_in_month(b_year, b_month) and 0 <= b_hour < 24 and 0 <= b_minute < 60 and 0 <= b_second < 60 and b_usec >= 0 ): return datetime.datetime_new( b_year, b_month, b_day, b_hour, b_minute, b_second, b_usec, None ) obj = ( obj).decode("ascii") elif isinstance(obj, bytearray): obj = obj.decode("ascii") # Fast path: Use string slicing for standard MySQL datetime format # Format: "YYYY-MM-DD HH:MM:SS" (19 chars) or "YYYY-MM-DD HH:MM:SS.ffffff" (26 chars) cdef int obj_len = len(obj) if obj_len >= 19: try: # Extract components using string slicing (faster than regex) # "2007-02-25 23:06:20.123456" # 0123456789012345678901234 year = int(obj[0:4]) month = int(obj[5:7]) day = int(obj[8:10]) hour = int(obj[11:13]) minute = int(obj[14:16]) second = int(obj[17:19]) # Check for microseconds if obj_len > 20 and obj[19] == '.': microsecond = _convert_second_fraction(obj[20:]) else: microsecond = 0 return datetime.datetime(year, month, day, hour, minute, second, microsecond) except (ValueError, IndexError): pass # Fallback to regex for non-standard formats m = DATETIME_RE.match(obj) if not m: return convert_date(obj) try: groups = list(m.groups()) groups[-1] = _convert_second_fraction(groups[-1]) return datetime.datetime(*[int(x) for x in groups]) except ValueError: return convert_date(obj) TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") cpdef object convert_timedelta(object obj): """Returns a TIME column as a timedelta object: >>> convert_timedelta('25:06:17') datetime.timedelta(1, 3977) >>> convert_timedelta('-25:06:17') datetime.timedelta(-2, 83177) Illegal values are returned as str: >>> convert_timedelta('random crap') 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. """ cdef: const unsigned char *s Py_ssize_t n, idx, k, mm_pos bint neg = False int b_hours, b_minutes, b_seconds, b_usec if type(obj) is bytes: # Fast path: parse "[-]H{1,3}:MM:SS[.ffffff]" straight from bytes. s = PyBytes_AS_STRING( obj) n = PyBytes_GET_SIZE( obj) idx = 0 if n > 0 and s[0] == 45: # '-' neg = True idx = 1 b_hours = 0 k = 0 while idx + k < n and k < 3 and 48 <= s[idx + k] <= 57: b_hours = b_hours * 10 + (s[idx + k] - 48) k += 1 if k > 0 and idx + k + 6 <= n and s[idx + k] == 58 and s[idx + k + 3] == 58: # ':' mm_pos = idx + k + 1 b_minutes = _p2(s + mm_pos) b_seconds = _p2(s + mm_pos + 3) if n > mm_pos + 6 and s[mm_pos + 5] == 46: # '.' b_usec = _pfrac(s + mm_pos + 6, n - (mm_pos + 6)) elif n == mm_pos + 5: b_usec = 0 else: b_usec = -1 if 0 <= b_minutes < 60 and 0 <= b_seconds < 60 and b_usec >= 0: if neg: return datetime.timedelta_new( 0, -(b_hours * 3600 + b_minutes * 60 + b_seconds), -b_usec ) return datetime.timedelta_new( 0, b_hours * 3600 + b_minutes * 60 + b_seconds, b_usec ) obj = ( obj).decode("ascii") elif isinstance(obj, bytearray): obj = obj.decode("ascii") m = TIMEDELTA_RE.match(obj) if not m: return obj try: groups = list(m.groups()) groups[-1] = _convert_second_fraction(groups[-1]) negate = -1 if groups[0] else 1 hours, minutes, seconds, microseconds = groups[1:] tdelta = ( datetime.timedelta( hours=int(hours), minutes=int(minutes), seconds=int(seconds), microseconds=int(microseconds), ) * negate ) return tdelta except ValueError: return obj TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") cpdef object convert_time(object obj): """Returns a TIME column as a time object: >>> convert_time('15:06:17') datetime.time(15, 6, 17) Illegal values are returned as str: >>> convert_time('-25:06:17') '-25:06:17' >>> convert_time('random crap') 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. Also note that MySQL's TIME column corresponds more closely to Python's timedelta and not time. However if you want TIME columns to be treated as time-of-day and not a time offset, then you can use set this function as the converter for TIME. """ cdef: const unsigned char *s Py_ssize_t n int b_hour, b_minute, b_second, b_usec if type(obj) is bytes: # Fast path: parse "HH:MM:SS[.ffffff]" straight from bytes. s = PyBytes_AS_STRING( obj) n = PyBytes_GET_SIZE( obj) if n >= 8 and s[2] == 58 and s[5] == 58: # ':' b_hour = _p2(s) b_minute = _p2(s + 3) b_second = _p2(s + 6) if n > 9 and s[8] == 46: # '.' b_usec = _pfrac(s + 9, n - 9) elif n == 8: b_usec = 0 else: b_usec = -1 if ( 0 <= b_hour < 24 and 0 <= b_minute < 60 and 0 <= b_second < 60 and b_usec >= 0 ): return datetime.time_new(b_hour, b_minute, b_second, b_usec, None) obj = ( obj).decode("ascii") elif isinstance(obj, bytearray): obj = obj.decode("ascii") # Fast path: Use string slicing for standard MySQL time format # Format: "HH:MM:SS" (8 chars) or "HH:MM:SS.ffffff" (15 chars) cdef int obj_len = len(obj) if obj_len >= 8 and obj[0] != '-': try: # "15:06:17.123456" # 01234567 hour = int(obj[0:2]) minute = int(obj[3:5]) second = int(obj[6:8]) if obj_len > 9 and obj[8] == '.': microsecond = _convert_second_fraction(obj[9:]) else: microsecond = 0 return datetime.time(hour, minute, second, microsecond) except (ValueError, IndexError): pass # Fallback to regex for non-standard formats m = TIME_RE.match(obj) if not m: return obj try: groups = list(m.groups()) groups[-1] = _convert_second_fraction(groups[-1]) hours, minutes, seconds, microseconds = groups return datetime.time( hour=int(hours), minute=int(minutes), second=int(seconds), microsecond=int(microseconds), ) except ValueError: return obj cpdef object convert_date(obj): """Returns a DATE column as a date object: >>> convert_date('2007-02-26') datetime.date(2007, 2, 26) Illegal values are returned as str: >>> convert_date('2007-02-31') '2007-02-31' >>> convert_date('0000-00-00') '0000-00-00' """ cdef: const unsigned char *s int b_year, b_month, b_day if type(obj) is bytes: # Fast path: parse "YYYY-MM-DD" straight from bytes. s = PyBytes_AS_STRING( obj) if PyBytes_GET_SIZE( obj) == 10 and s[4] == 45 and s[7] == 45: # '-' b_year = _p4(s) b_month = _p2(s + 5) b_day = _p2(s + 8) if ( b_year >= 1 and 1 <= b_month <= 12 and 1 <= b_day <= _days_in_month(b_year, b_month) ): return datetime.date_new(b_year, b_month, b_day) obj = ( obj).decode("ascii") elif isinstance(obj, bytearray): obj = obj.decode("ascii") # Fast path: Use string slicing for standard MySQL date format # Format: "YYYY-MM-DD" (10 chars) if len(obj) == 10: try: # "2007-02-26" # 0123456789 year = int(obj[0:4]) month = int(obj[5:7]) day = int(obj[8:10]) return datetime.date(year, month, day) except (ValueError, IndexError): pass # Fallback to split for non-standard formats try: return datetime.date(*[int(x) for x in obj.split("-", 2)]) except ValueError: return obj cpdef through(x): return x # def convert_bit(b): # b = "\x00" * (8 - len(b)) + b # pad w/ zeroes # return struct.unpack(">Q", b)[0] # # the snippet above is right, but MySQLdb doesn't process bits, # so we shouldn't either convert_bit = through cdef dict encoders = { bool: escape_bool, int: escape_int, float: escape_float, str: escape_str, bytes: escape_bytes, tuple: escape_sequence, list: escape_sequence, set: escape_sequence, frozenset: escape_sequence, type(None): escape_None, datetime.date: escape_date, datetime.datetime: escape_datetime, datetime.timedelta: escape_timedelta, datetime.time: escape_time, time.struct_time: escape_struct_time, Decimal: decimal2literal, } cdef dict decoders = { BIT: convert_bit, TINY: int, SHORT: int, LONG: int, FLOAT: float, DOUBLE: float, LONGLONG: int, INT24: int, YEAR: int, TIMESTAMP: convert_datetime, DATETIME: convert_datetime, TIME: convert_timedelta, DATE: convert_date, BLOB: through, TINY_BLOB: through, MEDIUM_BLOB: through, LONG_BLOB: through, STRING: through, VAR_STRING: through, VARCHAR: through, DECIMAL: Decimal, NEWDECIMAL: Decimal, } # for MySQLdb compatibility conversions = encoders.copy() conversions.update(decoders) python-asyncmy-0.2.15/asyncmy/cursors.pyi000066400000000000000000000154561525423006000205170ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/cursors.pyx from collections.abc import Generator from typing import Any from typing_extensions import Self import logging import re from asyncmy.connection import Connection from . import errors RE_INSERT_VALUES = re.compile( "\\s*((?:INSERT|REPLACE)\\b.+\\bVALUES?\\s*)" + "(\\(\\s*(?:%s|%\\(.+\\)s)\\s*(?:,\\s*(?:%s|%\\(.+\\)s)\\s*)*\\))" + "(\\s*(?:(?:AS|ON DUPLICATE).*)?);?\\s*\\Z", re.IGNORECASE | re.DOTALL, ) logger = logging.getLogger(__package__) class Cursor: """ This is the object used to interact with the database. Do not create an instance of a Cursor yourself. Call connections.Connection.cursor(). See `Cursor `_ in the specification. """ Warning = errors.Warning Error = errors.Error InterfaceError = errors.InterfaceError DatabaseError = errors.DatabaseError DataError = errors.DataError OperationalError = errors.OperationalError IntegrityError = errors.IntegrityError InternalError = errors.InternalError ProgrammingError = errors.ProgrammingError NotSupportedError = errors.NotSupportedError max_stmt_length: int rownumber: int arraysize: int rowcount: object description: tuple connection: Any lastrowid: int def __init__(self, connection: Connection, echo: bool = False, query_callback=None): ... async def close(self) -> None: """ Closing a cursor just exhausts all remaining data. """ def __aiter__(self): ... def __await__(self) -> Generator[Any, None, Self]: ... async def __aenter__(self) -> Self: ... async def __aexit__(self, exc_type, exc_val, exc_tb): ... async def __anext__(self): ... def setinputsizes(self, *args): """Does nothing, required by DB API.""" def setoutputsizes(self, *args): """Does nothing, required by DB API.""" async def nextset(self) -> bool | None: ... def mogrify(self, query, args=None) -> str: """ Returns the exact string that would be sent to the database by calling the execute() method. :param query: Query to mogrify. :type query: str :param args: Parameters used with query. (optional) :type args: tuple, list or dict :return: The query with argument binding applied. :rtype: str This method follows the extension to the DB API 2.0 followed by Psycopg. """ async def execute(self, query, args=None) -> int: """Execute a query. :param query: Query to execute. :type query: str :param args: Parameters used with query. (optional) :type args: tuple, list or dict :return: Number of affected rows. :rtype: int If args is a list or tuple, %s can be used as a placeholder in the query. If args is a dict, %(name)s can be used as a placeholder in the query. """ async def executemany(self, query: str, args) -> int | None: """Run several data against one query. :param query: Query to execute. :type query: str :param args: Sequence of sequences or mappings. It is used as parameter. :type args: tuple or list :return: Number of rows affected, if any. :rtype: int or None This method improves performance on multiple-row INSERT and REPLACE. Otherwise it is equivalent to looping over args with execute(). """ async def callproc(self, procname, args=()): """Execute stored procedure procname with args. :param procname: Name of procedure to execute on server. :type procname: str :param args: Sequence of parameters to use with procedure. :type args: tuple or list Returns the original args. Compatibility warning: PEP-249 specifies that any modified parameters must be returned. This is currently impossible as they are only available by storing them in a server variable and then retrieved by a query. Since stored procedures return zero or more result sets, there is no reliable way to get at OUT or INOUT parameters via callproc. The server variables are named @_procname_n, where procname is the parameter above and n is the position of the parameter (from zero). Once all result sets generated by the procedure have been fetched, you can issue a SELECT @_procname_0, ... query using .execute() to get any OUT or INOUT values. Compatibility warning: The act of calling a stored procedure itself creates an empty result set. This appears after any result sets generated by the procedure. This is non-standard behavior with respect to the DB-API. Be sure to use nextset() to advance through all result sets; otherwise you may get disconnected. """ async def fetchone(self) -> Any: """Fetch the next row.""" async def fetchmany(self, size=None) -> list[Any]: """Fetch several rows.""" async def fetchall(self) -> list[Any]: """Fetch all the rows.""" def scroll(self, value, mode="relative"): ... class DictCursorMixin: dict_type = dict class DictCursor(DictCursorMixin, Cursor): """A cursor which returns results as a dictionary""" class SSCursor(Cursor): """ Unbuffered Cursor, mainly useful for queries that return a lot of data, or for connections to remote servers over a slow network. Instead of copying every row of data into a buffer, this will fetch rows as needed. The upside of this is the client uses much less memory, and rows are returned much faster when traveling over a slow network or if the result set is very big. There are limitations, though. The MySQL protocol doesn't support returning the total number of rows, so the only way to tell how many rows there are is to iterate over every row returned. Also, it currently isn't possible to scroll backwards, as only the current row is held in memory. """ async def close(self) -> None: ... async def nextset(self) -> bool | None: ... async def read_next(self): """Read next row.""" async def fetchone(self) -> Any: """Fetch next row.""" async def fetchall(self) -> list[Any]: """ Fetch all, as per MySQLdb. Pretty useless for large queries, as it is buffered. See fetchall_unbuffered(), if you want an unbuffered generator version of this method. """ async def fetchmany(self, size=None) -> list[Any]: """Fetch many.""" async def scroll(self, value, mode="relative"): ... class SSDictCursor(DictCursorMixin, SSCursor): """An unbuffered cursor, which returns results as a dictionary""" python-asyncmy-0.2.15/asyncmy/cursors.pyx000066400000000000000000000555761525423006000205450ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. import logging import re import time import typing from . import errors #: Regular expression for :meth:`Cursor.executemany`. #: executemany only supports simple bulk insert. #: You can use it to load large dataset. RE_INSERT_VALUES = re.compile( r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)" + r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" + r"(\s*(?:(?:AS|ON DUPLICATE).*)?);?\s*\Z", re.IGNORECASE | re.DOTALL, ) logger = logging.getLogger(__package__) if typing.TYPE_CHECKING: from asyncmy.connection import Connection def _already_resolved(obj): """Iterator that finishes immediately with ``obj`` as the await result.""" return obj yield # noqa: unreachable - makes this a generator function cdef class Cursor: """ This is the object used to interact with the database. Do not create an instance of a Cursor yourself. Call connections.Connection.cursor(). See `Cursor `_ in the specification. """ #: Max statement size which :meth:`executemany` generates. #: #: Max size of allowed statement is max_allowed_packet - packet_header_size. #: Default value of max_allowed_packet is 1048576. cdef: public int max_stmt_length, rownumber, arraysize, _echo public object rowcount # may hold 2**64-1 for unbuffered cursors public tuple description public connection, _loop, _executed, _result, _rows public unsigned long long lastrowid public object _query_callback # Set while a batch operation reports as a whole, so the individual # execute() calls it delegates to stay silent. int _in_batch def __init__(self, connection: "Connection", echo: bool = False, query_callback=None): self.max_stmt_length = 1024000 self.connection = connection self.description = None self.rownumber = 0 self.rowcount = -1 self.arraysize = 1 self._executed = None self._result = None self._rows = None self._echo = echo self._query_callback = query_callback self._in_batch = 0 self._loop = self.connection.loop cdef inline bint _observed(self): """True when a statement's duration is worth measuring.""" return not self._in_batch and (self._echo or self._query_callback is not None) cdef _report(self, query, double start): """Emit the echo line and the query callback for one statement.""" cdef double elapsed = round((time.time() - start) * 1000, 2) if self._echo: logger.info(f"[{elapsed}ms] {query}") if self._query_callback is not None: self._query_callback(self, query, elapsed) async def close(self): """ Closing a cursor just exhausts all remaining data. """ conn = self.connection if conn is None: return try: while await self.nextset(): pass finally: self.connection = None def __aiter__(self): return self def __await__(self): # aiomysql's Connection.cursor() is a coroutine, so code written # against it does `cur = await conn.cursor()`. Ours returns the cursor # directly; making it awaitable keeps both spellings working (#145). return _already_resolved(self) async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() async def __anext__(self): ret = await self.fetchone() if ret is not None: return ret else: raise StopAsyncIteration # noqa cdef _get_db(self): if not self.connection: raise errors.ProgrammingError("Cursor closed") return self.connection cdef _check_executed(self): if not self._executed: raise errors.ProgrammingError("execute() first") cdef _conv_row(self, row): return row def setinputsizes(self, *args): """Does nothing, required by DB API.""" def setoutputsizes(self, *args): """Does nothing, required by DB API.""" async def _nextset(self, unbuffered=False): """Get the next query set.""" conn = self._get_db() current_result = self._result if current_result is None or current_result is not conn._result: return None if not current_result.has_next: return None self._result = None self._clear_result() await conn.next_result(unbuffered=unbuffered) await self._do_get_result() return True async def nextset(self): return await self._nextset(False) cdef _ensure_bytes(self, x, encoding=None): if isinstance(x, str): x = x.encode(encoding) elif isinstance(x, (tuple, list)): x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x) return x def _escape_args(self, args, conn): if isinstance(args, (tuple, list)): return tuple(conn.literal(arg) for arg in args) elif isinstance(args, dict): return {key: conn.literal(val) for (key, val) in args.items()} else: # If it's not a dictionary let's try escaping it anyways. # Worst case it will throw a Value error return conn.escape(args) cpdef mogrify(self, query, args=None): """ Returns the exact string that would be sent to the database by calling the execute() method. :param query: Query to mogrify. :type query: str :param args: Parameters used with query. (optional) :type args: tuple, list or dict :return: The query with argument binding applied. :rtype: str This method follows the extension to the DB API 2.0 followed by Psycopg. """ conn = self._get_db() if args is not None: query = query % self._escape_args(args, conn) return query async def execute(self, query, args=None): """Execute a query. :param query: Query to execute. :type query: str :param args: Parameters used with query. (optional) :type args: tuple, list or dict :return: Number of affected rows. :rtype: int If args is a list or tuple, %s can be used as a placeholder in the query. If args is a dict, %(name)s can be used as a placeholder in the query. """ while await self.nextset(): pass conn = self._get_db() # Transparent binary-protocol path: when the connection has a # statement cache, positional pyformat queries run as server-side # prepared statements (no escaping, binary row decoding). if ( args is not None and conn._stmt_cache_size > 0 and type(query) is str and not isinstance(args, dict) ): stmt_args = args if isinstance(args, (tuple, list)) else (args,) stmt = await conn._acquire_cached_statement(query, len(stmt_args)) if stmt is not None: observed = self._observed() if observed: start = time.time() self._clear_result() await stmt.execute(stmt_args) self._executed = query await self._do_get_result() if observed: self._report(query, start) return self.rowcount query = self.mogrify(query, args) if self._observed(): start = time.time() result = await self._query(query) self._executed = query self._report(query, start) else: result = await self._query(query) self._executed = query return result async def executemany(self, query: str, args): """Run several data against one query. :param query: Query to execute. :type query: str :param args: Sequence of sequences or mappings. It is used as parameter. :type args: tuple or list :return: Number of rows affected, if any. :rtype: int or None This method improves performance on multiple-row INSERT and REPLACE. Otherwise it is equivalent to looping over args with execute(). """ if not args: return if self._echo: logger.info("CALL %s", query) logger.info("%r", args) if self._query_callback is None: return await self._executemany(query, args) # Report the batch as one statement: the paths below may issue a # single bulk round-trip or loop over execute(), and the caller # cares about the executemany() they wrote either way. start = time.time() self._in_batch += 1 try: result = await self._executemany(query, args) finally: self._in_batch -= 1 # Reported after the fact, like echo: a statement that raised is not # reported, and a callback that raises cannot mask the database error. self._query_callback(self, query, round((time.time() - start) * 1000, 2)) return result async def _executemany(self, query: str, args): conn = self._get_db() # MariaDB fast path: one COM_STMT_BULK_EXECUTE round-trip binds every # row in binary form. Falls back to the batched text protocol when # the server/query/rows are not bulk-compatible. if ( conn._bulk_supported and conn._stmt_cache_size > 0 and type(query) is str and isinstance(args[0], (tuple, list)) ): stmt = await conn._acquire_cached_statement(query, len(args[0])) if stmt is not None: try: result = await stmt.execute_bulk(args) except ValueError: # not bulk-compatible (raised before any I/O): # use the batched text protocol below pass else: self._executed = query await self._do_get_result() return self.rowcount m = RE_INSERT_VALUES.match(query) if m: q_prefix: str = m.group(1) % () q_values = m.group(2).rstrip() q_postfix = m.group(3) or "" if q_values[0] != "(" and q_values[-1] == ")": raise errors.ProgrammingError("Query values error") return await self._do_execute_many( q_prefix, q_values, q_postfix, args, self.max_stmt_length, self._get_db()._encoding, ) rows = 0 for arg in args: await self.execute(query, arg) rows += self.rowcount self.rowcount = rows return self.rowcount async def _do_execute_many(self, prefix, values, postfix, args, max_stmt_length, encoding): conn = self._get_db() escape = self._escape_args # Pre-encode prefix and postfix once if isinstance(prefix, str): prefix = prefix.encode(encoding) if isinstance(postfix, str): postfix = postfix.encode(encoding) # Process and encode all values upfront # This avoids repeated isinstance() checks and encoding in the loop encoded_values = [] for arg in args: v = values % escape(arg, conn) if isinstance(v, str): v = v.encode(encoding, "surrogateescape") encoded_values.append(v) if not encoded_values: return 0 rows = 0 batch_parts = [prefix, encoded_values[0]] current_length = len(prefix) + len(encoded_values[0]) # Build batches using list accumulation + join (faster than bytearray +=) for v in encoded_values[1:]: # Check if adding this value would exceed max statement length if current_length + len(v) + len(postfix) + 1 > max_stmt_length: # Execute current batch sql = b''.join(batch_parts) + postfix rows += await self.execute(sql) # Start new batch batch_parts = [prefix, v] current_length = len(prefix) + len(v) else: # Add to current batch batch_parts.append(b",") batch_parts.append(v) current_length += 1 + len(v) # Execute final batch sql = b''.join(batch_parts) + postfix rows += await self.execute(sql) self.rowcount = rows return rows async def callproc(self, procname, args=()): """Execute stored procedure procname with args. :param procname: Name of procedure to execute on server. :type procname: str :param args: Sequence of parameters to use with procedure. :type args: tuple or list Returns the original args. Compatibility warning: PEP-249 specifies that any modified parameters must be returned. This is currently impossible as they are only available by storing them in a server variable and then retrieved by a query. Since stored procedures return zero or more result sets, there is no reliable way to get at OUT or INOUT parameters via callproc. The server variables are named @_procname_n, where procname is the parameter above and n is the position of the parameter (from zero). Once all result sets generated by the procedure have been fetched, you can issue a SELECT @_procname_0, ... query using .execute() to get any OUT or INOUT values. Compatibility warning: The act of calling a stored procedure itself creates an empty result set. This appears after any result sets generated by the procedure. This is non-standard behavior with respect to the DB-API. Be sure to use nextset() to advance through all result sets; otherwise you may get disconnected. """ conn = self._get_db() if self._echo: logger.info("CALL %s", procname) logger.info("%r", args) if self._query_callback is None: return await self._callproc(conn, procname, args) start = time.time() self._in_batch += 1 try: result = await self._callproc(conn, procname, args) finally: self._in_batch -= 1 self._query_callback(self, procname, round((time.time() - start) * 1000, 2)) return result async def _callproc(self, conn, procname, args): if args: fmt = f"@_{procname}_%d=%s" await self._query( "SET %s" % ",".join(fmt % (index, conn.escape(arg)) for index, arg in enumerate(args)) ) await self.nextset() q = "CALL %s(%s)" % ( procname, ",".join(["@_%s_%d" % (procname, i) for i in range(len(args))]), ) await self._query(q) self._executed = q return args async def fetchone(self): """Fetch the next row.""" self._check_executed() if self._rows is None or self.rownumber >= len(self._rows): return None result = self._rows[self.rownumber] self.rownumber += 1 return result async def fetchmany(self, size=None): """Fetch several rows.""" self._check_executed() if self._rows is None: return [] end = self.rownumber + (size or self.arraysize) result = self._rows[self.rownumber: end] self.rownumber = min(end, len(self._rows)) return result async def fetchall(self): """Fetch all the rows.""" self._check_executed() if self._rows is None: return [] if self.rownumber: result = self._rows[self.rownumber:] else: result = self._rows self.rownumber = len(self._rows) return result def scroll(self, value, mode="relative"): self._check_executed() if mode == "relative": r = self.rownumber + value elif mode == "absolute": r = value else: raise errors.ProgrammingError("unknown scroll mode %s" % mode) if not (0 <= r < len(self._rows)): raise IndexError("out of range") self.rownumber = r async def _query(self, q): conn = self._get_db() self._clear_result() await conn.query(q) await self._do_get_result() return self.rowcount cpdef _clear_result(self): self.rownumber = 0 self._result = None self.rowcount = 0 self.description = None self.lastrowid = 0 self._rows = None async def _do_get_result(self): conn = self._get_db() self._result = result = conn._result self.rowcount = result.affected_rows self.description = result.description self.lastrowid = result.insert_id self._rows = result.rows if result.warning_count > 0: await self._show_warnings(conn) async def _show_warnings(self, conn: "Connection"): if self._result and self._result.has_next: return ws = await conn.show_warnings() if ws is None: return for w in ws: msg = w[-1] logger.warning(msg) Warning = errors.Warning Error = errors.Error InterfaceError = errors.InterfaceError DatabaseError = errors.DatabaseError DataError = errors.DataError OperationalError = errors.OperationalError IntegrityError = errors.IntegrityError InternalError = errors.InternalError ProgrammingError = errors.ProgrammingError NotSupportedError = errors.NotSupportedError cdef list _rows_to_dicts(rows, list fields): """C-level construction of dict rows for the common dict_type=dict case.""" cdef: Py_ssize_t nf = len(fields) Py_ssize_t j list out = [] dict d for row in rows: d = {} for j in range(nf): d[fields[j]] = row[j] out.append(d) return out class DictCursorMixin: # You can override this to use OrderedDict or other dict-like types. dict_type = dict async def _do_get_result(self): await super(DictCursorMixin, self)._do_get_result() fields = [] if self.description: for f in self._result.fields: name = f.name if name in fields: name = f.table_name + "." + name fields.append(name) self._fields = fields if fields and self._rows: if self.dict_type is dict: self._rows = _rows_to_dicts(self._rows, fields) else: self._rows = [self._conv_row(r) for r in self._rows] def _conv_row(self, row): if row is None: return None return self.dict_type(zip(self._fields, row)) class DictCursor(DictCursorMixin, Cursor): """A cursor which returns results as a dictionary""" cdef class SSCursor(Cursor): """ Unbuffered Cursor, mainly useful for queries that return a lot of data, or for connections to remote servers over a slow network. Instead of copying every row of data into a buffer, this will fetch rows as needed. The upside of this is the client uses much less memory, and rows are returned much faster when traveling over a slow network or if the result set is very big. There are limitations, though. The MySQL protocol doesn't support returning the total number of rows, so the only way to tell how many rows there are is to iterate over every row returned. Also, it currently isn't possible to scroll backwards, as only the current row is held in memory. """ cdef _conv_row(self, row): return row async def close(self): conn = self.connection if conn is None: return if self._result is not None and self._result is conn._result: await self._result._finish_unbuffered_query() try: while await self.nextset(): pass finally: self.connection = None async def _query(self, q): conn = self._get_db() self._clear_result() await conn.query(q, unbuffered=True) await self._do_get_result() return self.rowcount async def nextset(self): return await self._nextset(unbuffered=True) async def read_next(self): """Read next row.""" return self._conv_row(await self._result._read_rowdata_packet_unbuffered()) async def fetchone(self): """Fetch next row.""" self._check_executed() row = await self.read_next() if row is None: return None self.rownumber += 1 return row async def fetchall(self): """ Fetch all, as per MySQLdb. Pretty useless for large queries, as it is buffered. See fetchall_unbuffered(), if you want an unbuffered generator version of this method. """ rows = [] while True: row = await self.fetchone() if row is None: break rows.append(row) return rows async def fetchmany(self, size=None): """Fetch many.""" self._check_executed() if size is None: size = self.arraysize rows = [] for i in range(size): row = await self.read_next() if row is None: break rows.append(row) self.rownumber += 1 return rows async def scroll(self, value, mode="relative"): self._check_executed() if mode == "relative": if value < 0: raise errors.NotSupportedError("Backwards scrolling not supported by this cursor") for _ in range(value): await self.read_next() self.rownumber += value elif mode == "absolute": if value < self.rownumber: raise errors.NotSupportedError("Backwards scrolling not supported by this cursor") end = value - self.rownumber for _ in range(end): await self.read_next() self.rownumber = value else: raise errors.ProgrammingError("unknown scroll mode %s" % mode) class SSDictCursor(DictCursorMixin, SSCursor): """An unbuffered cursor, which returns results as a dictionary""" python-asyncmy-0.2.15/asyncmy/errors.pyi000066400000000000000000000046201525423006000203220ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/errors.pyx from .constants.ER import * class MySQLError(Exception): """Exception related to operation with MySQL. ``sqlstate`` holds the 5-character SQLSTATE code the server sent with the error, or ``None`` when the error did not carry one (older servers, or errors raised client-side). ``args`` is unchanged: ``(errno, message)``. """ sqlstate: object class Warning(MySQLError): """Exception raised for important warnings like data truncations while inserting, etc.""" class Error(MySQLError): """Exception that is the base class of all other error exceptions (not Warning).""" class InterfaceError(Error): """Exception raised for errors that are related to the database interface rather than the database itself.""" class DatabaseError(Error): """Exception raised for errors that are related to the database.""" class DataError(DatabaseError): """Exception raised for errors that are due to problems with the processed data like division by zero, numeric value out of range, etc.""" class OperationalError(DatabaseError): """Exception raised for errors that are related to the database's operation and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs, the data source name is not found, a transaction could not be processed, a memory allocation error occurred during processing, etc.""" class IntegrityError(DatabaseError): """Exception raised when the relational integrity of the database is affected, e.g. a foreign key check fails, duplicate key, etc.""" class InternalError(DatabaseError): """Exception raised when the database encounters an internal error, e.g. the cursor is not valid anymore, the transaction is out of sync, etc.""" class ProgrammingError(DatabaseError): """Exception raised for programming errors, e.g. table not found or already exists, syntax error in the SQL statement, wrong number of parameters specified, etc.""" class NotSupportedError(DatabaseError): """Exception raised in case a method or database API was used which is not supported by the database, e.g. requesting a .rollback() on a connection that does not support transaction or has transactions turned off.""" def raise_mysql_exception(data: bytes): ... python-asyncmy-0.2.15/asyncmy/errors.pyx000066400000000000000000000114111525423006000203350ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. from .constants.ER import * from .structs import H cdef class MySQLError(Exception): """Exception related to operation with MySQL. ``sqlstate`` holds the 5-character SQLSTATE code the server sent with the error, or ``None`` when the error did not carry one (older servers, or errors raised client-side). ``args`` is unchanged: ``(errno, message)``. """ cdef public object sqlstate cdef class Warning(MySQLError): """Exception raised for important warnings like data truncations while inserting, etc.""" cdef class Error(MySQLError): """Exception that is the base class of all other error exceptions (not Warning).""" cdef class InterfaceError(Error): """Exception raised for errors that are related to the database interface rather than the database itself.""" cdef class DatabaseError(Error): """Exception raised for errors that are related to the database.""" cdef class DataError(DatabaseError): """Exception raised for errors that are due to problems with the processed data like division by zero, numeric value out of range, etc.""" cdef class OperationalError(DatabaseError): """Exception raised for errors that are related to the database's operation and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs, the data source name is not found, a transaction could not be processed, a memory allocation error occurred during processing, etc.""" cdef class IntegrityError(DatabaseError): """Exception raised when the relational integrity of the database is affected, e.g. a foreign key check fails, duplicate key, etc.""" cdef class InternalError(DatabaseError): """Exception raised when the database encounters an internal error, e.g. the cursor is not valid anymore, the transaction is out of sync, etc.""" cdef class ProgrammingError(DatabaseError): """Exception raised for programming errors, e.g. table not found or already exists, syntax error in the SQL statement, wrong number of parameters specified, etc.""" cdef class NotSupportedError(DatabaseError): """Exception raised in case a method or database API was used which is not supported by the database, e.g. requesting a .rollback() on a connection that does not support transaction or has transactions turned off.""" cdef dict error_map = {} cdef _map_error(exc, list errors): for error in errors: error_map[error] = exc _map_error( ProgrammingError, [ DB_CREATE_EXISTS, SYNTAX_ERROR, PARSE_ERROR, NO_SUCH_TABLE, WRONG_DB_NAME, WRONG_TABLE_NAME, FIELD_SPECIFIED_TWICE, INVALID_GROUP_FUNC_USE, UNSUPPORTED_EXTENSION, TABLE_MUST_HAVE_COLUMNS, CANT_DO_THIS_DURING_AN_TRANSACTION, WRONG_DB_NAME, WRONG_COLUMN_NAME, ] ) _map_error( DataError, [ WARN_DATA_TRUNCATED, WARN_NULL_TO_NOTNULL, WARN_DATA_OUT_OF_RANGE, NO_DEFAULT, PRIMARY_CANT_HAVE_NULL, DATA_TOO_LONG, DATETIME_FUNCTION_OVERFLOW, TRUNCATED_WRONG_VALUE_FOR_FIELD, ILLEGAL_VALUE_FOR_TYPE, ] ) _map_error( IntegrityError, [ DUP_ENTRY, NO_REFERENCED_ROW, NO_REFERENCED_ROW_2, ROW_IS_REFERENCED, ROW_IS_REFERENCED_2, CANNOT_ADD_FOREIGN, BAD_NULL_ERROR, ] ) _map_error( NotSupportedError, [ WARNING_NOT_COMPLETE_ROLLBACK, NOT_SUPPORTED_YET, FEATURE_DISABLED, UNKNOWN_STORAGE_ENGINE, ] ) _map_error( OperationalError, [ DBACCESS_DENIED_ERROR, ACCESS_DENIED_ERROR, CON_COUNT_ERROR, TABLEACCESS_DENIED_ERROR, COLUMNACCESS_DENIED_ERROR, CONSTRAINT_FAILED, LOCK_DEADLOCK, ] ) cpdef raise_mysql_exception(bytes data): cdef object sqlstate errno = H.unpack(data[1:3])[0] if data[3] == ord("#"): # SQL state marker: '#' followed by the 5-character SQLSTATE. sqlstate = data[4:9].decode("ascii", "replace") err_val = data[9:].decode("utf-8", "replace") else: sqlstate = None err_val = data[3:].decode("utf-8", "replace") error_class = error_map.get(errno) if error_class is None: error_class = InternalError if errno < 1000 else OperationalError exc = error_class(errno, err_val) exc.sqlstate = sqlstate raise exc python-asyncmy-0.2.15/asyncmy/optionfile.py000066400000000000000000000006601525423006000210050ustar00rootroot00000000000000from configparser import RawConfigParser class Parser(RawConfigParser): def __init__(self, **kwargs): kwargs["allow_no_value"] = True super().__init__(**kwargs) def get(self, section, option, **kwargs): value = super().get(section, option) quotes = ("'", '"') if len(value) >= 2 and value[0] == value[-1] and value[0] in quotes: return value[1:-1] return value python-asyncmy-0.2.15/asyncmy/pool.pyi000066400000000000000000000034721525423006000177630ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/pool.pyx from typing import Any from typing_extensions import Self from asyncmy.contexts import _PoolAcquireContextManager, _PoolContextManager from asyncmy.connection import Connection class Pool: """Connection pool, just from aiomysql""" def __init__( self, minsize: int, maxsize: int, pool_recycle: int = 3600, echo: bool = False, **kwargs ): ... @property def echo(self): ... @property def cond(self): ... @property def minsize(self): ... @property def maxsize(self): ... @property def size(self): ... @property def freesize(self): ... async def clear(self) -> None: """Close all free connections in pool.""" def close(self) -> None: """Close pool. Mark all pool connections to be closed on getting back to pool. Closed pool doesn't allow one to acquire new connections. """ def terminate(self) -> None: """Terminate pool. Close pool with instantly closing all acquired connections also. """ async def wait_closed(self) -> None: """Wait for closing all pool's connections.""" def acquire(self) -> _PoolAcquireContextManager[Connection]: """Acquire free connection from the pool.""" async def fill_free_pool(self, override_min: bool = False): ... def release(self, conn: Connection) -> Any: """ Release free connection back to the connection pool. This is **NOT** a coroutine. """ async def __aenter__(self) -> Self: ... async def __aexit__(self, exc_type, exc_val, exc_tb): ... def create_pool( minsize: int = 1, maxsize: int = 10, echo=False, pool_recycle: int = 3600, **kwargs ) -> _PoolContextManager[Pool]: ... python-asyncmy-0.2.15/asyncmy/pool.pyx000066400000000000000000000201071525423006000177740ustar00rootroot00000000000000# cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. import asyncio import collections from typing import Deque, Set from asyncmy.connection import Connection, connect from asyncmy.contexts import _PoolAcquireContextManager, _PoolContextManager class Pool(asyncio.AbstractServer): """Connection pool, just from aiomysql""" def __init__( self, minsize: int, maxsize: int, pool_recycle: int = 3600, echo: bool = False, **kwargs ): if minsize < 0: raise ValueError("minsize should be zero or greater") if maxsize < minsize: raise ValueError("maxsize should be not less than minsize") self._minsize = minsize self._loop = asyncio.get_event_loop() self._conn_kwargs = {**kwargs, "echo": echo} self._acquiring = 0 self._free: Deque[Connection] = collections.deque(maxlen=maxsize) self._cond = asyncio.Condition() self._used: Set[Connection] = set() self._terminated: Set[Connection] = set() self._closing = False self._closed = False self._echo = echo self._recycle = int(pool_recycle) @property def echo(self): return self._echo @property def cond(self): return self._cond @property def minsize(self): return self._minsize @property def maxsize(self): return self._free.maxlen @property def size(self): return self.freesize + len(self._used) + self._acquiring @property def freesize(self): return len(self._free) async def clear(self): """Close all free connections in pool.""" async with self._cond: while self._free: conn = self._free.popleft() await conn.ensure_closed() self._cond.notify() def close(self): """Close pool. Mark all pool connections to be closed on getting back to pool. Closed pool doesn't allow one to acquire new connections. """ if self._closed: return self._closing = True def terminate(self): """Terminate pool. Close pool with instantly closing all acquired connections also. """ self.close() for conn in list(self._used): conn.close() self._terminated.add(conn) self._used.clear() # size <= freesize now for every waiter parked in wait_closed(), but # nothing else will tell them: schedule a wake so all of them (not # just one, unlike release()'s _wakeup()) re-check the predicate. self._loop.create_task(self._wakeup_all()) async def wait_closed(self): """Wait for closing all pool's connections.""" if self._closed: return if not self._closing: raise RuntimeError(".wait_closed() should be called " "after .close()") async with self._cond: # Serialize drainers so none can report closure while another is # still closing a connection removed from _free. while self.size: while self._free: conn = self._free.popleft() try: await conn.ensure_closed() except asyncio.CancelledError: # The connection is no longer tracked by the pool. # Close it before propagating the shutdown deadline. conn.close() raise except Exception: # peer may already be gone; fall back to abrupt close conn.close() if self.size: await self._cond.wait() self._closed = True self._cond.notify_all() def acquire(self): """Acquire free connection from the pool.""" coro = self._acquire() return _PoolAcquireContextManager(coro, self) async def _acquire(self): if self._closing: raise RuntimeError("Cannot acquire connection after closing pool") async with self._cond: while True: await self.fill_free_pool(True) if self._free: conn = self._free.popleft() self._used.add(conn) return conn else: await self._cond.wait() async def fill_free_pool(self, override_min: bool = False): # iterate over free connections and remove timeouted ones free_size = len(self._free) n = 0 while n < free_size: conn = self._free[-1] if conn._stream_broken: self._free.pop() conn.close() elif self._recycle > -1 and self._loop.time() - conn.last_usage > self._recycle: self._free.pop() try: await conn.ensure_closed() except Exception: # peer may already be gone; fall back to abrupt close conn.close() else: self._free.rotate() n += 1 while self.size < self.minsize: self._acquiring += 1 try: conn = await connect(**self._conn_kwargs) # raise exception if pool is closing self._free.append(conn) finally: # decrementing _acquiring changes size, so waiters must be # notified even when connect() raised self._acquiring -= 1 self._cond.notify() if self._free: return if override_min and self.size < self.maxsize: self._acquiring += 1 try: conn = await connect(**self._conn_kwargs) # raise exception if pool is closing self._free.append(conn) finally: self._acquiring -= 1 self._cond.notify() async def _wakeup(self): async with self._cond: self._cond.notify() async def _wakeup_all(self): async with self._cond: self._cond.notify_all() def release(self, conn: Connection): """ Release free connection back to the connection pool. This is **NOT** a coroutine. """ fut = self._loop.create_future() fut.set_result(None) if conn in self._terminated: self._terminated.remove(conn) return fut self._used.remove(conn) if conn.connected: in_trans = conn.get_transaction_status() if in_trans: # connection with an open transaction is discarded, see #36 conn.close() elif self._closing: conn.close() else: self._free.append(conn) # removing from _used changed size, so wait_closed()/acquire() waiters # must be woken on every path return self._loop.create_task(self._wakeup()) async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): self.close() await self.wait_closed() def create_pool( minsize: int = 1, maxsize: int = 10, echo = False, pool_recycle: int = 3600, **kwargs ): coro = _create_pool( minsize = minsize, maxsize = maxsize, echo = echo, pool_recycle = pool_recycle, **kwargs ) return _PoolContextManager(coro) async def _create_pool( minsize: int = 1, maxsize: int = 10, echo = False, pool_recycle: int = 3600, **kwargs ): pool = Pool( minsize = minsize, maxsize = maxsize, echo = echo, pool_recycle = pool_recycle, **kwargs ) if minsize > 0: async with pool.cond: await pool.fill_free_pool(False) return pool python-asyncmy-0.2.15/asyncmy/protocol.pyi000066400000000000000000000144461525423006000206560ustar00rootroot00000000000000# This file was generated by stubgen-pyx v0.2.18 from /Users/long2ice/Developer/long2ice/asyncmy/asyncmy/protocol.pyx class MysqlPacket: """ Representation of a MySQL response packet. Provides an interface for reading/parsing the packet results. """ def __init__(self, data: bytes, encoding: str): ... def get_all_data(self) -> bytes: ... def read(self, size: int) -> bytes: """ Read the first 'size' bytes in packet and advance cursor past them. """ def read_all(self) -> bytes: """Read all remaining data in the packet. (Subsequent read() will return errors.) """ def advance(self, length: int): """ Advance the cursor in data buffer 'length' bytes. """ def rewind(self, position: int = 0): """ Set the position of the data buffer cursor to 'position'. """ def get_bytes(self, position: int, length: int = 1) -> bytes: """ Get 'length' bytes starting at 'position'. Position is start of payload (first four packet header bytes are not included) starting at index '0'. No error checking is done. If requesting outside end of buffer an empty string (or string shorter than 'length') may be returned! """ def read_uint8(self) -> int: ... def read_uint16(self) -> int: ... def read_uint24(self) -> int: ... def read_uint32(self) -> int: ... def read_uint64(self) -> int: ... def read_string(self) -> bytes: ... def read_length_encoded_integer(self): """ Read a 'Length Coded Binary' number from the data buffer. Length coded numbers can be anywhere from 1 to 9 bytes depending on the value of the first byte. """ def read_length_coded_string(self): """ Read a 'Length Coded String' from the data buffer. A 'Length Coded String' consists first of a length coded (unsigned, positive) integer represented in 1-9 bytes followed by that many bytes of binary data. (For example "cat" would be "3cat".) """ def read_row(self, converters: tuple) -> tuple: """Parse the whole packet as one text-protocol result row.""" def read_binary_row(self, colspecs: tuple) -> tuple: """Parse the whole packet as one binary-protocol result row.""" def read_struct(self, fmt: str) -> tuple: ... def is_ok_packet(self) -> int: ... def is_eof_packet(self) -> int: ... def is_auth_switch_request(self) -> int: ... def is_extra_auth_data(self) -> int: ... def is_resultset_packet(self) -> int: ... def is_load_local_packet(self) -> int: ... def is_error_packet(self) -> int: ... def check_error(self): ... def raise_for_error(self): ... class FieldDescriptorPacket(MysqlPacket): """ A MysqlPacket that represents a specific column's metadata in the result. Parsing is automatically done and the results are exported via public attributes on the class such as: db, table_name, name, length, type_code. """ table_name: str org_table: str name: str org_name: str charsetnr: int length: int type_code: int flags: int scale: int def __init__(self, data: bytes, encoding: str): ... def description(self): """Provides a 7-item tuple compatible with the Python PEP249 DB Spec.""" def __str__(self): ... class OKPacketWrapper: """ OK Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ server_status: int warning_count: int has_next: int message: bytes affected_rows: int insert_id: int def __init__(self, from_packet: MysqlPacket): ... def __getattr__(self, key): ... class EOFPacketWrapper: """ EOF Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ server_status: int warning_count: int has_next: int def __init__(self, from_packet: MysqlPacket): ... def __getattr__(self, key): ... class LoadLocalPacketWrapper: """ Load Local Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ filename: bytes def __init__(self, from_packet: MysqlPacket): ... def __getattr__(self, key): ... def parse_rows_from_buffer( buf: bytearray, pos: int, buf_len: int, converters: tuple, seq_id: int, rows: list, deprecate_eof: bool = False, ): """Parse as many complete row packets as available in ``buf[pos:buf_len]``. Stops (without consuming) at the first packet that is incomplete, has a wrong sequence id, is an ERROR/EOF packet, is empty, or spans multiple wire packets (16MB). Parsed rows are appended to ``rows``. ``buf_len`` is the number of valid bytes (the bytearray's capacity may be larger when it is used as a receive buffer). Returns ``(new_pos, new_seq_id)``. """ def parse_binary_rows_from_buffer( buf: bytearray, pos: int, buf_len: int, colspecs: tuple, seq_id: int, rows: list, deprecate_eof: bool = False, ): """Binary-protocol counterpart of parse_rows_from_buffer.""" def skip_packets_from_buffer(buf: bytearray, pos: int, buf_len: int, seq_id: int, count: int): """Skip up to ``count`` complete packets without materializing them. Stops early at an incomplete packet, a sequence mismatch, a jumbo packet or an error packet (which the caller reads via read_packet to raise). Returns ``(new_pos, new_seq_id, skipped)``. """ def pack_bulk_rows(rows, nparams: int, encoding: str): """Serialize rows for MariaDB COM_STMT_BULK_EXECUTE. Returns ``(types_bytes, rows_bytes)`` or None when the rows are not bulk-compatible (a column is NULL in every row, or mixes value types). Each value is prefixed with an indicator byte (0 = value, 1 = NULL). """ def pack_binary_params(args: tuple, encoding: str) -> bytes: """Serialize COM_STMT_EXECUTE parameters. Returns null_bitmap + new_params_bound_flag(1) + types + values. """ python-asyncmy-0.2.15/asyncmy/protocol.pyx000066400000000000000000001212601525423006000206660ustar00rootroot00000000000000# cython: boundscheck=False, wraparound=False, cdivision=True, initializedcheck=False # cython: freethreading_compatible=True # Declares the module safe to import without re-enabling the GIL on # free-threaded CPython. Module-level state (encoders/decoders, error_map, # charset tables, escape table) is built during import and read-only after; # per-connection state lives on the instances. It does NOT make a single # Connection/Cursor safe to share between threads. from cpython cimport datetime from cpython.bytearray cimport PyByteArray_AS_STRING, PyByteArray_GET_SIZE from cpython.bytes cimport (PyBytes_AS_STRING, PyBytes_FromStringAndSize, PyBytes_GET_SIZE) from cpython.ref cimport Py_INCREF from cpython.tuple cimport PyTuple_New, PyTuple_SET_ITEM from cpython.unicode cimport PyUnicode_DecodeASCII, PyUnicode_DecodeUTF8 from libc.string cimport memchr, memcpy from decimal import Decimal from .constants.FIELD_TYPE import VAR_STRING from .constants.SERVER_STATUS import SERVER_MORE_RESULTS_EXISTS include "charset.pxd" from . import errors, structs datetime.import_datetime() # Length-coded integer markers # NULL_COLUMN = 251, UNSIGNED_CHAR_COLUMN = 251, UNSIGNED_SHORT_COLUMN = 252, # UNSIGNED_INT24_COLUMN = 253, UNSIGNED_INT64_COLUMN = 254 cdef tuple _parse_row(const unsigned char *p, Py_ssize_t size, tuple converters): """Parse one text-protocol row directly from raw memory. ``converters`` holds one ``(code, encoding, converter)`` tuple per column: code 0 -> bytes, 1 -> utf8 decode, 2 -> ascii decode, 3 -> decode(encoding). """ cdef: Py_ssize_t n = len(converters) Py_ssize_t pos = 0 Py_ssize_t i, length unsigned int c int code tuple row = PyTuple_New(n) tuple conv object value, converter for i in range(n): if pos >= size: raise errors.InternalError("Truncated row packet") c = p[pos] pos += 1 if c == 251: # NULL Py_INCREF(None) PyTuple_SET_ITEM(row, i, None) continue if c < 251: length = c elif c == 252: length = (p[pos] | (p[pos + 1] << 8)) pos += 2 elif c == 253: length = (p[pos] | (p[pos + 1] << 8) | (p[pos + 2] << 16)) pos += 3 elif c == 254: length = ( p[pos] | ( p[pos + 1] << 8) | ( p[pos + 2] << 16) | ( p[pos + 3] << 24) | ( p[pos + 4] << 32) | ( p[pos + 5] << 40) | ( p[pos + 6] << 48) | ( p[pos + 7] << 56) ) pos += 8 else: raise errors.InternalError("Invalid length encoded integer in row") if pos + length > size: raise errors.InternalError("Truncated row packet") conv = converters[i] code = conv[0] if code == 0: value = PyBytes_FromStringAndSize( (p + pos), length) elif code == 1: value = PyUnicode_DecodeUTF8( (p + pos), length, NULL) elif code == 2: value = PyUnicode_DecodeASCII( (p + pos), length, NULL) else: value = PyBytes_FromStringAndSize( (p + pos), length).decode( conv[1]) pos += length converter = conv[2] if converter is not None: value = converter(value) Py_INCREF(value) PyTuple_SET_ITEM(row, i, value) return row def parse_rows_from_buffer(bytearray buf, Py_ssize_t pos, Py_ssize_t buf_len, tuple converters, unsigned int seq_id, list rows, bint deprecate_eof=False): """Parse as many complete row packets as available in ``buf[pos:buf_len]``. Stops (without consuming) at the first packet that is incomplete, has a wrong sequence id, is an ERROR/EOF packet, is empty, or spans multiple wire packets (16MB). Parsed rows are appended to ``rows``. ``buf_len`` is the number of valid bytes (the bytearray's capacity may be larger when it is used as a receive buffer). Returns ``(new_pos, new_seq_id)``. """ cdef: const unsigned char *base = PyByteArray_AS_STRING(buf) Py_ssize_t payload_len unsigned int first while buf_len - pos >= 4: payload_len = (base[pos] | (base[pos + 1] << 8) | (base[pos + 2] << 16)) if base[pos + 3] != seq_id: break # sequence mismatch: let read_packet() raise the proper error if payload_len == 0 or payload_len == 0xFFFFFF: break # empty or multi-packet payload: slow path if buf_len - pos - 4 < payload_len: break # incomplete packet first = base[pos + 4] if first == 0xFF: break # error packet if first == 0xFE and (deprecate_eof or payload_len < 9): break # EOF packet / DEPRECATE_EOF terminating OK packet rows.append(_parse_row(base + pos + 4, payload_len, converters)) pos += 4 + payload_len seq_id = (seq_id + 1) & 0xFF return pos, seq_id cdef tuple _parse_binary_row(const unsigned char *p, Py_ssize_t size, tuple colspecs): """Parse one binary-protocol row from raw memory. ``p`` points at the packet payload: [0x00 header][NULL bitmap][values...]. ``colspecs`` holds one ``(type_code, is_unsigned, code, encoding, converter)`` tuple per column, where code/encoding/converter follow the text-row rules and apply only to length-encoded (string-ish) values. Assumes a little-endian host (as does the rest of the wire parsing). """ cdef: Py_ssize_t n = len(colspecs) Py_ssize_t bitmap_len = (n + 9) >> 3 Py_ssize_t pos = 1 + bitmap_len const unsigned char *bitmap = p + 1 Py_ssize_t i, length unsigned int c, u32 unsigned long long u64 int type_code, code, L int year, month, day, hour, minute, second long usec, days, secs float fv double dv tuple row = PyTuple_New(n) tuple spec object value, converter if size < pos: raise errors.InternalError("Truncated binary row packet") for i in range(n): if bitmap[(i + 2) >> 3] & (1 << ((i + 2) & 7)): Py_INCREF(None) PyTuple_SET_ITEM(row, i, None) continue spec = colspecs[i] type_code = spec[0] if type_code == 3 or type_code == 9: # LONG, INT24 (both 4 bytes) if pos + 4 > size: raise errors.InternalError("Truncated binary row packet") u32 = p[pos] | (p[pos + 1] << 8) | (p[pos + 2] << 16) | ( p[pos + 3] << 24) value = u32 if spec[1] else u32 pos += 4 elif type_code == 8: # LONGLONG if pos + 8 > size: raise errors.InternalError("Truncated binary row packet") u64 = ( p[pos] | ( p[pos + 1] << 8) | ( p[pos + 2] << 16) | ( p[pos + 3] << 24) | ( p[pos + 4] << 32) | ( p[pos + 5] << 40) | ( p[pos + 6] << 48) | ( p[pos + 7] << 56) ) value = u64 if spec[1] else u64 pos += 8 elif type_code == 1: # TINY value = p[pos] if spec[1] else ( p[pos]) pos += 1 elif type_code == 2 or type_code == 13: # SHORT, YEAR c = p[pos] | (p[pos + 1] << 8) value = c if ( spec[1] or type_code == 13) else ( c) pos += 2 elif type_code == 5: # DOUBLE if pos + 8 > size: raise errors.InternalError("Truncated binary row packet") memcpy(&dv, p + pos, 8) value = dv pos += 8 elif type_code == 4: # FLOAT memcpy(&fv, p + pos, 4) value = fv pos += 4 elif type_code == 7 or type_code == 12: # TIMESTAMP, DATETIME L = p[pos] pos += 1 if L == 0: value = None else: if pos + L > size: raise errors.InternalError("Truncated binary row packet") year = p[pos] | (p[pos + 1] << 8) month = p[pos + 2] day = p[pos + 3] if L >= 7: hour = p[pos + 4] minute = p[pos + 5] second = p[pos + 6] else: hour = minute = second = 0 if L >= 11: usec = ( p[pos + 7] | (p[pos + 8] << 8) | (p[pos + 9] << 16) | ( p[pos + 10] << 24) ) else: usec = 0 if year >= 1 and 1 <= month <= 12 and 1 <= day <= 31: value = datetime.datetime_new(year, month, day, hour, minute, second, usec, None) else: value = None pos += L elif type_code == 10 or type_code == 14: # DATE, NEWDATE L = p[pos] pos += 1 if L == 0: value = None else: if pos + L > size: raise errors.InternalError("Truncated binary row packet") year = p[pos] | (p[pos + 1] << 8) month = p[pos + 2] day = p[pos + 3] if year >= 1 and 1 <= month <= 12 and 1 <= day <= 31: value = datetime.date_new(year, month, day) else: value = None pos += L elif type_code == 11: # TIME L = p[pos] pos += 1 if L == 0: value = datetime.timedelta_new(0, 0, 0) else: if pos + L > size: raise errors.InternalError("Truncated binary row packet") days = ( p[pos + 1] | (p[pos + 2] << 8) | (p[pos + 3] << 16) | ( p[pos + 4] << 24) ) secs = p[pos + 5] * 3600 + p[pos + 6] * 60 + p[pos + 7] if L >= 12: usec = ( p[pos + 8] | (p[pos + 9] << 8) | (p[pos + 10] << 16) | ( p[pos + 11] << 24) ) else: usec = 0 if p[pos]: # negative value = datetime.timedelta_new(-days, -secs, -usec) else: value = datetime.timedelta_new(days, secs, usec) pos += L else: # Length-encoded value: strings, blobs, decimals, JSON, BIT, ... if pos >= size: raise errors.InternalError("Truncated binary row packet") c = p[pos] pos += 1 if c < 251: length = c elif c == 252: length = (p[pos] | (p[pos + 1] << 8)) pos += 2 elif c == 253: length = (p[pos] | (p[pos + 1] << 8) | (p[pos + 2] << 16)) pos += 3 elif c == 254: length = ( p[pos] | ( p[pos + 1] << 8) | ( p[pos + 2] << 16) | ( p[pos + 3] << 24) | ( p[pos + 4] << 32) | ( p[pos + 5] << 40) | ( p[pos + 6] << 48) | ( p[pos + 7] << 56) ) pos += 8 else: raise errors.InternalError("Invalid length encoded integer in binary row") if pos + length > size: raise errors.InternalError("Truncated binary row packet") code = spec[2] if code == 0: value = PyBytes_FromStringAndSize( (p + pos), length) elif code == 1: value = PyUnicode_DecodeUTF8( (p + pos), length, NULL) elif code == 2: value = PyUnicode_DecodeASCII( (p + pos), length, NULL) else: value = PyBytes_FromStringAndSize( (p + pos), length).decode( spec[3]) pos += length converter = spec[4] if converter is not None: value = converter(value) Py_INCREF(value) PyTuple_SET_ITEM(row, i, value) return row def parse_binary_rows_from_buffer(bytearray buf, Py_ssize_t pos, Py_ssize_t buf_len, tuple colspecs, unsigned int seq_id, list rows, bint deprecate_eof=False): """Binary-protocol counterpart of parse_rows_from_buffer.""" cdef: const unsigned char *base = PyByteArray_AS_STRING(buf) Py_ssize_t payload_len unsigned int first while buf_len - pos >= 4: payload_len = (base[pos] | (base[pos + 1] << 8) | (base[pos + 2] << 16)) if base[pos + 3] != seq_id: break if payload_len == 0 or payload_len == 0xFFFFFF: break if buf_len - pos - 4 < payload_len: break first = base[pos + 4] if first == 0xFF: break # error packet if first == 0xFE and (deprecate_eof or payload_len < 9): break # EOF packet / DEPRECATE_EOF terminating OK packet rows.append(_parse_binary_row(base + pos + 4, payload_len, colspecs)) pos += 4 + payload_len seq_id = (seq_id + 1) & 0xFF return pos, seq_id def skip_packets_from_buffer(bytearray buf, Py_ssize_t pos, Py_ssize_t buf_len, unsigned int seq_id, Py_ssize_t count): """Skip up to ``count`` complete packets without materializing them. Stops early at an incomplete packet, a sequence mismatch, a jumbo packet or an error packet (which the caller reads via read_packet to raise). Returns ``(new_pos, new_seq_id, skipped)``. """ cdef: const unsigned char *base = PyByteArray_AS_STRING(buf) Py_ssize_t payload_len Py_ssize_t skipped = 0 while skipped < count and buf_len - pos >= 4: payload_len = (base[pos] | (base[pos + 1] << 8) | (base[pos + 2] << 16)) if base[pos + 3] != seq_id: break if payload_len == 0xFFFFFF: break if buf_len - pos - 4 < payload_len: break if payload_len and base[pos + 4] == 0xFF: break # error packet: let read_packet() raise properly pos += 4 + payload_len seq_id = (seq_id + 1) & 0xFF skipped += 1 return pos, seq_id, skipped cdef inline void _write_lenenc(bytearray out, Py_ssize_t n): if n < 251: out.append(n) elif n < (1 << 16): out.append(0xFC) out.append(n & 0xFF) out.append((n >> 8) & 0xFF) elif n < (1 << 24): out.append(0xFD) out.append(n & 0xFF) out.append((n >> 8) & 0xFF) out.append((n >> 16) & 0xFF) else: out.append(0xFE) out.append(n & 0xFF) out.append((n >> 8) & 0xFF) out.append((n >> 16) & 0xFF) out.append((n >> 24) & 0xFF) out.append((n >> 32) & 0xFF) out.append((n >> 40) & 0xFF) out.append((n >> 48) & 0xFF) out.append((n >> 56) & 0xFF) cdef inline void _write_u32(bytearray out, unsigned long v): out.append(v & 0xFF) out.append((v >> 8) & 0xFF) out.append((v >> 16) & 0xFF) out.append((v >> 24) & 0xFF) cdef inline void _write_u64(bytearray out, unsigned long long v): out.append(v & 0xFF) out.append((v >> 8) & 0xFF) out.append((v >> 16) & 0xFF) out.append((v >> 24) & 0xFF) out.append((v >> 32) & 0xFF) out.append((v >> 40) & 0xFF) out.append((v >> 48) & 0xFF) out.append((v >> 56) & 0xFF) cdef int _bulk_param_type(object v) except -1: """MYSQL_TYPE code used for a value in COM_STMT_BULK_EXECUTE.""" if isinstance(v, bool): return 1 # TINY if isinstance(v, int): return 8 # LONGLONG if isinstance(v, float): return 5 # DOUBLE if isinstance(v, str): return 253 # VAR_STRING if isinstance(v, (bytes, bytearray)): return 252 # BLOB if datetime.PyDateTime_Check(v): return 12 # DATETIME if datetime.PyDate_Check(v): return 10 # DATE if datetime.PyDelta_Check(v) or datetime.PyTime_Check(v): return 11 # TIME if isinstance(v, Decimal): return 246 # NEWDECIMAL return 253 # stringified fallback cdef _append_binary_value(bytearray values, object v, str encoding): """Append one non-NULL value in binary wire format (shared with execute).""" cdef: long long i64v unsigned long long u64v double dv long days, secs long usec unsigned char tmp[8] bytes encoded if isinstance(v, bool): values.append(1 if v else 0) elif isinstance(v, int): if -9223372036854775808 <= v <= 9223372036854775807: i64v = v _write_u64(values, i64v) elif 0 < v <= 18446744073709551615: u64v = v _write_u64(values, u64v) else: raise ValueError("int parameter out of 64-bit range for MySQL: %r" % (v,)) elif isinstance(v, float): dv = v memcpy(tmp, &dv, 8) values += tmp[:8] elif isinstance(v, str): encoded = ( v).encode(encoding) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded elif isinstance(v, (bytes, bytearray)): encoded = bytes(v) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded elif datetime.PyDateTime_Check(v): values.append(11) values.append(datetime.PyDateTime_GET_YEAR(v) & 0xFF) values.append((datetime.PyDateTime_GET_YEAR(v) >> 8) & 0xFF) values.append(datetime.PyDateTime_GET_MONTH(v)) values.append(datetime.PyDateTime_GET_DAY(v)) values.append(datetime.PyDateTime_DATE_GET_HOUR(v)) values.append(datetime.PyDateTime_DATE_GET_MINUTE(v)) values.append(datetime.PyDateTime_DATE_GET_SECOND(v)) _write_u32(values, datetime.PyDateTime_DATE_GET_MICROSECOND(v)) elif datetime.PyDate_Check(v): values.append(4) values.append(datetime.PyDateTime_GET_YEAR(v) & 0xFF) values.append((datetime.PyDateTime_GET_YEAR(v) >> 8) & 0xFF) values.append(datetime.PyDateTime_GET_MONTH(v)) values.append(datetime.PyDateTime_GET_DAY(v)) elif datetime.PyDelta_Check(v): days = datetime.PyDateTime_DELTA_GET_DAYS(v) secs = datetime.PyDateTime_DELTA_GET_SECONDS(v) usec = datetime.PyDateTime_DELTA_GET_MICROSECONDS(v) values.append(12) if days < 0: if secs or usec: days = -days - 1 secs = 86400 - secs if usec: secs -= 1 usec = 1000000 - usec else: days = -days values.append(1) else: values.append(0) _write_u32(values, days) values.append(secs // 3600) values.append((secs % 3600) // 60) values.append(secs % 60) _write_u32(values, usec) elif datetime.PyTime_Check(v): values.append(12) values.append(0) _write_u32(values, 0) values.append(datetime.PyDateTime_TIME_GET_HOUR(v)) values.append(datetime.PyDateTime_TIME_GET_MINUTE(v)) values.append(datetime.PyDateTime_TIME_GET_SECOND(v)) _write_u32(values, datetime.PyDateTime_TIME_GET_MICROSECOND(v)) elif isinstance(v, Decimal): encoded = format(v, "f").encode("ascii") _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded else: encoded = str(v).encode(encoding) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded def pack_bulk_rows(rows, Py_ssize_t nparams, str encoding): """Serialize rows for MariaDB COM_STMT_BULK_EXECUTE. Returns ``(types_bytes, rows_bytes)`` or None when the rows are not bulk-compatible (a column is NULL in every row, or mixes value types). Each value is prefixed with an indicator byte (0 = value, 1 = NULL). """ cdef: bytearray types = bytearray() bytearray values = bytearray() list col_types = [-1] * nparams Py_ssize_t i int t object row, v for row in rows: if len(row) != nparams: return None for i in range(nparams): v = row[i] if v is None: continue t = _bulk_param_type(v) if col_types[i] == -1: col_types[i] = t elif col_types[i] != t: return None # heterogeneous column: fall back for i in range(nparams): t = col_types[i] if t == -1: return None # all-NULL column: server needs a concrete type types.append(t) types.append(0) for row in rows: for i in range(nparams): v = row[i] if v is None: values.append(1) # STMT_INDICATOR_NULL else: values.append(0) # STMT_INDICATOR_NONE _append_binary_value(values, v, encoding) return bytes(types), bytes(values) cpdef bytes pack_binary_params(tuple args, str encoding): """Serialize COM_STMT_EXECUTE parameters. Returns null_bitmap + new_params_bound_flag(1) + types + values. """ cdef: Py_ssize_t n = len(args) bytearray bitmap = bytearray((n + 7) >> 3) bytearray types = bytearray() bytearray values = bytearray() Py_ssize_t i object v long long i64v unsigned long long u64v double dv long days, secs long usec unsigned char tmp[8] bytes encoded for i in range(n): v = args[i] if v is None: bitmap[i >> 3] |= 1 << (i & 7) types.append(6) # MYSQL_TYPE_NULL types.append(0) elif isinstance(v, bool): types.append(1) # MYSQL_TYPE_TINY types.append(0) values.append(1 if v else 0) elif isinstance(v, int): if -9223372036854775808 <= v <= 9223372036854775807: types.append(8) # MYSQL_TYPE_LONGLONG types.append(0) i64v = v _write_u64(values, i64v) elif 0 < v <= 18446744073709551615: types.append(8) types.append(0x80) # unsigned flag u64v = v _write_u64(values, u64v) else: raise ValueError("int parameter out of 64-bit range for MySQL: %r" % (v,)) elif isinstance(v, float): types.append(5) # MYSQL_TYPE_DOUBLE types.append(0) dv = v memcpy(tmp, &dv, 8) values += tmp[:8] elif isinstance(v, str): types.append(253) # MYSQL_TYPE_VAR_STRING types.append(0) encoded = ( v).encode(encoding) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded elif isinstance(v, (bytes, bytearray)): types.append(252) # MYSQL_TYPE_BLOB types.append(0) encoded = bytes(v) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded elif datetime.PyDateTime_Check(v): types.append(12) # MYSQL_TYPE_DATETIME types.append(0) values.append(11) values.append(datetime.PyDateTime_GET_YEAR(v) & 0xFF) values.append((datetime.PyDateTime_GET_YEAR(v) >> 8) & 0xFF) values.append(datetime.PyDateTime_GET_MONTH(v)) values.append(datetime.PyDateTime_GET_DAY(v)) values.append(datetime.PyDateTime_DATE_GET_HOUR(v)) values.append(datetime.PyDateTime_DATE_GET_MINUTE(v)) values.append(datetime.PyDateTime_DATE_GET_SECOND(v)) _write_u32(values, datetime.PyDateTime_DATE_GET_MICROSECOND(v)) elif datetime.PyDate_Check(v): types.append(10) # MYSQL_TYPE_DATE types.append(0) values.append(4) values.append(datetime.PyDateTime_GET_YEAR(v) & 0xFF) values.append((datetime.PyDateTime_GET_YEAR(v) >> 8) & 0xFF) values.append(datetime.PyDateTime_GET_MONTH(v)) values.append(datetime.PyDateTime_GET_DAY(v)) elif datetime.PyDelta_Check(v): types.append(11) # MYSQL_TYPE_TIME types.append(0) days = datetime.PyDateTime_DELTA_GET_DAYS(v) secs = datetime.PyDateTime_DELTA_GET_SECONDS(v) usec = datetime.PyDateTime_DELTA_GET_MICROSECONDS(v) values.append(12) if days < 0: # normalize to positive components with a sign byte if secs or usec: days = -days - 1 secs = 86400 - secs if usec: secs -= 1 usec = 1000000 - usec else: days = -days values.append(1) else: values.append(0) _write_u32(values, days) values.append(secs // 3600) values.append((secs % 3600) // 60) values.append(secs % 60) _write_u32(values, usec) elif datetime.PyTime_Check(v): types.append(11) # MYSQL_TYPE_TIME types.append(0) values.append(12) values.append(0) _write_u32(values, 0) values.append(datetime.PyDateTime_TIME_GET_HOUR(v)) values.append(datetime.PyDateTime_TIME_GET_MINUTE(v)) values.append(datetime.PyDateTime_TIME_GET_SECOND(v)) _write_u32(values, datetime.PyDateTime_TIME_GET_MICROSECOND(v)) elif isinstance(v, Decimal): types.append(246) # MYSQL_TYPE_NEWDECIMAL types.append(0) encoded = format(v, "f").encode("ascii") _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded else: # Fallback: stringify, mirroring the text protocol's default encoder types.append(253) types.append(0) encoded = str(v).encode(encoding) _write_lenenc(values, PyBytes_GET_SIZE(encoded)) values += encoded return bytes(bitmap) + b"\x01" + bytes(types) + bytes(values) cdef class MysqlPacket: """ Representation of a MySQL response packet. Provides an interface for reading/parsing the packet results. """ cdef: bytes _data const unsigned char *_ptr Py_ssize_t _size Py_ssize_t _position def __init__(self, bytes data, str encoding): self._position = 0 self._data = data self._ptr = PyBytes_AS_STRING(data) self._size = PyBytes_GET_SIZE(data) cpdef bytes get_all_data(self): return self._data cdef inline bytes _read_fast(self, Py_ssize_t size): """Fast internal read without bounds checking.""" cdef Py_ssize_t pos = self._position self._position = pos + size return PyBytes_FromStringAndSize( (self._ptr + pos), size) cpdef bytes read(self, Py_ssize_t size): """ Read the first 'size' bytes in packet and advance cursor past them. """ cdef Py_ssize_t pos = self._position if pos + size > self._size: error = ( "Result length not requested length:\n" "Expected=%s. Actual=%s. Position: %s. Data Length: %s" % (size, self._size - pos, pos, self._size) ) raise AssertionError(error) self._position = pos + size return PyBytes_FromStringAndSize( (self._ptr + pos), size) cpdef bytes read_all(self): """Read all remaining data in the packet. (Subsequent read() will return errors.) """ cdef bytes result = PyBytes_FromStringAndSize( (self._ptr + self._position), self._size - self._position ) self._position = 0 return result cpdef advance(self, Py_ssize_t length): """ Advance the cursor in data buffer 'length' bytes. """ cdef Py_ssize_t new_position = self._position + length if new_position < 0 or new_position > self._size: raise Exception( "Invalid advance amount (%s) for cursor. " "Position=%s" % (length, new_position) ) self._position = new_position cpdef rewind(self, Py_ssize_t position=0): """ Set the position of the data buffer cursor to 'position'. """ if position < 0 or position > self._size: raise Exception("Invalid position to rewind cursor to: %s." % position) self._position = position cpdef bytes get_bytes(self, Py_ssize_t position, Py_ssize_t length=1): """ Get 'length' bytes starting at 'position'. Position is start of payload (first four packet header bytes are not included) starting at index '0'. No error checking is done. If requesting outside end of buffer an empty string (or string shorter than 'length') may be returned! """ return self._data[position: (position + length)] cpdef unsigned int read_uint8(self): cdef unsigned int result = self._ptr[self._position] self._position += 1 return result cpdef unsigned int read_uint16(self): cdef const unsigned char *p = self._ptr + self._position self._position += 2 return p[0] | (p[1] << 8) cpdef unsigned int read_uint24(self): cdef const unsigned char *p = self._ptr + self._position self._position += 3 return p[0] | (p[1] << 8) | (p[2] << 16) cpdef unsigned int read_uint32(self): cdef const unsigned char *p = self._ptr + self._position self._position += 4 return p[0] | (p[1] << 8) | (p[2] << 16) | ( p[3] << 24) cpdef unsigned long long read_uint64(self): cdef const unsigned char *p = self._ptr + self._position self._position += 8 return ( p[0] | ( p[1] << 8) | ( p[2] << 16) | ( p[3] << 24) | ( p[4] << 32) | ( p[5] << 40) | ( p[6] << 48) | ( p[7] << 56) ) cpdef bytes read_string(self): cdef: const char *start = (self._ptr + self._position) Py_ssize_t remaining = self._size - self._position const char *end end = memchr(start, 0, remaining) if end == NULL: return None cdef bytes result = PyBytes_FromStringAndSize(start, end - start) self._position += (end - start) + 1 return result cpdef read_length_encoded_integer(self): """ Read a 'Length Coded Binary' number from the data buffer. Length coded numbers can be anywhere from 1 to 9 bytes depending on the value of the first byte. """ cdef unsigned int c = self._ptr[self._position] self._position += 1 if c == 251: # NULL_COLUMN return None if c < 251: return c elif c == 252: return self.read_uint16() elif c == 253: return self.read_uint24() elif c == 254: return self.read_uint64() cpdef read_length_coded_string(self): """ Read a 'Length Coded String' from the data buffer. A 'Length Coded String' consists first of a length coded (unsigned, positive) integer represented in 1-9 bytes followed by that many bytes of binary data. (For example "cat" would be "3cat".) """ cdef: unsigned int c Py_ssize_t length c = self._ptr[self._position] self._position += 1 if c == 251: # NULL return None if c < 251: length = c elif c == 252: length = self.read_uint16() elif c == 253: length = self.read_uint24() elif c == 254: length = self.read_uint64() else: return None return self._read_fast(length) cpdef tuple read_row(self, tuple converters): """Parse the whole packet as one text-protocol result row.""" cdef tuple row = _parse_row( self._ptr + self._position, self._size - self._position, converters ) self._position = self._size return row cpdef tuple read_binary_row(self, tuple colspecs): """Parse the whole packet as one binary-protocol result row.""" cdef tuple row = _parse_binary_row(self._ptr, self._size, colspecs) self._position = self._size return row cpdef tuple read_struct(self, str fmt): s = getattr(structs, fmt[1:]) result = s.unpack_from(self._data, self._position) self._position += s.size return tuple(result) cpdef int is_ok_packet(self): # https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html return self._size >= 7 and self._ptr[0] == 0 cpdef int is_eof_packet(self): # http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-EOF_Packet # Caution: \xFE may be LengthEncodedInteger. # If \xFE is LengthEncodedInteger header, 8bytes followed. return self._size < 9 and self._size > 0 and self._ptr[0] == 0xFE cpdef int is_auth_switch_request(self): # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest return self._size > 0 and self._ptr[0] == 0xFE cpdef int is_extra_auth_data(self): # https://dev.mysql.com/doc/internals/en/successful-authentication.html return self._size > 0 and self._ptr[0] == 1 cpdef int is_resultset_packet(self): return self._size > 0 and 1 <= self._ptr[0] <= 250 cpdef int is_load_local_packet(self): return self._size > 0 and self._ptr[0] == 0xFB cpdef int is_error_packet(self): return self._size > 0 and self._ptr[0] == 0xFF def check_error(self): if self.is_error_packet(): self.raise_for_error() cpdef raise_for_error(self): errors.raise_mysql_exception(self._data) cdef class FieldDescriptorPacket(MysqlPacket): """ A MysqlPacket that represents a specific column's metadata in the result. Parsing is automatically done and the results are exported via public attributes on the class such as: db, table_name, name, length, type_code. """ cdef: bytes catalog, db public str table_name, org_table, name, org_name public long long charsetnr, length, type_code, flags, scale def __init__(self, bytes data, str encoding): super(FieldDescriptorPacket, self).__init__(data, encoding) self._parse_field_descriptor(encoding) cdef _parse_field_descriptor(self, str encoding): """ Parse the 'Field Descriptor' (Metadata) packet. This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0). """ self.catalog = self.read_length_coded_string() self.db = self.read_length_coded_string() self.table_name = self.read_length_coded_string().decode(encoding) self.org_table = self.read_length_coded_string().decode(encoding) self.name = self.read_length_coded_string().decode(encoding) self.org_name = self.read_length_coded_string().decode(encoding) # layout: filler(1) charsetnr(2) length(4) type_code(1) flags(2) scale(1) filler(2) self._position += 1 self.charsetnr = self.read_uint16() self.length = self.read_uint32() self.type_code = self.read_uint8() self.flags = self.read_uint16() self.scale = self.read_uint8() self._position += 2 cpdef description(self): """Provides a 7-item tuple compatible with the Python PEP249 DB Spec.""" cdef int column_length = self.get_column_length() return ( self.name, self.type_code, None, # TODO: display_length; should this be self.length? column_length, # 'internal_size' column_length, # 'precision' # TODO: why!?!? self.scale, self.flags % 2 == 0, ) cdef int get_column_length(self): if self.type_code == VAR_STRING: mb_len = MB_LENGTH.get(self.charsetnr, 1) return self.length // mb_len return self.length def __str__(self): return "%s %r.%r.%r, type=%s, flags=%x" % ( self.__class__, self.db, self.table_name, self.name, self.type_code, self.flags, ) cdef class OKPacketWrapper: """ OK Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ cdef: MysqlPacket packet public int server_status, warning_count, has_next public bytes message public unsigned long long affected_rows, insert_id def __init__(self, MysqlPacket from_packet): if not from_packet.is_ok_packet(): raise ValueError( "Cannot create " + str(self.__class__.__name__) + " object from invalid packet type" ) self.packet = from_packet self.packet.advance(1) self.affected_rows = self.packet.read_length_encoded_integer() self.insert_id = self.packet.read_length_encoded_integer() self.server_status = self.packet.read_uint16() self.warning_count = self.packet.read_uint16() self.message = self.packet.read_all() self.has_next = self.server_status & SERVER_MORE_RESULTS_EXISTS def __getattr__(self, key): return getattr(self.packet, key) cdef class EOFPacketWrapper: """ EOF Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ cdef: MysqlPacket packet public int server_status, warning_count, has_next def __init__(self, MysqlPacket from_packet): if not from_packet.is_eof_packet(): raise ValueError( f"Cannot create '{self.__class__}' object from invalid packet type" ) self.packet = from_packet self.packet.advance(1) self.warning_count = self.packet.read_uint16() self.server_status = self.packet.read_uint16() self.has_next = self.server_status & SERVER_MORE_RESULTS_EXISTS def __getattr__(self, key): return getattr(self.packet, key) cdef class LoadLocalPacketWrapper: """ Load Local Packet Wrapper. It uses an existing packet object, and wraps around it, exposing useful variables while still providing access to the original packet objects variables and methods. """ cdef: MysqlPacket packet public bytes filename def __init__(self, MysqlPacket from_packet): if not from_packet.is_load_local_packet(): raise ValueError( f"Cannot create '{self.__class__}' object from invalid packet type" ) self.packet = from_packet self.filename = self.packet.get_all_data()[1:] def __getattr__(self, key): return getattr(self.packet, key) python-asyncmy-0.2.15/asyncmy/py.typed000066400000000000000000000000001525423006000177460ustar00rootroot00000000000000python-asyncmy-0.2.15/asyncmy/replication/000077500000000000000000000000001525423006000205725ustar00rootroot00000000000000python-asyncmy-0.2.15/asyncmy/replication/__init__.py000066400000000000000000000000641525423006000227030ustar00rootroot00000000000000from .binlogstream import BinLogStream # noqa:F401 python-asyncmy-0.2.15/asyncmy/replication/binlogstream.py000066400000000000000000000323061525423006000236360ustar00rootroot00000000000000from __future__ import annotations import struct from typing import Any from asyncmy import Connection from asyncmy.constants.COMMAND import COM_BINLOG_DUMP, COM_BINLOG_DUMP_GTID, COM_REGISTER_SLAVE from asyncmy.cursors import DictCursor from asyncmy.errors import OperationalError from asyncmy.replication.constants import ( BINLOG_DUMP_NON_BLOCK, BINLOG_THROUGH_GTID, MAX_HEARTBEAT, ROTATE_EVENT, TABLE_MAP_EVENT, ) from asyncmy.replication.errors import BinLogNotEnabledError from asyncmy.replication.events import ( BeginLoadQueryEvent, BinLogEvent, ExecuteLoadQueryEvent, FormatDescriptionEvent, GtidEvent, HeartbeatLogEvent, NotImplementedEvent, QueryEvent, RotateEvent, StopEvent, XidEvent, ) from asyncmy.replication.gtid import Gtid, GtidSet from asyncmy.replication.packets import BinLogPacket from asyncmy.replication.row_events import ( DeleteRowsEvent, TableMapEvent, UpdateRowsEvent, WriteRowsEvent, ) class ReportSlave: def __init__(self, value: str | tuple | dict): self._hostname = "" self._username = "" self._password = "" # nosec: B105 self._port = 0 if isinstance(value, (tuple, list)): try: self._hostname = value[0] self._username = value[1] self._password = value[2] self._port = int(value[3]) except IndexError: pass elif isinstance(value, dict): for key in ["hostname", "username", "password", "port"]: try: setattr(self, key, value[key]) except KeyError: pass else: self._hostname = value def encoded(self, server_id: int, master_id: int = 0): len_hostname = len(self._hostname.encode()) len_username = len(self._username.encode()) len_password = len(self._password.encode()) packet_len = ( 1 + 4 # command + 1 # server-id + len_hostname # hostname length + 1 + len_username # username length + 1 + len_password # password length + 2 + 4 # slave mysql port + 4 # replication rank ) # master-id max_string_len = 257 # one byte for length + 256 chars return ( struct.pack(" MAX_HEARTBEAT: heartbeat = MAX_HEARTBEAT heartbeat = int(heartbeat * 1000000000) await cursor.execute(f"set @master_heartbeat_period= {heartbeat}") await self._register_slave() if not self._master_auto_position: if self._master_log_file is None or self._master_log_position is None: await cursor.execute("SHOW MASTER STATUS") master_status = await cursor.fetchone() if master_status is None: raise BinLogNotEnabledError("MySQL binary logging is not enabled.") self._master_log_file, self._master_log_position = master_status[:2] prelude = struct.pack("> 8 if real_type == SET or real_type == ENUM: self.type = real_type self.size = metadata & 0x00FF self._read_enum_metadata(column_schema) else: self.max_length = (((metadata >> 4) & 0x300) ^ 0x300) + (metadata & 0x00FF) def _read_enum_metadata(self, column_schema): enums = column_schema["COLUMN_TYPE"] if self.type == ENUM: self.enum_values = [""] + enums.replace("enum(", "").replace(")", "").replace( "'", "" ).split(",") else: self.set_values = enums.replace("set(", "").replace(")", "").replace("'", "").split(",") @property def data(self): return dict((k, v) for (k, v) in self.__dict__.items() if not k.startswith("_")) python-asyncmy-0.2.15/asyncmy/replication/constants.py000066400000000000000000000032271525423006000231640ustar00rootroot00000000000000BINLOG_DUMP_NON_BLOCK = 0x01 BINLOG_THROUGH_GTID = 0x04 MAX_HEARTBEAT = 4294967 # field type TIMESTAMP2 = 17 DATETIME2 = 18 TIME2 = 19 # binlog UNKNOWN_EVENT = 0x00 START_EVENT_V3 = 0x01 QUERY_EVENT = 0x02 STOP_EVENT = 0x03 ROTATE_EVENT = 0x04 INTVAR_EVENT = 0x05 LOAD_EVENT = 0x06 SLAVE_EVENT = 0x07 CREATE_FILE_EVENT = 0x08 APPEND_BLOCK_EVENT = 0x09 EXEC_LOAD_EVENT = 0x0A DELETE_FILE_EVENT = 0x0B NEW_LOAD_EVENT = 0x0C RAND_EVENT = 0x0D USER_VAR_EVENT = 0x0E FORMAT_DESCRIPTION_EVENT = 0x0F XID_EVENT = 0x10 BEGIN_LOAD_QUERY_EVENT = 0x11 EXECUTE_LOAD_QUERY_EVENT = 0x12 TABLE_MAP_EVENT = 0x13 PRE_GA_WRITE_ROWS_EVENT = 0x14 PRE_GA_UPDATE_ROWS_EVENT = 0x15 PRE_GA_DELETE_ROWS_EVENT = 0x16 WRITE_ROWS_EVENT_V1 = 0x17 UPDATE_ROWS_EVENT_V1 = 0x18 DELETE_ROWS_EVENT_V1 = 0x19 INCIDENT_EVENT = 0x1A HEARTBEAT_LOG_EVENT = 0x1B IGNORABLE_LOG_EVENT = 0x1C ROWS_QUERY_LOG_EVENT = 0x1D WRITE_ROWS_EVENT_V2 = 0x1E UPDATE_ROWS_EVENT_V2 = 0x1F DELETE_ROWS_EVENT_V2 = 0x20 GTID_LOG_EVENT = 0x21 ANONYMOUS_GTID_LOG_EVENT = 0x22 PREVIOUS_GTIDS_LOG_EVENT = 0x23 # INTVAR types INTVAR_INVALID_INT_EVENT = 0x00 INTVAR_LAST_INSERT_ID_EVENT = 0x01 INTVAR_INSERT_ID_EVENT = 0x02 UNSIGNED_SHORT_LENGTH = 2 UNSIGNED_INT24_LENGTH = 3 UNSIGNED_INT64_LENGTH = 8 # json type JSONB_TYPE_SMALL_OBJECT = 0x0 JSONB_TYPE_LARGE_OBJECT = 0x1 JSONB_TYPE_SMALL_ARRAY = 0x2 JSONB_TYPE_LARGE_ARRAY = 0x3 JSONB_TYPE_LITERAL = 0x4 JSONB_TYPE_INT16 = 0x5 JSONB_TYPE_UINT16 = 0x6 JSONB_TYPE_INT32 = 0x7 JSONB_TYPE_UINT32 = 0x8 JSONB_TYPE_INT64 = 0x9 JSONB_TYPE_UINT64 = 0xA JSONB_TYPE_DOUBLE = 0xB JSONB_TYPE_STRING = 0xC JSONB_TYPE_OPAQUE = 0xF JSONB_LITERAL_NULL = 0x0 JSONB_LITERAL_TRUE = 0x1 JSONB_LITERAL_FALSE = 0x2 python-asyncmy-0.2.15/asyncmy/replication/errors.py000066400000000000000000000003111525423006000224530ustar00rootroot00000000000000class TableMetadataUnavailableError(Exception): """ raise when table metadata is unavailable """ class BinLogNotEnabledError(Exception): """ raise when binlog not enabled """ python-asyncmy-0.2.15/asyncmy/replication/events.py000066400000000000000000000143061525423006000224540ustar00rootroot00000000000000import binascii import struct from asyncmy.replication.utils import byte2int, int2byte class BinLogEvent: def __init__( self, from_packet, event_size, table_map, connection, only_tables=None, ignored_tables=None, only_schemas=None, ignored_schemas=None, freeze_schema=None, fail_on_table_metadata_unavailable=False, ): self.packet = from_packet self.table_map = table_map self.event_type = self.packet.event_type self.timestamp = self.packet.timestamp self.event_size = event_size self._connection = connection self._fail_on_table_metadata_unavailable = fail_on_table_metadata_unavailable # The event have been fully processed, if processed is false # the event will be skipped self._processed = True self.complete = True @property def processed(self): return self._processed def _read_table_id(self): # Table ID is 6 byte # pad little-endian number table_id = self.packet.read(6) + int2byte(0) + int2byte(0) return struct.unpack(" i2[0] @staticmethod def contains(i1, i2): return i2[0] >= i1[0] and i2[1] <= i1[1] @staticmethod def parse_interval(interval): """ We parse a human-generated string here. So our end value b is incremented to conform to the internal representation format. """ m = re.search("^([0-9]+)(?:-([0-9]+))?$", interval) if not m: raise ValueError("GTID format is incorrect: %r" % (interval,)) a = int(m.group(1)) b = int(m.group(2) or a) return a, b + 1 @staticmethod def parse(gtid: str): m = re.search( "^([0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})((?::[0-9-]+)+)$", gtid, ) if not m: raise ValueError("GTID format is incorrect: %r" % (gtid,)) sid = m.group(1) intervals = m.group(2) intervals_parsed = [Gtid.parse_interval(x) for x in intervals.split(":")[1:]] return sid, intervals_parsed def _add_interval(self, itvl): """ Use the internal representation format and add it to our intervals, merging if required. """ new = [] if itvl[0] > itvl[1]: raise Exception("Malformed interval %s" % (itvl,)) if any(self.overlap(x, itvl) for x in self.intervals): raise Exception("Overlapping interval %s" % (itvl,)) # Merge: arrange interval to fit existing set for existing in sorted(self.intervals): if itvl[0] == existing[1]: itvl = (existing[0], itvl[1]) continue if itvl[1] == existing[0]: itvl = (itvl[0], existing[1]) continue new.append(existing) self.intervals = sorted(new + [itvl]) def __sub_interval(self, itvl): """Using the internal representation, remove an interval""" new = [] if itvl[0] > itvl[1]: raise Exception("Malformed interval %s" % (itvl,)) if not any(self.overlap(x, itvl) for x in self.intervals): # No raise return # Merge: arrange existing set around interval for existing in sorted(self.intervals): if self.overlap(existing, itvl): if existing[0] < itvl[0]: new.append((existing[0], itvl[0])) if existing[1] > itvl[1]: new.append((itvl[1], existing[1])) else: new.append(existing) self.intervals = new def __contains__(self, other): if other.sid != self.sid: return False return all( any(self.contains(me, them) for me in self.intervals) for them in other.intervals ) def __init__(self, gtid: str, sid=None, intervals=None): if intervals is None: intervals = [] if sid: intervals = intervals else: sid, intervals = Gtid.parse(gtid) self.sid = sid self.intervals = [] for itvl in intervals: self._add_interval(itvl) def __add__(self, other): """Include the transactions of this gtid. Raise if the attempted merge has different SID""" if self.sid != other.sid: raise Exception("Attempt to merge different SID%s != %s" % (self.sid, other.sid)) result = Gtid(str(self)) for itvl in other.intervals: result._add_interval(itvl) return result def __sub__(self, other): """Remove intervals. Do not raise, if different SID simply ignore""" result = Gtid(str(self)) if self.sid != other.sid: return result for itvl in other.intervals: result.__sub_interval(itvl) return result def __str__(self): """We represent the human value here - a single number for one transaction, or a closed interval (decrementing b)""" return "%s:%s" % ( self.sid, ":".join( ("%d-%d" % (x[0], x[1] - 1)) if x[0] + 1 != x[1] else str(x[0]) for x in self.intervals ), ) def __repr__(self): return '' % self @property def encoded_length(self): return ( 16 + 8 # sid + 2 # n_intervals * 8 # stop/start * len(self.intervals) # stop/start mark encoded as int64 ) def encode(self): buffer = b"" # sid buffer += binascii.unhexlify(self.sid.replace("-", "")) # n_intervals buffer += struct.pack(" other.sid return self.intervals > other.intervals def __ge__(self, other): if other.sid != self.sid: return self.sid >= other.sid return self.intervals >= other.intervals class GtidSet: def __init__(self, gtid_set: set[Gtid]): self._gtid_set = gtid_set def merge_gtid(self, gtid: Gtid): new_gtid_set = set() for existing in self._gtid_set: if existing.sid == gtid.sid: new_gtid_set.add(existing + gtid) else: new_gtid_set.add(existing) if gtid.sid not in (x.sid for x in new_gtid_set): new_gtid_set.add(gtid) self._gtid_set = new_gtid_set def __contains__(self, other: Gtid | GtidSet): if isinstance(other, GtidSet): return all(other_gtid in self._gtid_set for other_gtid in other._gtid_set) if isinstance(other, Gtid): return any(other in x for x in self._gtid_set) raise NotImplementedError def __add__(self, other: Gtid | GtidSet): if isinstance(other, Gtid): new = GtidSet(self._gtid_set) new.merge_gtid(other) return new if isinstance(other, GtidSet): new = GtidSet(self._gtid_set) for gtid in other._gtid_set: new.merge_gtid(gtid) return new raise NotImplementedError def __str__(self): return ",".join(str(x) for x in self._gtid_set) def __repr__(self): return "" % self._gtid_set @property def encoded_length(self): return 8 + sum(x.encoded_length for x in self._gtid_set) # n_sids def encoded(self): return b"" + ( struct.pack(" 0: data = self._data_buffer[:size] self._data_buffer = self._data_buffer[size:] if len(data) == size: return data else: return data + self._packet.read(size - len(data)) return self._packet.read(size) def unread(self, data): self.read_bytes -= len(data) self._data_buffer += data def advance(self, size): size = int(size) self.read_bytes += size buffer_len = len(self._data_buffer) if buffer_len > 0: self._data_buffer = self._data_buffer[size:] if size > buffer_len: self._packet.advance(size - buffer_len) else: self._packet.advance(size) def read_length_coded_binary(self): c = byte2int(self.read(1)) if c == NULL_COLUMN: return None if c < UNSIGNED_CHAR_COLUMN: return c elif c == UNSIGNED_SHORT_COLUMN: return self.unpack_uint16(self.read(UNSIGNED_SHORT_LENGTH)) elif c == UNSIGNED_INT24_COLUMN: return self.unpack_int24(self.read(UNSIGNED_INT24_LENGTH)) elif c == UNSIGNED_INT64_COLUMN: return self.unpack_int64(self.read(UNSIGNED_INT64_LENGTH)) def read_length_coded_string(self): """Read a 'Length Coded String' from the data buffer. A 'Length Coded String' consists first of a length coded (unsigned, positive) integer represented in 1-9 bytes followed by that many bytes of binary data. (For example "cat" would be "3cat".) From PyMYSQL source code """ length = self.read_length_coded_binary() if length is None: return None return self.read(length).decode() def __getattr__(self, key): if hasattr(self._packet, key): return getattr(self._packet, key) raise AttributeError("%s instance has no attribute '%s'" % (self.__class__, key)) def read_int_be_by_size(self, size): """Read a big endian integer values based on byte number""" if size == 1: return struct.unpack(">b", self.read(size))[0] elif size == 2: return struct.unpack(">h", self.read(size))[0] elif size == 3: return self.read_int24_be() elif size == 4: return struct.unpack(">i", self.read(size))[0] elif size == 5: return self.read_int40_be() elif size == 8: return struct.unpack(">l", self.read(size))[0] def read_uint_by_size(self, size): """Read a little endian integer values based on byte number""" if size == 1: return self.read_uint8() elif size == 2: return self.read_uint16() elif size == 3: return self.read_uint24() elif size == 4: return self.read_uint32() elif size == 5: return self.read_uint40() elif size == 6: return self.read_uint48() elif size == 7: return self.read_uint56() elif size == 8: return self.read_uint64() def read_length_coded_pascal_string(self, size): """Read a string with length coded using pascal style. The string start by the size of the string """ length = self.read_uint_by_size(size) return self.read(length) def read_variable_length_string(self): """Read a variable length string where the first 1-5 bytes stores the length of the string. For each byte, the first bit being high indicates another byte must be read. """ byte = 0x80 length = 0 bits_read = 0 while byte & 0x80 != 0: byte = struct.unpack("!B", self.read(1))[0] length = length | ((byte & 0x7F) << bits_read) bits_read = bits_read + 7 return self.read(length) def read_int24(self): a, b, c = struct.unpack("BBB", self.read(3)) res = a | (b << 8) | (c << 16) if res >= 0x800000: res -= 0x1000000 return res def read_int24_be(self): a, b, c = struct.unpack("BBB", self.read(3)) res = (a << 16) | (b << 8) | c if res >= 0x800000: res -= 0x1000000 return res def read_uint8(self): return struct.unpack("IB", self.read(5)) return b + (a << 8) def read_uint48(self): a, b, c = struct.unpack(" length: raise ValueError("Json length is larger than packet length") if large: key_offset_lengths = [ ( self.read_uint32(), # offset (we don't actually need that) self.read_uint16(), # size of the key ) for _ in range(elements) ] else: key_offset_lengths = [ ( self.read_uint16(), # offset (we don't actually need that) self.read_uint16(), # size of key ) for _ in range(elements) ] value_type_inlined_lengths = [ self.read_offset_or_inline(self, large) for _ in range(elements) ] keys = [self.read(x[1]) for x in key_offset_lengths] out = {} for i in range(elements): if value_type_inlined_lengths[i][1] is None: data = value_type_inlined_lengths[i][2] else: t = value_type_inlined_lengths[i][0] data = self.read_binary_json_type(t, length) out[keys[i]] = data return out def read_binary_json_array(self, length, large): if large: elements = self.read_uint32() size = self.read_uint32() else: elements = self.read_uint16() size = self.read_uint16() if size > length: raise ValueError("Json length is larger than packet length") values_type_offset_inline = [ self.read_offset_or_inline(self, large) for _ in range(elements) ] def _read(x): if x[1] is None: return x[2] return self.read_binary_json_type(x[0], length) return [_read(x) for x in values_type_offset_inline] @staticmethod def read_offset_or_inline(packet, large): t = packet.read_uint8() if t in (JSONB_TYPE_LITERAL, JSONB_TYPE_INT16, JSONB_TYPE_UINT16): return (t, None, packet.read_binary_json_type_inlined(t, large)) if large and t in (JSONB_TYPE_INT32, JSONB_TYPE_UINT32): return (t, None, packet.read_binary_json_type_inlined(t, large)) if large: return (t, packet.read_uint32(), None) return (t, packet.read_uint16(), None) python-asyncmy-0.2.15/asyncmy/replication/row_events.py000066400000000000000000000541461525423006000233510ustar00rootroot00000000000000import datetime import decimal import struct from asyncmy.charset import charset_by_name from asyncmy.constants.FIELD_TYPE import ( BIT, BLOB, DATE, DATETIME, DOUBLE, ENUM, FLOAT, GEOMETRY, INT24, JSON, LONG, LONGLONG, NEWDECIMAL, SET, SHORT, STRING, TIME, TIMESTAMP, TINY, VARCHAR, YEAR, ) from .bitmap import bit_count, bit_get from .column import Column from .constants import ( DATETIME2, DELETE_ROWS_EVENT_V2, TIME2, TIMESTAMP2, UPDATE_ROWS_EVENT_V2, WRITE_ROWS_EVENT_V2, ) from .errors import TableMetadataUnavailableError from .events import BinLogEvent from .table import Table from .utils import byte2int class RowsEvent(BinLogEvent): def __init__(self, from_packet, event_size, table_map, ctl_connection, **kwargs): super().__init__(from_packet, event_size, table_map, ctl_connection, **kwargs) self._rows = None self._only_tables = kwargs["only_tables"] self._ignored_tables = kwargs["ignored_tables"] self._only_schemas = kwargs["only_schemas"] self._ignored_schemas = kwargs["ignored_schemas"] # Header self.table_id = self._read_table_id() # Additional information try: self.primary_key = table_map[self.table_id].data["primary_key"] self.schema = self.table_map[self.table_id].schema self.table = self.table_map[self.table_id].table except KeyError: # If we have filter the corresponding TableMap Event self._processed = False return schema_table = f"{self.schema}.{self.table}" if self._only_tables is not None and schema_table not in self._only_tables: self._processed = False return elif self._ignored_tables is not None and schema_table in self._ignored_tables: self._processed = False return if self._only_schemas is not None and self.schema not in self._only_schemas: self._processed = False return elif self._ignored_schemas is not None and self.schema in self._ignored_schemas: self._processed = False return # Event V2 if ( self.event_type == WRITE_ROWS_EVENT_V2 or self.event_type == DELETE_ROWS_EVENT_V2 or self.event_type == UPDATE_ROWS_EVENT_V2 ): self.flags, self.extra_data_length = struct.unpack(" 255: values[name] = self.__read_string(2, column) else: values[name] = self.__read_string(1, column) elif column.type == NEWDECIMAL: values[name] = self.__read_new_decimal(column) elif column.type == BLOB: values[name] = self.__read_string(column.length_size, column) elif column.type == DATETIME: values[name] = self.__read_datetime() elif column.type == TIME: values[name] = self.__read_time() elif column.type == DATE: values[name] = self.__read_date() elif column.type == TIMESTAMP: values[name] = datetime.datetime.fromtimestamp(self.packet.read_uint32()) # For new date format: elif column.type == DATETIME2: values[name] = self.__read_datetime2(column) elif column.type == TIME2: values[name] = self.__read_time2(column) elif column.type == TIMESTAMP2: values[name] = self.__add_fsp_to_time( datetime.datetime.fromtimestamp(self.packet.read_int_be_by_size(4)), column, ) elif column.type == LONGLONG: if unsigned: values[name] = self.packet.read_uint64() else: values[name] = self.packet.read_int64() elif column.type == YEAR: values[name] = self.packet.read_uint8() + 1900 elif column.type == ENUM: values[name] = column.enum_values[self.packet.read_uint_by_size(column.size)] elif column.type == SET: # We read set columns as a bitmap telling us which options # are enabled bit_mask = self.packet.read_uint_by_size(column.size) values[name] = ( set(val for idx, val in enumerate(column.set_values) if bit_mask & 2**idx) or None ) elif column.type == BIT: values[name] = self.__read_bit(column) elif column.type == GEOMETRY: values[name] = self.packet.read_length_coded_pascal_string(column.length_size) elif column.type == JSON: values[name] = self.packet.read_binary_json(column.length_size) else: raise NotImplementedError("Unknown MySQL column type: %d" % (column.type)) null_bitmap_index += 1 return values def __add_fsp_to_time(self, time, column): """Read and add the fractional part of time For more details about new date format: http://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html """ microsecond = self.__read_fsp(column) if microsecond > 0: time = time.replace(microsecond=microsecond) return time def __read_fsp(self, column): read = 0 if column.fsp == 1 or column.fsp == 2: read = 1 elif column.fsp == 3 or column.fsp == 4: read = 2 elif column.fsp == 5 or column.fsp == 6: read = 3 if read > 0: microsecond = self.packet.read_int_be_by_size(read) if column.fsp % 2: microsecond = int(microsecond / 10) return microsecond * (10 ** (6 - column.fsp)) return 0 @staticmethod def charset_to_encoding(name): charset = charset_by_name(name) return charset.encoding if charset else name def __read_string(self, size, column): string = self.packet.read_length_coded_pascal_string(size) if column.character_set_name is not None: encoding = self.charset_to_encoding(column.character_set_name) string = string.decode(encoding) return string def __read_bit(self, column): """Read MySQL BIT type""" resp = "" for byte in range(0, column.bytes): current_byte = "" data = self.packet.read_uint8() if byte == 0: if column.bytes == 1: end = column.bits else: end = column.bits % 8 if end == 0: end = 8 else: end = 8 for bit in range(0, end): if data & (1 << bit): current_byte += "1" else: current_byte += "0" resp += current_byte[::-1] return resp def __read_time(self): time = self.packet.read_uint24() date = datetime.timedelta( hours=int(time / 10000), minutes=int((time % 10000) / 100), seconds=int(time % 100), ) return date def __read_time2(self, column): """TIME encoding for nonfractional part: 1 bit sign (1= non-negative, 0= negative) 1 bit unused (reserved for future extensions) 10 bits hour (0-838) 6 bits minute (0-59) 6 bits second (0-59) --------------------- 24 bits = 3 bytes """ data = self.packet.read_int_be_by_size(3) sign = 1 if self._read_binary_slice(data, 0, 1, 24) else -1 if sign == -1: # negative integers are stored as 2's compliment # hence take 2's compliment again to get the right value. data = ~data + 1 t = datetime.timedelta( hours=sign * self._read_binary_slice(data, 2, 10, 24), minutes=self._read_binary_slice(data, 12, 6, 24), seconds=self._read_binary_slice(data, 18, 6, 24), microseconds=self.__read_fsp(column), ) return t def __read_date(self): time = self.packet.read_uint24() if time == 0: # nasty mysql 0000-00-00 dates return None year = (time & ((1 << 15) - 1) << 9) >> 9 month = (time & ((1 << 4) - 1) << 5) >> 5 day = time & ((1 << 5) - 1) if year == 0 or month == 0 or day == 0: return None date = datetime.date(year=year, month=month, day=day) return date def __read_datetime(self): value = self.packet.read_uint64() if value == 0: # nasty mysql 0000-00-00 dates return None date = value / 1000000 time = int(value % 1000000) year = int(date / 10000) month = int((date % 10000) / 100) day = int(date % 100) if year == 0 or month == 0 or day == 0: return None date = datetime.datetime( year=year, month=month, day=day, hour=int(time / 10000), minute=int((time % 10000) / 100), second=int(time % 100), ) return date def __read_datetime2(self, column): """DATETIME 1 bit sign (1= non-negative, 0= negative) 17 bits year*13+month (year 0-9999, month 0-12) 5 bits day (0-31) 5 bits hour (0-23) 6 bits minute (0-59) 6 bits second (0-59) --------------------------- 40 bits = 5 bytes """ data = self.packet.read_int_be_by_size(5) year_month = self._read_binary_slice(data, 1, 17, 40) try: t = datetime.datetime( year=int(year_month / 13), month=year_month % 13, day=self._read_binary_slice(data, 18, 5, 40), hour=self._read_binary_slice(data, 23, 5, 40), minute=self._read_binary_slice(data, 28, 6, 40), second=self._read_binary_slice(data, 34, 6, 40), ) except ValueError: self.__read_fsp(column) return None return self.__add_fsp_to_time(t, column) def __read_new_decimal(self, column): """Read MySQL's new decimal format introduced in MySQL 5""" # This project was a great source of inspiration for # understanding this storage format. # https://github.com/jeremycole/mysql_binlog digits_per_integer = 9 compressed_bytes = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4] integral = column.precision - column.decimals uncomp_integral = int(integral / digits_per_integer) uncomp_fractional = int(column.decimals / digits_per_integer) comp_integral = integral - (uncomp_integral * digits_per_integer) comp_fractional = column.decimals - (uncomp_fractional * digits_per_integer) # Support negative # The sign is encoded in the high bit of the the byte # But this bit can also be used in the value value = self.packet.read_uint8() if value & 0x80 != 0: res = "" mask = 0 else: mask = -1 res = "-" self.packet.unread(struct.pack(" 0: value = self.packet.read_int_be_by_size(size) ^ mask res += str(value) for i in range(0, uncomp_integral): value = struct.unpack(">i", self.packet.read(4))[0] ^ mask res += "%09d" % value res += "." for i in range(0, uncomp_fractional): value = struct.unpack(">i", self.packet.read(4))[0] ^ mask res += "%09d" % value size = compressed_bytes[comp_fractional] if size > 0: value = self.packet.read_int_be_by_size(size) ^ mask res += "%0*d" % (comp_fractional, value) return decimal.Decimal(res) @staticmethod def _read_binary_slice(binary, start, size, data_length): """ Read a part of binary data and extract a number binary: the data start: From which bit (1 to X) size: How many bits should be read data_length: data size """ binary = binary >> data_length - (start + size) mask = (1 << size) - 1 return binary & mask def _fetch_rows(self): self._rows = [] if not self.complete: return while self.packet.read_bytes < self.event_size: self._rows.append(self._fetch_one_row()) @property def rows(self): if self._rows is None: self._fetch_rows() return self._rows class DeleteRowsEvent(RowsEvent): """This event is trigger when a row in the database is removed For each row you have a hash with a single key: values which contain the data of the removed line. """ def __init__(self, from_packet, event_size, table_map, ctl_connection, **kwargs): super().__init__(from_packet, event_size, table_map, ctl_connection, **kwargs) if self._processed: self.columns_present_bitmap = self.packet.read((self.number_of_columns + 7) / 8) def _fetch_one_row(self): return {"values": self._read_column_data(self.columns_present_bitmap)} class WriteRowsEvent(RowsEvent): """This event is triggered when a row in database is added For each row you have a hash with a single key: values which contain the data of the new line. """ def __init__(self, from_packet, event_size, table_map, ctl_connection, **kwargs): super().__init__(from_packet, event_size, table_map, ctl_connection, **kwargs) if self._processed: self.columns_present_bitmap = self.packet.read((self.number_of_columns + 7) / 8) def _fetch_one_row(self): return {"values": self._read_column_data(self.columns_present_bitmap)} class UpdateRowsEvent(RowsEvent): """This event is triggered when a row in the database is changed For each row you got a hash with two keys: * before_values * after_values Depending of your MySQL configuration the hash can contains the full row or only the changes: http://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#sysvar_binlog_row_image """ def __init__(self, from_packet, event_size, table_map, ctl_connection, **kwargs): super().__init__(from_packet, event_size, table_map, ctl_connection, **kwargs) if self._processed: # Body self.columns_present_bitmap = self.packet.read((self.number_of_columns + 7) / 8) self.columns_present_bitmap2 = self.packet.read((self.number_of_columns + 7) / 8) def _fetch_one_row(self): row = { "before_values": self._read_column_data(self.columns_present_bitmap), "after_values": self._read_column_data(self.columns_present_bitmap2), } return row class TableMapEvent(BinLogEvent): """This event describes the structure of a table. It's sent before a change happens on a table. An end user of the lib should have no usage of this """ def __init__(self, from_packet, event_size, table_map, connection, **kwargs): super().__init__(from_packet, event_size, table_map, connection, **kwargs) self._only_tables = kwargs["only_tables"] self._ignored_tables = kwargs["ignored_tables"] self._only_schemas = kwargs["only_schemas"] self._ignored_schemas = kwargs["ignored_schemas"] self._freeze_schema = kwargs["freeze_schema"] self._table_map = table_map # Post-Header self.table_id = self._read_table_id() if self.table_id in table_map and self._freeze_schema: self._processed = False return self.flags = struct.unpack("= 200000") start = time.time() await cur.executemany( """INSERT INTO benchmark_data (user_id, username, email, created_at, updated_at, score, is_active, data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", data ) elapsed = time.time() - start await conn.ensure_closed() return elapsed async def test_aiomysql(data): """Batch insert with aiomysql""" conn = await aiomysql.connect(**connection_kwargs) async with conn.cursor() as cur: # Clean up before test await cur.execute("DELETE FROM benchmark_data WHERE user_id >= 200000") start = time.time() await cur.executemany( """INSERT INTO benchmark_data (user_id, username, email, created_at, updated_at, score, is_active, data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", data ) elapsed = time.time() - start conn.close() return elapsed def test_mysqlclient(data): """Batch insert with mysqlclient""" # Filter out None values for mysqlclient kwargs = {k: v for k, v in connection_kwargs.items() if v is not None} conn = MySQLdb.connect(**kwargs) cur = conn.cursor() # Clean up before test cur.execute("DELETE FROM benchmark_data WHERE user_id >= 200000") start = time.time() cur.executemany( """INSERT INTO benchmark_data (user_id, username, email, created_at, updated_at, score, is_active, data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", data ) elapsed = time.time() - start cur.close() conn.close() return elapsed def test_pymysql(data): """Batch insert with pymysql""" conn = pymysql.connect(**connection_kwargs) cur = conn.cursor() # Clean up before test cur.execute("DELETE FROM benchmark_data WHERE user_id >= 200000") start = time.time() cur.executemany( """INSERT INTO benchmark_data (user_id, username, email, created_at, updated_at, score, is_active, data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", data ) elapsed = time.time() - start cur.close() conn.close() return elapsed def run_benchmark(): """Run the batch insert benchmark""" print("\n" + "="*60) print(f"Benchmark 4: Batch Insert ({BATCH_SIZE} rows)") print("="*60) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Generate test data once data = generate_test_data() results = {} print("\nTesting mysqlclient...") elapsed = best_result(lambda: test_mysqlclient(data)) results['mysqlclient'] = elapsed print(f" Time: {elapsed:.3f}s") print(f" Throughput: {BATCH_SIZE/elapsed:.0f} rows/sec") print("\nTesting pymysql...") elapsed = best_result(lambda: test_pymysql(data)) results['pymysql'] = elapsed print(f" Time: {elapsed:.3f}s") print(f" Throughput: {BATCH_SIZE/elapsed:.0f} rows/sec") print("\nTesting asyncmy...") elapsed = best_result(lambda: loop.run_until_complete(test_asyncmy(data))) results['asyncmy'] = elapsed print(f" Time: {elapsed:.3f}s") print(f" Throughput: {BATCH_SIZE/elapsed:.0f} rows/sec") print("\nTesting aiomysql...") elapsed = best_result(lambda: loop.run_until_complete(test_aiomysql(data))) results['aiomysql'] = elapsed print(f" Time: {elapsed:.3f}s") print(f" Throughput: {BATCH_SIZE/elapsed:.0f} rows/sec") # Print rankings print("\n" + "-"*60) print("Rankings:") sorted_results = sorted(results.items(), key=lambda x: x[1]) for i, (name, time_val) in enumerate(sorted_results, 1): speedup = sorted_results[0][1] / time_val print(f" {i}. {name:15s} {time_val:6.3f}s ({speedup:.2f}x vs best)") return sorted_results if __name__ == "__main__": run_benchmark() python-asyncmy-0.2.15/benchmark/concurrent.py000066400000000000000000000063401525423006000212670ustar00rootroot00000000000000""" Benchmark 2: Concurrent Queries Tests the efficiency of running multiple queries concurrently. This is where async libraries shine - they can interleave I/O operations. Synchronous libraries are not included as they cannot execute truly concurrent queries without threads/processes. """ import asyncio import time import aiomysql import asyncmy from benchmark import best_result, connection_kwargs, CONCURRENT_COUNT async def test_asyncmy(num_queries=CONCURRENT_COUNT): """Run concurrent queries with asyncmy (each query gets its own connection)""" async def single_query(query_id): conn = await asyncmy.connect(**connection_kwargs) try: async with conn.cursor() as cur: user_id = query_id % 10000 await cur.execute( "SELECT * FROM benchmark_data WHERE user_id = %s LIMIT 10", (user_id,) ) return await cur.fetchall() finally: await conn.ensure_closed() start = time.time() tasks = [single_query(i) for i in range(num_queries)] results = await asyncio.gather(*tasks) elapsed = time.time() - start return elapsed, len(results) async def test_aiomysql(num_queries=CONCURRENT_COUNT): """Run concurrent queries with aiomysql (each query gets its own connection)""" async def single_query(query_id): conn = await aiomysql.connect(**connection_kwargs) try: async with conn.cursor() as cur: user_id = query_id % 10000 await cur.execute( "SELECT * FROM benchmark_data WHERE user_id = %s LIMIT 10", (user_id,) ) return await cur.fetchall() finally: conn.close() start = time.time() tasks = [single_query(i) for i in range(num_queries)] results = await asyncio.gather(*tasks) elapsed = time.time() - start return elapsed, len(results) def run_benchmark(): """Run the concurrent queries benchmark""" print("\n" + "="*60) print(f"Benchmark 2: Concurrent Queries ({CONCURRENT_COUNT} queries)") print("="*60) print("Note: Only async libraries tested (sync libs can't do this efficiently)") loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) results = {} print("\nTesting asyncmy...") elapsed, count = best_result(lambda: loop.run_until_complete(test_asyncmy())) results['asyncmy'] = elapsed print(f" Time: {elapsed:.3f}s, Queries: {count}") print(f" Throughput: {count/elapsed:.0f} queries/sec") print("\nTesting aiomysql...") elapsed, count = best_result(lambda: loop.run_until_complete(test_aiomysql())) results['aiomysql'] = elapsed print(f" Time: {elapsed:.3f}s, Queries: {count}") print(f" Throughput: {count/elapsed:.0f} queries/sec") # Print rankings print("\n" + "-"*60) print("Rankings:") sorted_results = sorted(results.items(), key=lambda x: x[1]) for i, (name, time_val) in enumerate(sorted_results, 1): speedup = sorted_results[0][1] / time_val print(f" {i}. {name:15s} {time_val:6.3f}s ({speedup:.2f}x vs best)") return sorted_results if __name__ == "__main__": run_benchmark() python-asyncmy-0.2.15/benchmark/crosslang/000077500000000000000000000000001525423006000205235ustar00rootroot00000000000000python-asyncmy-0.2.15/benchmark/crosslang/README.md000066400000000000000000000044121525423006000220030ustar00rootroot00000000000000# Cross-language benchmark: asyncmy vs native Go/Rust drivers Compares asyncmy against the mainstream native MySQL drivers of Go and Rust on identical data, identical queries, with full type materialization in every language (ints, floats, strings, and real datetime/date/duration objects — no scanning into raw strings). ## Results Apple M4 Max, MySQL 9.7.1 on localhost, warmup + best-of-N: | Scenario | Go (go-sql-driver v1.10) | asyncmy text | asyncmy binary (`conn.prepare`) | Rust (mysql_async 0.36) | | --- | --- | --- | --- | --- | | 50k-row mixed-type full scan | 0.032s | 0.037s | **0.023s** | 0.048s | | Pooled 20×200 point queries | 0.076s | 0.093s (0.067s with uvloop) | 0.084s (**0.062s** with uvloop) | **0.048s** | With `stmt_cache_size` set, plain `cursor.execute("%s")` code gets the binary path transparently: 0.025s scans and 0.064s pooled point queries (uvloop) with zero code changes. Takeaways: - With the binary protocol (server-side prepared statements, added in 0.2.13) asyncmy's large scan is the **fastest of the three languages** — integers arrive as little-endian bytes and datetimes as packed fields, so text parsing disappears entirely. - Even the text-protocol scan sits between the native drivers: ~12% behind go-sql-driver and ~25% ahead of mysql_async. - On point-query throughput Rust still leads; the remaining asyncmy gap is event-loop scheduling cost (uvloop closes much of it), not protocol parsing. ## Fairness notes - All three scan the same `test.bench` table (10 columns: int/bigint/double/ decimal/varchar×2/datetime(6)/date/time) created by `python_bench.py`. - DECIMAL and TIME are scanned as `String` in Go/Rust; Python constructs `Decimal`/`timedelta` objects (slightly more work on the Python side). - Go goes through `database/sql`; the raw driver would be marginally faster. - Rust uses `mysql_async` (the mainstream async driver, same category as asyncmy); `sqlx` and the sync `mysql` crate were not measured. - Point queries: Go/Rust use prepared statements (binary protocol); asyncmy uses client-side interpolation (text protocol). ## Reproduce ```bash # 1. create data + run the Python side (needs a local MySQL, root/123456) python python_bench.py # 2. Go cd go && go run . # 3. Rust cd rust && cargo run --release ``` python-asyncmy-0.2.15/benchmark/crosslang/go/000077500000000000000000000000001525423006000211305ustar00rootroot00000000000000python-asyncmy-0.2.15/benchmark/crosslang/go/go.mod000066400000000000000000000002071525423006000222350ustar00rootroot00000000000000module gobench go 1.26.5 require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect ) python-asyncmy-0.2.15/benchmark/crosslang/go/main.go000066400000000000000000000031021525423006000223770ustar00rootroot00000000000000package main import ( "database/sql" "fmt" "sync" "time" _ "github.com/go-sql-driver/mysql" ) func main() { dsn := "root:123456@tcp(127.0.0.1:3306)/test?parseTime=true" db, err := sql.Open("mysql", dsn) if err != nil { panic(err) } db.SetMaxOpenConns(10) defer db.Close() // --- large result set scan, best of 5 --- best := time.Duration(1 << 62) for iter := 0; iter < 6; iter++ { // first is warmup start := time.Now() rows, err := db.Query("SELECT * FROM test.bench") if err != nil { panic(err) } n := 0 var ( id, a int64 b int64 c float64 d string s1, s2 string dt, da time.Time tm string ) for rows.Next() { if err := rows.Scan(&id, &a, &b, &c, &d, &s1, &s2, &dt, &da, &tm); err != nil { panic(err) } n++ } rows.Close() el := time.Since(start) if iter > 0 && el < best { best = el } if n != 50000 { panic(fmt.Sprintf("rows=%d", n)) } } fmt.Printf("go_select_all_mixed_50k\t%.4f\n", best.Seconds()) // --- pooled concurrency: 20 goroutines x 200 point queries --- best = time.Duration(1 << 62) for iter := 0; iter < 4; iter++ { start := time.Now() var wg sync.WaitGroup for w := 0; w < 20; w++ { wg.Add(1) go func() { defer wg.Done() var a int64 for q := 0; q < 200; q++ { if err := db.QueryRow("SELECT a FROM test.bench WHERE id=?", 1).Scan(&a); err != nil { panic(err) } } }() } wg.Wait() el := time.Since(start) if iter > 0 && el < best { best = el } } fmt.Printf("go_pool_20x200\t%.4f\n", best.Seconds()) } python-asyncmy-0.2.15/benchmark/crosslang/python_bench.py000066400000000000000000000043651525423006000235650ustar00rootroot00000000000000import asyncio, datetime, time import asyncmy KW = dict(host="127.0.0.1", port=3306, user="root", password="123456") async def main(): conn = await asyncmy.connect(**KW) async with conn.cursor() as cur: await cur.execute("CREATE DATABASE IF NOT EXISTS test") await cur.execute("DROP TABLE IF EXISTS test.bench") await cur.execute( "CREATE TABLE test.bench (id INT PRIMARY KEY AUTO_INCREMENT," " a INT, b BIGINT, c DOUBLE, d DECIMAL(12,4), s1 VARCHAR(64)," " s2 VARCHAR(255), dt DATETIME(6), da DATE, tm TIME)") rows = [(i, i * 1000000007, i * 0.31415, "%d.%04d" % (i, i % 10000), "user-name-%08d" % i, "payload-" + "z" * (i % 120), datetime.datetime(2021, 3, 4, 5, 6, i % 60, i % 1000000), datetime.date(2000 + i % 25, 1 + i % 12, 1 + i % 28), datetime.timedelta(hours=i % 120, minutes=i % 60, seconds=i % 60)) for i in range(50000)] await cur.executemany( "INSERT INTO test.bench (a,b,c,d,s1,s2,dt,da,tm) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", rows) await conn.commit() # large scan, warmup + best of 5 async with conn.cursor() as cur: times = [] for i in range(6): t0 = time.perf_counter() await cur.execute("SELECT * FROM test.bench") got = await cur.fetchall() el = time.perf_counter() - t0 if i: times.append(el) assert len(got) == 50000 print("py_select_all_mixed_50k\t%.4f" % min(times)) # pool 20x200 point queries pool = await asyncmy.create_pool(minsize=10, maxsize=10, **KW) async def worker(): for _ in range(200): async with pool.acquire() as c: async with c.cursor() as cu: await cu.execute("SELECT a FROM test.bench WHERE id=%s", (1,)) await cu.fetchall() times = [] for i in range(4): t0 = time.perf_counter() await asyncio.gather(*[worker() for _ in range(20)]) el = time.perf_counter() - t0 if i: times.append(el) print("py_pool_20x200\t%.4f" % min(times)) pool.close(); await pool.wait_closed() await conn.ensure_closed() asyncio.run(main()) python-asyncmy-0.2.15/benchmark/crosslang/rust/000077500000000000000000000000001525423006000215205ustar00rootroot00000000000000python-asyncmy-0.2.15/benchmark/crosslang/rust/Cargo.toml000066400000000000000000000003721525423006000234520ustar00rootroot00000000000000[package] name = "rustbench" version = "0.1.0" edition = "2021" [dependencies] mysql_async = { version = "0.36", features = ["time"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } time = "0.3" [profile.release] opt-level = 3 python-asyncmy-0.2.15/benchmark/crosslang/rust/src/000077500000000000000000000000001525423006000223075ustar00rootroot00000000000000python-asyncmy-0.2.15/benchmark/crosslang/rust/src/main.rs000066400000000000000000000033241525423006000236030ustar00rootroot00000000000000use mysql_async::prelude::*; use std::time::Instant; #[tokio::main] async fn main() -> Result<(), Box> { let url = "mysql://root:123456@127.0.0.1:3306/test?pool_min=10&pool_max=10"; let pool = mysql_async::Pool::new(url); // --- large result set scan, warmup + best of 5 --- let mut conn = pool.get_conn().await?; let mut best = f64::MAX; for iter in 0..6 { let start = Instant::now(); let rows: Vec<( i64, i64, i64, f64, String, String, String, time::PrimitiveDateTime, time::Date, time::Duration, )> = conn.query("SELECT * FROM test.bench").await?; let el = start.elapsed().as_secs_f64(); assert_eq!(rows.len(), 50000); if iter > 0 && el < best { best = el; } } println!("rust_select_all_mixed_50k\t{:.4}", best); drop(conn); // --- pool concurrency: 20 tasks x 200 point queries, acquire per query --- let mut best2 = f64::MAX; for iter in 0..4 { let start = Instant::now(); let mut handles = vec![]; for _ in 0..20 { let pool = pool.clone(); handles.push(tokio::spawn(async move { for _ in 0..200 { let mut c = pool.get_conn().await.unwrap(); let _v: Option = c .exec_first("SELECT a FROM test.bench WHERE id=?", (1,)) .await .unwrap(); } })); } for h in handles { h.await?; } let el = start.elapsed().as_secs_f64(); if iter > 0 && el < best2 { best2 = el; } } println!("rust_pool_20x200\t{:.4}", best2); pool.disconnect().await?; Ok(()) } python-asyncmy-0.2.15/benchmark/large_resultset.py000066400000000000000000000103641525423006000223120ustar00rootroot00000000000000""" Benchmark 1: Large Result Set Fetching Tests the efficiency of fetching and processing large result sets. This measures: - Packet reading efficiency (optimized with buffer management) - Data parsing speed (optimized with datetime conversion) - Memory efficiency """ import asyncio import time import aiomysql import asyncmy import MySQLdb import pymysql from benchmark import best_result, connection_kwargs async def test_asyncmy(limit=50000): """Fetch large result set with asyncmy""" conn = await asyncmy.connect(**connection_kwargs) async with conn.cursor() as cur: start = time.time() await cur.execute( "SELECT * FROM benchmark_data WHERE is_active = 1 LIMIT %s", (limit,) ) rows = await cur.fetchall() elapsed = time.time() - start await conn.ensure_closed() return elapsed, len(rows) async def test_asyncmy_binary(limit=50000): """Fetch large result set with asyncmy via the binary protocol""" conn = await asyncmy.connect(stmt_cache_size=16, **connection_kwargs) async with conn.cursor() as cur: start = time.time() await cur.execute( "SELECT * FROM benchmark_data WHERE is_active = 1 LIMIT %s", (limit,) ) rows = await cur.fetchall() elapsed = time.time() - start await conn.ensure_closed() return elapsed, len(rows) async def test_aiomysql(limit=50000): """Fetch large result set with aiomysql""" conn = await aiomysql.connect(**connection_kwargs) async with conn.cursor() as cur: start = time.time() await cur.execute( "SELECT * FROM benchmark_data WHERE is_active = 1 LIMIT %s", (limit,) ) rows = await cur.fetchall() elapsed = time.time() - start conn.close() return elapsed, len(rows) def test_mysqlclient(limit=50000): """Fetch large result set with mysqlclient""" conn = MySQLdb.connect(**connection_kwargs) cur = conn.cursor() start = time.time() cur.execute( "SELECT * FROM benchmark_data WHERE is_active = 1 LIMIT %s", (limit,) ) rows = cur.fetchall() elapsed = time.time() - start cur.close() conn.close() return elapsed, len(rows) def test_pymysql(limit=50000): """Fetch large result set with pymysql""" conn = pymysql.connect(**connection_kwargs) cur = conn.cursor() start = time.time() cur.execute( "SELECT * FROM benchmark_data WHERE is_active = 1 LIMIT %s", (limit,) ) rows = cur.fetchall() elapsed = time.time() - start cur.close() conn.close() return elapsed, len(rows) def run_benchmark(): """Run the large result set benchmark""" print("\n" + "="*60) print("Benchmark 1: Large Result Set (fetch 50k rows)") print("="*60) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) results = {} print("\nTesting mysqlclient...") elapsed, count = best_result(test_mysqlclient) results['mysqlclient'] = elapsed print(f" Time: {elapsed:.3f}s, Rows: {count}") print("\nTesting pymysql...") elapsed, count = best_result(test_pymysql) results['pymysql'] = elapsed print(f" Time: {elapsed:.3f}s, Rows: {count}") print("\nTesting asyncmy...") elapsed, count = best_result(lambda: loop.run_until_complete(test_asyncmy())) results['asyncmy'] = elapsed print(f" Time: {elapsed:.3f}s, Rows: {count}") print("\nTesting asyncmy (binary protocol)...") elapsed, count = best_result(lambda: loop.run_until_complete(test_asyncmy_binary())) results['asyncmy-binary'] = elapsed print(f" Time: {elapsed:.3f}s, Rows: {count}") print("\nTesting aiomysql...") elapsed, count = best_result(lambda: loop.run_until_complete(test_aiomysql())) results['aiomysql'] = elapsed print(f" Time: {elapsed:.3f}s, Rows: {count}") # Print rankings print("\n" + "-"*60) print("Rankings:") sorted_results = sorted(results.items(), key=lambda x: x[1]) for i, (name, time_val) in enumerate(sorted_results, 1): speedup = sorted_results[0][1] / time_val print(f" {i}. {name:15s} {time_val:6.3f}s ({speedup:.2f}x vs best)") return sorted_results if __name__ == "__main__": run_benchmark() python-asyncmy-0.2.15/benchmark/pool.py000066400000000000000000000064371525423006000200650ustar00rootroot00000000000000""" Benchmark 3: Connection Pool Performance Tests connection pool management under concurrent load. This measures: - Connection acquisition/release efficiency - Connection reuse - Concurrent access handling """ import asyncio import time import aiomysql from asyncmy.pool import create_pool from benchmark import best_result, connection_kwargs POOL_SIZE_MIN = 5 POOL_SIZE_MAX = 20 NUM_QUERIES = 2000 async def test_asyncmy_pool(): """Test connection pool with asyncmy""" pool = await create_pool( minsize=POOL_SIZE_MIN, maxsize=POOL_SIZE_MAX, **connection_kwargs ) async def query_with_pool(query_id): async with pool.acquire() as conn: async with conn.cursor() as cur: user_id = query_id % 10000 await cur.execute( "SELECT * FROM benchmark_data WHERE user_id = %s LIMIT 5", (user_id,) ) return await cur.fetchall() start = time.time() tasks = [query_with_pool(i) for i in range(NUM_QUERIES)] results = await asyncio.gather(*tasks) elapsed = time.time() - start pool.close() await pool.wait_closed() return elapsed, len(results) async def test_aiomysql_pool(): """Test connection pool with aiomysql""" pool = await aiomysql.create_pool( minsize=POOL_SIZE_MIN, maxsize=POOL_SIZE_MAX, **connection_kwargs ) async def query_with_pool(query_id): async with pool.acquire() as conn: async with conn.cursor() as cur: user_id = query_id % 10000 await cur.execute( "SELECT * FROM benchmark_data WHERE user_id = %s LIMIT 5", (user_id,) ) return await cur.fetchall() start = time.time() tasks = [query_with_pool(i) for i in range(NUM_QUERIES)] results = await asyncio.gather(*tasks) elapsed = time.time() - start pool.close() await pool.wait_closed() return elapsed, len(results) def run_benchmark(): """Run the connection pool benchmark""" print("\n" + "="*60) print(f"Benchmark 3: Connection Pool ({NUM_QUERIES} queries)") print(f"Pool size: {POOL_SIZE_MIN}-{POOL_SIZE_MAX} connections") print("="*60) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) results = {} print("\nTesting asyncmy pool...") elapsed, count = best_result(lambda: loop.run_until_complete(test_asyncmy_pool())) results['asyncmy'] = elapsed print(f" Time: {elapsed:.3f}s, Queries: {count}") print(f" Throughput: {count/elapsed:.0f} queries/sec") print("\nTesting aiomysql pool...") elapsed, count = best_result(lambda: loop.run_until_complete(test_aiomysql_pool())) results['aiomysql'] = elapsed print(f" Time: {elapsed:.3f}s, Queries: {count}") print(f" Throughput: {count/elapsed:.0f} queries/sec") # Print rankings print("\n" + "-"*60) print("Rankings:") sorted_results = sorted(results.items(), key=lambda x: x[1]) for i, (name, time_val) in enumerate(sorted_results, 1): speedup = sorted_results[0][1] / time_val print(f" {i}. {name:15s} {time_val:6.3f}s ({speedup:.2f}x vs best)") return sorted_results if __name__ == "__main__": run_benchmark() python-asyncmy-0.2.15/benchmark/run_all.py000066400000000000000000000101301525423006000205310ustar00rootroot00000000000000""" Run all benchmarks and generate a comprehensive report. """ import sys from rich.console import Console from rich.table import Table from benchmark.setup import setup_database, cleanup_database from benchmark import large_resultset, concurrent, pool, batch_insert def create_summary_table(all_results): """Create a summary table of all benchmark results""" console = Console() # Overall summary table = Table(title="Benchmark Summary", show_header=True, header_style="bold cyan") table.add_column("Test", style="yellow", width=30) table.add_column("Winner", style="green", width=15) table.add_column("asyncmy Rank", style="cyan", width=15) table.add_column("asyncmy vs Best", style="magenta", width=15) asyncmy_wins = 0 total_tests = len(all_results) for test_name, results in all_results.items(): winner = results[0][0] winner_time = results[0][1] # Find asyncmy's position asyncmy_rank = None asyncmy_time = None for idx, (name, time_val) in enumerate(results, 1): if name == "asyncmy": asyncmy_rank = idx asyncmy_time = time_val break if asyncmy_rank: if winner == "asyncmy": asyncmy_wins += 1 rank_str = f"ðŸ�† #{asyncmy_rank}" diff_str = "WINNER" else: rank_str = f"#{asyncmy_rank}/{len(results)}" diff_pct = ((asyncmy_time - winner_time) / winner_time * 100) diff_str = f"+{diff_pct:.1f}%" else: rank_str = "N/A" diff_str = "N/A" table.add_row(test_name, winner, rank_str, diff_str) console.print("\n") console.print(table) console.print(f"\n[bold]asyncmy wins: {asyncmy_wins}/{total_tests} tests[/bold]") console.print() return asyncmy_wins, total_tests def main(): console = Console() console.print("\n[bold cyan]" + "="*70 + "[/bold cyan]") console.print("[bold cyan] asyncmy Performance Benchmark Suite (Complete Rewrite)[/bold cyan]") console.print("[bold cyan]" + "="*70 + "[/bold cyan]\n") # Setup database console.print("[yellow]Setting up database and test data...[/yellow]") try: setup_database() except Exception as e: console.print(f"[red]Error setting up database: {e}[/red]") console.print("[yellow]Make sure MySQL is running and accessible.[/yellow]") return 1 all_results = {} try: # Run all benchmarks console.print("\n[bold green]Running benchmarks...[/bold green]") all_results["Large Result Set"] = large_resultset.run_benchmark() all_results["Concurrent Queries"] = concurrent.run_benchmark() all_results["Connection Pool"] = pool.run_benchmark() all_results["Batch Insert"] = batch_insert.run_benchmark() # Generate summary console.print("\n[bold cyan]" + "="*70 + "[/bold cyan]") console.print("[bold cyan] Final Summary[/bold cyan]") console.print("[bold cyan]" + "="*70 + "[/bold cyan]") asyncmy_wins, total_tests = create_summary_table(all_results) # Performance insights console.print("\n[bold yellow]Key Insights:[/bold yellow]") console.print(" • Large Result Set: Tests data processing & parsing efficiency") console.print(" • Concurrent Queries: Shows async advantage over sync libraries") console.print(" • Connection Pool: Tests connection management under load") console.print(" • Batch Insert: Tests bulk operation efficiency") if asyncmy_wins >= total_tests * 0.5: console.print(f"\n[bold green]✓ asyncmy performs excellently![/bold green]") else: console.print(f"\n[bold yellow]â—‹ asyncmy shows competitive performance[/bold yellow]") finally: # Cleanup console.print("\n[yellow]Cleaning up...[/yellow]") cleanup_database() console.print("\n[bold green]Benchmark complete![/bold green]\n") return 0 if __name__ == "__main__": sys.exit(main()) python-asyncmy-0.2.15/benchmark/setup.py000066400000000000000000000051021525423006000202400ustar00rootroot00000000000000""" Database setup and data generation for benchmarks. """ import MySQLdb from benchmark import connection_kwargs, ROW_COUNT def setup_database(): """Create table and populate with test data.""" # First connect without database to create it kwargs = connection_kwargs.copy() db_name = kwargs.pop('db') conn = MySQLdb.connect(**kwargs) cur = conn.cursor() cur.execute(f"CREATE DATABASE IF NOT EXISTS {db_name}") conn.close() # Now connect to the created database conn = MySQLdb.connect(**connection_kwargs) cur = conn.cursor() # Create table cur.execute("DROP TABLE IF EXISTS test.benchmark_data") cur.execute( """CREATE TABLE test.benchmark_data ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, score DECIMAL(10,2) NOT NULL, is_active TINYINT NOT NULL, data TEXT, PRIMARY KEY (id), INDEX idx_user_id (user_id), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""" ) print(f"Populating table with {ROW_COUNT} rows...") # Prepare data data = [] for i in range(ROW_COUNT): data.append(( i % 10000, # user_id (creates duplicates for realistic queries) f"user_{i}", f"user_{i}@example.com", "2024-01-01 00:00:00", "2024-01-01 00:00:00", round(i * 0.123, 2), 1 if i % 3 == 0 else 0, f"Sample data for row {i}" * 5, # ~100 bytes of text )) # Batch insert batch_size = 10000 for i in range(0, len(data), batch_size): batch = data[i:i + batch_size] cur.executemany( """INSERT INTO test.benchmark_data (user_id, username, email, created_at, updated_at, score, is_active, data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", batch ) print(f" Inserted {min(i + batch_size, len(data))}/{len(data)} rows") conn.commit() cur.close() conn.close() print("Database setup complete!") def cleanup_database(): """Drop the test table.""" conn = MySQLdb.connect(**connection_kwargs) cur = conn.cursor() cur.execute("DROP TABLE IF EXISTS test.benchmark_data") cur.close() conn.close() print("Database cleanup complete!") if __name__ == "__main__": setup_database() python-asyncmy-0.2.15/build_cython.py000066400000000000000000000014521525423006000176350ustar00rootroot00000000000000import sys from Cython.Build import cythonize from setuptools import Extension from setuptools.command.build_ext import build_ext COMPILER_DIRECTIVES = { "language_level": 3, "cdivision": True, "initializedcheck": False, } if sys.platform == "win32": EXTRA_COMPILE_ARGS = ["/O2"] else: EXTRA_COMPILE_ARGS = ["-O3"] def build(setup_kwargs): setup_kwargs.update( { "ext_modules": cythonize( [ Extension( "asyncmy.*", ["asyncmy/*.pyx"], extra_compile_args=EXTRA_COMPILE_ARGS, ), ], compiler_directives=COMPILER_DIRECTIVES, ), "cmdclass": {"build_ext": build_ext}, } ) python-asyncmy-0.2.15/conftest.py000066400000000000000000000035251525423006000170020ustar00rootroot00000000000000import asyncio import os import pytest_asyncio from asyncmy.cursors import DictCursor import asyncmy from asyncmy import connect connection_kwargs = dict( host=os.getenv("MYSQL_HOST") or "127.0.0.1", port=int(os.getenv("MYSQL_PORT") or 3306), user=os.getenv("MYSQL_USER") or "root", password=os.getenv("MYSQL_PASS") or "123456", echo=True, ) @pytest_asyncio.fixture(scope="session") def event_loop(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._close = loop.close loop.close = lambda: None yield loop loop._close() @pytest_asyncio.fixture(scope="session") async def connection(): conn = await connect(**connection_kwargs) yield conn await conn.ensure_closed() @pytest_asyncio.fixture(scope="session", autouse=True) async def initialize_tests(connection): async with connection.cursor(cursor=DictCursor) as cursor: await cursor.execute("create database if not exists test") await cursor.execute( """CREATE TABLE IF NOT EXISTS test.`asyncmy` ( `id` int NOT NULL AUTO_INCREMENT, `decimal` decimal(10,2) DEFAULT NULL, `date` date DEFAULT NULL, `datetime` datetime DEFAULT NULL, `time` time DEFAULT NULL, `float` float DEFAULT NULL, `string` varchar(200) DEFAULT NULL, `tinyint` tinyint DEFAULT NULL, PRIMARY KEY (`id`), KEY `asyncmy_string_index` (`string`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci""" ) @pytest_asyncio.fixture(scope="function", autouse=True) async def truncate_table(connection): async with connection.cursor(cursor=DictCursor) as cursor: await cursor.execute("truncate table test.asyncmy") @pytest_asyncio.fixture(scope="session") async def pool(): pool = await asyncmy.create_pool(**connection_kwargs) yield pool pool.close() await pool.wait_closed() python-asyncmy-0.2.15/examples/000077500000000000000000000000001525423006000164145ustar00rootroot00000000000000python-asyncmy-0.2.15/examples/__init__.py000066400000000000000000000000001525423006000205130ustar00rootroot00000000000000python-asyncmy-0.2.15/examples/main.py000066400000000000000000000007261525423006000177170ustar00rootroot00000000000000import asyncio import logging import os from asyncmy import connect logging.basicConfig(level=logging.INFO) async def main(): conn = await connect( user="root", password=os.getenv("MYSQL_PASS", "123456"), database="test", echo=True, ) async with conn.cursor() as cursor: await cursor.execute("select 1") ret = await cursor.fetchone() print(ret) if __name__ == "__main__": asyncio.run(main()) python-asyncmy-0.2.15/examples/replication.py000066400000000000000000000012411525423006000212750ustar00rootroot00000000000000import asyncio from asyncmy import connect from asyncmy.replication import BinLogStream from asyncmy.replication.row_events import DeleteRowsEvent, UpdateRowsEvent, WriteRowsEvent from conftest import connection_kwargs async def main(): conn = await connect(**connection_kwargs) ctl_conn = await connect(**connection_kwargs) stream = BinLogStream( conn, ctl_conn, 1, resume_stream=True, blocking=True, only_events=[WriteRowsEvent, UpdateRowsEvent, DeleteRowsEvent], ) async for event in stream: print(event.schema, event.table, event.rows) if __name__ == "__main__": asyncio.run(main()) python-asyncmy-0.2.15/images/000077500000000000000000000000001525423006000160435ustar00rootroot00000000000000python-asyncmy-0.2.15/images/benchmark.png000066400000000000000000001700431525423006000205100ustar00rootroot00000000000000‰PNG  IHDRT$Wö«ß pHYs  šœ IDATx^ìÝy”–õ}7þ÷0ì‚" (‹àŠˆ`R“(&R—,<5Æš¤ñ$5I�š4öqIkª‰‰Õž&±6õ‰š…S£T©Zª( (AfDVÙWA¶™ùý1¿¹�¹A¹@PÀ×ëÏ>Ÿï5ÌrÏ=ø¾?×÷[QUUUÞQÿþý+*ªÅ´l|£_¿~Mëüÿª««“$-Êê¼ �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ªµ,/–©S§æŽ;î(/—TTT¤²²2íÛ·Ïa‡–ã�?>#GŽLçÎ�Ë—î·~øÃfúôé:thþú¯ÿº¼}@Û¸qc®¸âŠ$ÉÕW_�ÓO?½lÅ{oëÖ­Ù²eK:è fõŸÿüçù¯ÿú¯ôéÓ'·ÜrK³Þþæ½þº¯Zµªôؾîºë2`À€²4Ú¸qcZ·n�V­Z•· ¨À@}}}y©¤¾¾>uuuyýõ×óúë¯gΜ9™õ©òÖõ}z¯?�÷úï+wç�wfÑ¢E:th.ºè¢òv’bkö¶ñãǧ¦¦&—^ziy  �*|€\|ñÅ9ñÄ›Õêëë³yóæ¬\¹2“'OÎìÙ³³jÕªÜ|óÍù§ú§TTT4[ïÆìÙ³óÌ3ÏäöÛo/oP***Ò±cÇ$IeeeY÷À´lÙ²,X° Ç{ly«¤Èš½í¼óÎËÕW_�Áƒçøã�/oì”@>@zöì™N8¡¼\òñ�;mÚ´iúî›Y´hQ&Nœ˜+VdíÚµiÙ²e9ä� 4(C‡Ýî§åË—gòäÉiÙ²e>ùÉO&I,X� &déÒ¥¹à‚ Ò·oßf×ìÈ’%K2eÊ”$IÇŽ3bĈ=¾ÕC]]]~øá 6l»¯ñ®Øºuk¦OŸžùóçgÞ¼y©««Kß¾}sÔQGåä“OÞî1ûꫯ–^1bD>øàfý$™ùä“›tÞ²téÒüøÇ?μyóšÕC“Q£FåÏÿüÏ›õV¯^�±cÇ&IFŽ™{î¹g»é°çŸ>ÿùŸÿ™K.¹$ç�w^³^’ÔÖÖf̘1yôÑGË[I’‡~8ÿøÇóùÏþm£ &ä¾ûîËÖ­[Kµêêê$É“O>™o~ó›éܹs©×ôã>÷ÜsóoÿöoùÃþPê' Ÿ÷ĉsà 7”öŸm Ž=ûì³ùÏÿüÏüÃ?üC:uêÔ¬W[[›ÿøÇyöÙg›Õ=ûì³ù�ÿø�\wÝuͤ¥K—fìØ±iÛ¶m>ùÉOæ‘Gɯ~õ«ÒáDûØÇJkß΂ ò½ï}/ëׯO—.]ríµ×¾í×îݘ2eJV¬X‘#F”· [¼xq~ô£eáÂ…Íê3fÌHÒðbÀ׿þõ}ôÑÍú�ß¿N�:嬳ÎjÖK’_þò—¥ï×À�·»~Û¶m¹çž{²yóæ\vÙeÍzogË–-¥¿·W¯^ÍÕÇ{,¿øÅ/R[[[ª5zúé§Ó¥K—Üxã�…^Ø‘ÚÚÚÜu×]yúé§›Õ§M›–$2dH¾úÕ¯n>' Aì¸qãrÿý÷7ûøfΜ™|0�;wÎ 7ÜPz®\¹²ôy6zõÕWóꫯ&IF��~ýúZÓhwž_vçgaĈyä‘G2eÊ”œqÆåm€·%PJÞ|óÍüû¿ÿ{’䨣ŽJ·nÝÊV4L–ÝtÓMÙ´iS:wÎ:+½zõÊš5k2mÚ´Ìž=;?üpêëësÉ%—”_ž$¥0uРA8p`:uê”iÓ¦åùçŸÏÖ­[3f̘œvÚiÍÂÃúúú|ï{ßËÌ™3“$Ç{l˜ž={¦ªª*S§Nͺuë2~üø´iÓ&£G�.]Ûèµ×^Ë´iÓÒ®]»œsÎ99òÈ#³jÕª<óÌ3YºtiæÏŸŸûï¿?_ùÊWÊ/M’Ü~ûíy饗rÆgdÀ€iÓ¦M¦M›–)S¦dΜ9ùçþç¼òÊ+Ù²eKÎ?ÿüôéÓ'[¶lÉ”)S2}úô¬Y³&?ûÙÏrõÕW7{¿ÿïÿý¿<û쳩¨¨HÿþýÓ¯_¿uÔQÙ²eK¦OŸž?üáYµjUî¾ûî\wÝuÍ®m4eÊ”üò—¿LÒpR÷îÝw8�ÙÔ«¯¾šïÿûÙ¸qczôè‘¿ýÛ¿M—.]Ê—íS¦LI·nÝÒ³gÏòV!³gÏέ·ÞšÍ›7§E‹9óÌ3Ó¿ÿ´hÑ"sæÌÉĉ³|ùòüÝßý]®ºêªœzê©I’£�>:]ºtÉêÕ«3cÆŒíÕ+V4 ¿_~ùåíÕªªªlÞ¼9I2xðàf½]5}úôÜwß}©¯¯OŸ>}2|øðvØa©©©ÉK/½”I“&eõêÕùþ÷¿ŸÛn»m·Âí{ï½7K—.M�=rÚi§¥oß¾Y¾|y^|ñÅÌž=;S¦LɲeËrÓM7m71}ç�w–‚Ë¡C‡fÀ€9è ƒ2oÞ¼L˜0!kÖ¬ÉM7Ý”n¸!‡~x:uê”áÇ'I^xá…lذ!GqDŽ;î¸$É‘GYhM£=ñüRôg¡gÏžéÖ­›@ØeUøyùå—SSSS^Îúõë³bÅŠ<û쳩©©Éé§Ÿž¿ú«¿*_–úúúÜ{ï½Ù´iSŽ?þø\sÍ5ÍnCÿÄ'>‘‡z(cǎ͸qãrÊ)§dÀ€MÞCƒ‰'æ _øB>þñ�—jùÈGRUU•¿ÿû¿Ommm&L˜Ðl mòäÉ¥0uäȑͦPÏ<óÌ|á _Èõ×_Ÿyóæå¡‡Êyç�·]ˆ²dÉ’ôéÓ'ó7“C=´T¿è¢‹rË-·”­Ë/¿|‡Ó{/¿ür¾þõ¯çÃþp©6|øðüã?þc¦M›–iÓ¦¥C‡¹õÖ[›MþéŸþin½õÖ̘1#³gÏ.Õ“†»qK�Ï~ö³¹à‚ šõ‡ –Ž;æ±ÇËœ9sR[[»Ý©ñµµµ¹ï¾ûÒ¶mÛ\~ùå9óÌ3Kk6nÜØlm£Ù³gç?øA6mÚ”cŽ9&ßþö·Ó¡C‡òe{D}}}^|ñÅœrÊ)å­Bêëësß}÷eóæÍéСC®¾úêf§³ÿéŸþiÎ>ûìÜ~ûíY³fMî»ï¾œtÒIiÙ²e***2dÈ�Œ?>/¾øbêëë›…”³fÍJÒ0½ºnݺ̜93£F�*õ“·&`�:ê¨tíÚµYoW=÷Üs©¯¯OçÎ�sÓM75;iذa8p`þå_þ%K—.ͼyó¶ w‹XºtiN=õÔüõ_ÿuZ·n]ª_tÑEùÍo~“ÿø�ÿÈ¢E‹òøã�çüóÏ/õg̘‘I“&¥¢¢"ßøÆ7š…ŒgžyfÎ=÷Ü|ï{ß˲eËòoÿöo¹á†rÄG”^€¸ñƳaÆ 0 _úÒ—J×&)´fO<¿¼ÓÏÂŽôïß?Ó§OßîqðNl &LÈÝwß½Ý÷ßž|òÉÔÔÔ¤sçÎù³?û³´k×®üòüïÿþoi¿Ó¯~õ«ÛíéYQQ‘ .¸ '�tR’†¿oG Ô,LmÔ¿ÿÒTêŠ+Jõºººüö·¿MÒj]vÙeÛ…ùÜç>—¤!˜yá…šõ}á _h¦6jœ «­­Íêի˺ † Ö,Lmô¡}¨ôöÅ_¼Ý­Ú¥5o¼ñF6lØPêUUU•œ�~ô£¥zS�[/lÞ¼y»­’†ý:×®]›¿û»¿ËðáÃß1@J’_|1·Þzk6mÚ”“N:)×]wÝ^ S“†m6lØ�þýû—· yúé§³`Á‚$ ·‡7 S}ôÑùüç?Ÿ¤á±óØc�•z�{ž¾ñÆ™;wn©ž¼¨þÙŸýY***RUUµÝ­ø/¾øb’†‰ÍwkÕªUI’Ö­[ïpŸÔ3Ï<3§Ÿ~z ”µk×–· éСÃvaj£Ï|æ38p`’䡇ʦM›’¼Z'ÉG?úÑNlvØa¥`tÖ¬YYºtiÙŠwgO<¿ìêÏBÿþý³aÆÒã  �*|€4ÞTþ_÷îÝK™kÖ¬Éw¾ó�ÜvÛm¥Ûœ5Kýû÷ß.4lê#ùH’†[oß|óͲnChôvN8á„$ ‡A5Z¾|y)¼ÙÑ^ˆ� �Ûn»-·Þzë÷íÖ­Ûv‡ß4j:éövAVãçU®éž¨�·1—;ä�CJo×ÕÕ•Þ>ùä“sß}÷åÞ{ïÝa¨Y__ŸéÓ§——·óá¸Ù­ÓogÚ´i¥ïí©§žšo}ë[;œÆÝ“Ãñwz̼“Æ ÑC=ô÷`=ãŒ3J[ 4^“4<^§•›Ö“·Õ3Î8#½{÷ΦM›J{{& ÓÛ�a[‘èv¦q+‚eË–å_ÿõ_· È[´h‘k®¹&×^{mN?ýôf½¢FŒ±Ã0µÑ'>ñ‰$ óœ9s’4ìÜø3öv�ó$éׯ_i+�'Ÿ|²yó]ÚSÏ/E’·“M_ÀØ™í_X—^zé',-Z´(>ø`þð‡?äùçŸÏÏ~ö³|õ«_-õ/^œ¤!t½ë®»Jõr�˜[·nÍŠ+Ò·oßfýòÏ›j .ÌâÅ‹³hѢ“€;š(,7yòäLž<¹4�Ù8-¹·½þúëIÞþk³3�ŸïÞ½wz{ß¾}³xñâf_³-ZdðàÁ™0aB^|ñÅ\tÑEI>ÿ•+Wæˆ#ŽÈ¡‡šdáÂ…™9sf)xoÜ& G�ïøØ*êCúPÆ�Ÿ•+Wfâĉyúé§Ó·oßœp éß¿N<ñÄ´oß¾ü²]rì±Ç–—šiÚ_¶lY˜×^{­T{ì±Çv8ýÙhÛ¶mIš`·'ì©ç—"? �:vì˜ä­Ç(@U ¤wïÞùÚ×¾–7ß|3üãóÌ3Ïä3ŸùLißÈÆeñâÅ¥ðcgÖ¯__^Úî œ�Y¶lYéíÎ�;7éìš½=‰¹»jjjòøã�ç©§žÚ.ùä\|ñÅ;}ñàíìì±Ð¡C‡´iÓ&›7o.Mf6 Gÿ÷ÿ·ôö;ÙÓ!äžz~ÙÙçß”@ØU ™ŠŠŠ :4üãS__Ÿ ”ÕŽ;¦¦¦&ýúõ+ݺ¼3{âÔø¦{M6NÇ(6nܘï|ç;Y²dI***r 'äÄO,mÇУG�¼úê«ùîw¿[~i3;Ú¶\EEE.¿üò|ìcËÍ7ßœ™3gæá‡Î�!CrÔQG•/ßc^ÚÝï]ûöíSSS³ÃÕÊ5ÞÞ¦M›f�›ÆÉÏ7ß|3/¿ür\ ©O<ñÄ$ ÁjEEEª««³uëÖ´jÕj�îŸÚ¨mÛ¶ù⿘¿ø‹¿HUUU^zé¥TWWç•W^ÉÖ­[ó /dÆŒùæ7¿¹Ã­+vfg_§mÛ¶eË–-IÞzq£1XL¯z§-5½fOØSÏ/E~5>&›°3U`;M÷mºGaÏž=³bÅŠtéÒe»Óè÷¦¦“‰«V­jöñ•›={v6mÚ”Î�;ÞGñýôë_ÿ:K–,IëÖ­sÍ5×”ÜÙU;:à¨ÜŸÿùŸ—ûò—¿œk¯½6›7oÎ]wÝ•›o¾¹ÐûØ�·úoذ¡4Aº+Ž8∬^½ºÐ-æ�Ó¬åÛJ´lÙ2§žzj&Mš”3fdðàÁÛM¨¶oß>}ûöͼyó2gΜtèÐ!ëÖ­K×®]÷Jà\YY™”öïݰaCžxâ‰üö·¿Mmmmî½÷ÞÜqÇeWíÜÊ•+ËKͬ^½:õõõIÞúÙjÜ{6iØ£´üë÷^ØSÏ/»ò8nÜ>`w·£>˜Þy*àiþüù¥·{÷î]z»W¯^I’9sælwzSÕÕÕùÙÏ~–{î¹g·§›j¨N›6­I§¹¥K—æ»ßýn~ðƒdæÌ™åí}Ruuu’†)É· Sw�Õ4$ëÞ½{>ó™Ï$iØ;÷�(õö´¦�êîh|Ü-\¸ð¿ëׯOUUU’æa£Æ)Ó3fdíÚµY¶lYzöìÙ, oœV�9sfévÿÁƒ—úïÆ–-[òóŸÿ}’$Ï=÷ÜvOÍ�;77ß|s³iÆÝ�òÜ‘ŠŠŠ\qÅiÕªUêêêr×]weëÖ­åËÞµž={æÐC-MãîªîÝ»çÜsÏMÒðuºí¶Û²fÍšRýúõùÉO~R:™þ#ùÈoÑoÓ¦M ”¤!|OÞšHmtüñǧ²²2sæÌɬY³rðÁ§ÿþÍÖ¼�SÈãÇ�Ï£�>º]p9{öì<þøãI’~ýú¥²²²Y¿¨þð‡yòÉ'³iÓ¦$ ?# ,ÈM7Ý”U«V%IiB¹Ñ§>õ©´nÝ:«V­Êu×]—9sæ”~Æjjj2eÊ”Œ3&IÃ4éñÇßôòÒ‹¯¼òJ-Z”µk×–þþ"kÞ�ç—êêêzè¡;œhx;ïî%]`¿òÀºµ»eË–ùÌg>³Ã“Í?ýéOgåÊ•™6mZ|ðÁ<øàƒ9ì°ÃR[[Û,ä:ÿüóó¡}¨É•ïÎQG•Ïþó3fLV¬X‘믿>tPºvíšåË——B™îÝ»çË_þrÙÕû®‘#GfÖ¬YÙ´iSnºé¦tïÞ=íڵ˲e˲iÓ¦´iÓ&_üâ3f̘lÚ´)?øÁòÑ�~4£G�.W»¥gÏž¹øâ‹3vìØ¼öÚkyà�òþÏÿ)_ö® 8°tÐYEEEy{§>ýéOçõ×_ϤI“òüóÏçùçŸO×®]SYY™åË——Ö�vÚi¹ì²Ëš\ÙÜСC3eÊ”Ò$kù„jÛ¶msôÑGç•W^IÒÎîÎÇûv>÷¹ÏeΜ9Y¸pa~ñ‹_äþûïO·nÝÒºuë¬^½:ëÖ­KÒ°�ñ_üÅ_”]]̹瞛Ç}úäœsÎi¶wjSíÛ·Ï5×\“Ç{,ãÇ�ÏòåË›íkÙ£G�\|ñÅ¥} ÷¤‘#Gæøã�Ͻ÷Þ›yóæeãÆ�¥�¨M›69rd.¸à‚´mÛ¶ìÊ}×àÁƒó•¯|%¿ýío³råÊR8زeËôë×/ù—™^½zeþüù™0aBÞxã�ÝÚWó�œþùyî¹ç2oÞ¼Œ7.C† É1ÇS¾ì]2dHž~úéÌš5«t[ý®hÛ¶m®¼òÊ 4(ãÆ�Ë’%KJ“–9âˆ#rÎ9çäœsÎ)»²¹SO=5-[¶Ì¶mÛÒ«W¯î�9`À€R º£Þ�víÚåÿþßÿ›qãÆåþç²qãÆ,Z´¨ÔoÛ¶m>ô¡å‚ .Øí)ë!C†äôÓOϯýëÌ›7¯t[{Ë–-sä‘Gæ’K.yÛïÁàÁƒóýï?÷ÜsOªªªòæ›o–>¾ÊÊÊœuÖY¹ð ӵkײ+“Q£FeõêÕùãÿ˜7Þxc‡û îlÍ{ùü2kÖ¬¬_¿~��€_EUUU}ÒpKÀ®Ú¼ys.\˜úúútëÖ­Ù?{S]]]–.]šµkצ{÷îéÚµëvsû“ººº,_¾<«W¯Î!‡’=zlw»÷âÅ‹S[[›^½z•Ú_Ô××çoþæoÒ·oß\yå•åí]¶uëÖ,Z´(uuuéÝ»wÚ´iS¾dŸ·mÛ¶¬^½:«W¯N}}}:uê”nݺ¥U«VåKwÛš5k²dÉ’´oß>Gyä.í9Z__ŸU«Veùòå9ôÐC÷øÇVÄÞ|~ùÉO~’ùóçç?øÁ~ýܼw·°¨ðž˜4iR~úÓŸæ'?ùI:tèPÞ†÷Ìo¼‘+¯¼2W\qEÎ<óÌò6À5ªû×hû­øÃ9òÈ# íã {Ó<�#�øàòìªìWºuë–aÆåÒK/ÍèÑ£ËÛÛY±bE~õ«_å+_ùJ*++ËÛ°KL¨²_éÝ»wz÷î�$YµjUÆŽ[¶â-uuu¹óÎ;ó‰O|¢t ¼UX¿ÿýï³uëÖœþùå­ÝR]]]^öýúõ+/Á^ã–H¯¾újzè!·ú°G™P倳yóæÜyç�¹ð ÷è­þ^í@ Êç‰'žÈÊ•Ž#Ñ‚ IDAT+³iÓ¦üæ7¿)ÕkjjRUU•ßüæ7>|xºwïÞä*Ø9�*œúúú´k×.&LhVß¼ys,X�eË–eàÀ�Uv™@•Îù矿讼òÊœy晹ä’KÊ[PˆC© ¨TQUUUŸ8ÁàíTWW'1¡ P˜@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@A-Ë ìžª(¯ìΪ/¯À~Å„*@AU€‚ª T ¨$P(H  PPËòì/jkksíµ×fÔ¨Q>|xy;‹-ÊC=”ùóç§eË–9æ˜crá…¦k×®åK ªì—jkk3vìØ,^¼8[¶l)ogúôé¹þúë³xñâ :4ÌóÏ?ŸoûÛyíµ×Ê—@!&TÙ¯L›6-ãÆ�Ë‚ ²iÓ¦òv’¤®®.?ýéOsÔQGå†nHeee’䓟üd®¹æšüæ7¿ÉUW]Uvìœ Uö+ݺu˰aÃr饗fôèÑåí$ÉÚµk³nݺ|ä#)…©IÒ©S§ôêÕË„*»Í„*û•Þ½{§wïÞI’U«VeìØ±e+’¶mÛæÊ+¯Lÿþý›Õëêê²|ùòôíÛ·YЍrÀ9è ƒræ™g–—3nܸ¬_¿>ò'RÞ*¤ººº¼ï›~å…ý„ß§ÀÞЯßþú¯#öGUxõõõùÝï~—x C† ɰaÃÊ—@!Uh+V¬È�wÞ™êêêœsÎ9ùüç?ŸŠŠŠòe…xµ €}ÊÒòÂþÁïSöwUXS¦LÉ¿üË¿¤cÇŽ¹öÚk3hРò%°Kª�f̘‘þçÎi§�–¯|å+i×®]ùØeU8µµµ¹÷Þ{Ó¿ÿ|ãßHeeeùØ-U8³gÏβeË2hР<ûì³åí|ðÁ8p`yvJ ÊgîܹI’Ç{,�=öXY·á0 �*»£¢ªªª>qâ*À~婊òÊþá¬úò 쪫«“$-Êê¼ �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨Ô²¼ðA3f̘òÒ>ï’žåà½`B  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚Z–`Q[[›k¯½6£F�ÊðáÃËÛY±bExà�ÌŸ??IÒ·oß\|ñÅéÞ½{ó…P� UöKµµµ;vl/^œ-[¶”·³hÑ¢|ûÛßάY³rÊ)§ä”SNɬY³ríµ×fáÂ…åË ªìW¦M›–qãÆeÁ‚Ù´iSy»ä¿øEZ·n�[n¹%:tH’Œ5*ßüæ7óóŸÿ<×_}Ù°s&TÙ¯tëÖ-Æ Ë¥—^šÑ£G—·“$›7oÎÌ™33dÈ�R˜š$:tÈ�!C2kÖ¬w càí˜Pe¿Ò»wïôîÝ;I²jÕªŒ;¶lEC½®®.=zô(o¥G�©¯¯ÏªU«Ò«W¯ò6¼#ªpjjj’$íÛ·/ë¼U{óÍ7Ë:°s&T9àlÛ¶-IRQQQÖy«V[[[ÖÙ¹êêêò°‹ü>ö†~ýú•—`¯1¡Ê§eË†× ƒÕ¦k•••eØ9ªp:è $Ɇ Ê:oÕšVU”W»\S§N-/±—ø} Àþ΄*œ.]º¤²²2‹/.oeÉ’%iÑ¢EºtéRÞ€�¨rÀiݺuŽ;î¸L›6-¯¿þz©¾~ýúL�:5Çw\Ú´iÓä (F ÊiÔ¨QÙ´iSî¸ãŽL™2%S§NÍwÜ‘šššŒ5ª|9bUH§žzj¾üå/ç׿þuî¸ãŽ$ {«þå_þeN;í´²ÕPŒ@•ýV×®]3f̘òrɈ#2bĈ¬^½:IÃÞªðnT9à RØSì¡ P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ªµ,/ì®57žV^Ú?ÿÍò À™P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@AU€‚ª T ¨$P(H  P�@  �*@A-Ë p X´hQzè¡Ì�;7­ZµJïÞ½sÑE¥G�åK ª�ª««sýõ×gîܹùð‡?œ¤ºº:û·›¹sç–/€Bª�~øá´iÓ&7ß|s>ýéOç²Ë.Ëu×]—ººº<üðÃåË �*¬víÚ¥mÛ¶¥?rÈ!iÕªUZ¶´Ó»G ÊièСY¹reþð‡?$IêêêòØc�eÓ¦M:thÙj(ƨ¤aÆeݺuùÑ�~”x o¾ùfÖ­[—Ë/¿Pݺuk¦L™’G}4 .,oÃvV®\™úúú´iÓ¦¼•.]º$‰@€Ý²OÝò¿qãÆÜ{ï½Y¾|ynºé¦Ô××çûßÿ~fÍš•$©¬¬Ì׿þu§´óŽzõê•ŠŠŠLœ81çŸ~³ÞôéÓSQQ‘^½z5«@ûÔ„ê?þã?fÒ¤I騱c’ä¹çžË¬Y³rÒI'åŠ+®Èᇞ»ï¾;Û¶m+»ÞÒ¶mÛ|üãÏk¯½–Ûo¿=Ï>ûlæÌ™“{î¹'Ï=÷\FŽ™¶mÛ–_;µÏL¨¾öÚk™={vFŽ™Ë.»,IC ZYY™+®¸"]ºtI×®]sË-·döìÙ8p`Ù{€·|îsŸK»víò_ÿõ_™:uj’† õ¢‹.ÊE]T¶ŠÙgÕeË–%IÎ:ë¬RmÖ¬Yéß¿ißËÆ†ìÉδjÕ*£G�Îg?ûÙ¬^½:õõõéÚµk***Ê—@aûÌ-ÿµµµI’–-2Þ¥K—fݺuéß¿iÍæÍ›“4ì¥ ETTT¤k×®9ì°Ã„©¼kûL zøá‡'I^~ùå$ÉÿüÏÿ$IN>ùäÒšiÓ¦%yk-À{iŸ¹å¿OŸ>éÓ§O~ùË_f„ Y¸pazöì™ãŽ;.Û¶m˯ýë<þøãéÙ³gŽ9æ˜òËöº}fB5I®ºêªôéÓ'‹-J×®]óµ¯}-Ù²eK}ôÑtíÚ5W_}uùeï‰}fB5Iºuë–ï~÷»ÙºukZµjUª·iÓ&?üáÓ³gÏ&«Þ[ûÔ„j£¦ajÒp•0x¿½oª555ùñ�\^.ä²Ë.K÷îÝËË{Õû¨nÛ¶-/¼ðBy¹™-Z¤¢¢"µµµÍêŸúÔ§šýà½ð¾ª;v̘1cšÕ¦OŸžú§Êgœ‘O~ò“éÙ³gZ´h‘%K–düøñyê©§ò…/|!G}t³ëÞ ï[ ZnË–-¹ë®»2xðà|õ«_mÖëÙ³g¾ô¥/¥²²2÷ÜsO†šƒ:¨Ù€½mŸ9”ªªª*ëׯÏðáÃË[%Ç϶mÛ2þüòÀ^·Ïª6lHÒ°ÀÛi×®]’dåÊ•e€½oŸ T»té’$yùå—Ë:o™>}z’¤sçÎe€½oŸ T�=öØ~øá¹ÿþûóÌ3Ϥ¶¶¶Ô«««ËÓO?�ÿ÷O÷îÝ3`À€&W¼7ö™C©*++sÕUWåÆoÌ�wÞ™_üâ9âˆ#Ò¢E‹,Y²$ëׯOÛ¶msõÕW§eË}æÃ>@ö©d²wïÞ¹í¶Ûòàƒæ…^È+¯¼’¤a;€sÎ9'^xa:uêTvÀ{cŸ T“†ðôK_úR’dëÖ­I’V­Z5]ð¾ØçÕ¦©À¾dŸ Tׯ_Ÿßÿþ÷y饗òú믧®®®|I’äºë®KŸ>}ÊË{Õ>¨ÖÕÕåöÛoOuuu9ä�tëÖ­|I‰ÉUàý°Ïª‹-Juuu†ž+®¸"-Z´(_ð¾ÚgRËeË–%IÎ;ﳇj«V­rÕUWåŽ;îH’œuÖYéÒ¥ËoÿoÛ¶íë{Ó>¨®X±"·ÜrK¶mÛ–ñãÇgüøñåKJþáþ!G}ty`¯ÚgÕ6mÚdðàÁååêØ±cy `¯ÛgÕC9$W^yey`Ÿ±ÏªåÖ¬Y“%K–¤®®.=zôH—.]RQQQ¾ þ?öî=ªê:ßÿøk³ÙlÄK ²E.ŠW4“@I ¡ËÊQÔËlê¬jªã93åÌ4ê1DZã¥)G3§ÉÎØ˜Z"šåiR±c5*�¢‚™’·@EPÐH¹ìýûƒ{Ø_uúš· >k¹V¼ÞŸÏ×�Åb­—Ÿï÷ \7^W¨îß¿_ï¼óŽ<è‘·oß^�<òˆ¢££=r¸^¼ªPÝ¿¿^zé%¹\.ÅÆÆª]»vòññÑÑ£GµsçNÍž=[/¼ð‚bbbŒ[àšóšBµ¶¶V .”¿¿¿&Mš¤Î�;{Ì 4sæL-Z´HúÓŸd·Û=æp­ùƒ%//O§N�Ò˜1c.(S%©]»v7nœÊÊÊ´oß>ã®9¯)TKKK%Iݺu3Lþ©~V¿®'¯)Týýý%ýë²ôôéÓ’þ¹®'¯)T£¢¢dµZµzõjÕÖÖÇr¹\JKK“ÅbQTT”q ל׼”ªeË–5j”V®\©I“&iøðájß¾½|||tôèQ}øá‡:tè�†ªÖ­[·À5ç5…ª$ >\gΜQzzº.\è1³X,8p ÆŒã‘ÀõâU…ªÅbÑã�?®Áƒ+;;[Ç�—ÓéTXX˜bcciÜ×�WªõÂÃÃnŒà†òš—RÕËÊÊÒÌ™3uôèQwöî»ïjîܹÊËËk°®/¯:¡ºaÃ-Y²DV«U>>ÿìz´sçNíܹSÏ<óŒ Ð`\^sBµ²²R+W®TÇŽµ`Á………¹gÆ Ó¼yó¡·ß~[ååå vÀõá5…ê¡C‡T^^®‡zH���ƱZ·n­qãÆéìÙ³:tè�q לתgΜ‘¤‹–©õBCC%I%%%† \{^S¨¶lÙR’täÈÏAõ/ªr8† \{^S¨FEE) @©©©=�Z^^®+VÈf³©}ûöÆ1pÍÕ:]ú¶¢Æô¯Êj§ñ�Ö—_~©±cÇ*77×8ú—¦OŸ®iÓ¦]òë«í›o¾Ñ7ß|cŒ¯_cp£Øív=û쳚;w®&L˜ øøxEDDÈf³éĉÚ¶m›Î�;§ŸýìgjÑ¢…q;pÍå—VêÅ÷ãKÙÛ¡‘½¹Ûº¡°°09�×®h~ûí·U[[«ßÿþ÷ÆÑUá5…ª$ÅÅÅi„ z÷ÝwµeË�YË–-õøã�ëž{îñÈ4O>ù¤1jT¼ªP•êJÕÞ½{ëøñã:qâ„*++Õ¶m[EDDÈn·—�Ò‚ Ô±cGEEEiõêÕ:pà€BCC5zôhEGGkÕªUÊÌÌTii©:uê¤G}T‘‘‘:vì˜Þ|óM%''+99Ùãš;wîÔÚµkõÔSO)<<\[¶lÑG}¤cÇŽÉÏÏO�:uÒƒ>¨.]º¸÷8�N­Y³FÙÙÙ:zô¨BCC5lØ0•••iûöíš2eJƒßÁ“ËåÒG}¤ÌÌL¨U«VºãŽ;4bĈKÞeþ׿þUN§SO<ñ„;+--Õòå˵oß>Y­VÝvÛm>|¸BBB$I{öìѪU«4aÂýïÿþ¯²³³uúôiuêÔI�=ö˜ÂÃÃõå—_*55Ur¹\š6mšbbb4bÄ÷ïs5xÍ3T²X,ª¬¬TUU•Ú´i£:¨ªªÊ¸ h´Ž9¢ÌÌLÍ�;W‡C÷ÝwŸJKKµ`ÁÍŸ?_7nTÏž=•œœ¬¼¼<Í™3G.—Kaaa:}ú´6nÜh¼¤6oÞ¬ÒÒR………)33S .”ÝnרQ£4hÐ åççköìÙï0úãÿ¨´´4ÝrË-JIIQDD„-Z¤õë×ë믿npõ ½þúëZ¶l™üýý5tèP…‡‡kÆ úÃþ ššãrIRAA� Ü_?~\“'OÖ�”˜˜¨øøxíØ±C3fÌpβ²2åååiñâÅÚµk—âããÕ«W/íß¿_/¿ü²jkkåçç§Ö­[Ëf³É××W­[·¾d©{%¼î„ê¶mÛ´jÕ*J’xàµmÛVãÇ�WÿþýõÄOÈÏÏϰ h|<¨)S¦¨G�’¤®]»jöìÙÚ³g�^{í5÷ÛÍš5Óš5kT\\¬��%%%iÕªU***rŸâ¬ªªRNNކ "‹Å¢¬¬,ÙívM™2E>>uç*ûõë§I“&i×®]4h�rrr´cÇ :TcÇŽ­ûP’âãã5oÞ<Ùl6wf´ÿ~mÙ²E÷ß¿Æ�çÎÓÒÒ”––¦ÜÜ\õêÕ«ÁŽ‹{ç�wÔ¬Y3Íš5Kþþþ’¤!C†hâĉJKKÓÏþs÷Úo¿ýVÿýßÿíþ\aaaJMMÕ�Ô­[7uíÚUÓ§OWmm­þó?ÿÓ½ïjòªªŸ}ö™^{í59�N�=Úýãïï¯èÿø‡-ZdØ4N]»vu—©’!IêÛ·¯Çã/ëóòòrIÒÀ�e±X”‘‘á^“““£ªª* 8P’¬ªª*¥§§«¶¶V’©?ÿùÏ0`€$iË–-òõõÕðáÃÝבê ÕN�:ydFŸ~ú©|}}•’’â‘?ðÀzê©§è‘_Ì™3g´k×. 2Ä]¦JRëÖ­•œœ¬ŒŒ UWW»óûî»Ï£ä�‰‰‘$�>}Ú�]k^sBõìÙ³zë­·Ô¹sgM™2E~~~úàƒ$I6›MÏ<óŒ‡ÒÒÒTXX¨ÐÐPÀÆÅX:úúÖÕuaaa¹Õjõø:88X·Ýv›222ÜehVV–ºtéâîÍ~üã+;;[K–,ÑÊ•+Õ½{wEGG+!!Á]^Éáp¨yóæîk×ëܹ³Ç­ùFõ{�·Õûûûë®»îòÈ.åøñã’¤ 6èÿø‡Ç¬¬¬L•••*--ugõ§qëÕ—«õ…ñõà5'T¿þúkUWWkäÈ‘—¼¥ÿÎ;ï”$åçç&ÀÍ%))Iß|ó� USS£ììl%&&ºç‡C3gÎÔsÏ=§;î¸Cùùùzûí·õÜsÏiÏž=’¤êêêK¾þR]½òòr�S¥?Dý{“~ô£©mÛ¶¿¢¢¢4`ÀY,÷zc±|#xÍ Õú¹uëÖ†É?¹\.IREE…aÜ\úô飀€edd¨C‡ª®®VBB‚{^[[+«ÕªøøxÅÇÇK’víÚ¥9sæhÅŠš1c†Ú´i£ÜÜ\÷Ú†Nœ8áñµ‘Ãáо}ûär¹¬cÇŽiãÆ�:pà€zöì)IJLLTÛ¶mµxñb}üñÇ:|ø°2335cÆŒ N¬ }ºþò—¿è­·Þr?Ò³èY IDATgÏžzâ‰'Ü/Œú> ����–-[¦ÌÌLY,uîÜYãÆ�Ó}÷Ýg\þ½ ¤Ã‡ëÍ7ßÔ�wÞ©.]º—\Ëþýû]RÝ]oPYY©µk×ê‹/¾PQQ‘\.—‡bcc5bĵjÕʸx‰Ò©½�Q£ðq÷_#¯76ücÔ8$]»[¯nF.—K¿üå/Õ»wo=þøãƱ¤ºO•––Êét*((è’/¡ª®®VII‰‡¬V«–.]ªôôt½ýöÛÆ¥¨®®ÖÉ“'tE/ª*++“$¯ìëïª÷ªª’äïï¯1cÆh̘1ª©©‘Ëå2Ýf7“¬¬,�:uJwÝu—qäf³ÙbŒ/`³Ù~ð»‹l6›ûSW‹T#¯+T¥ºFÛf³É××WçÎ�Ó'Ÿ|¢’’EGG+::Ú¸¸©lݺU™™™ÊÍÍU||¼"##�Kp�xU¡ZRR¢E‹éÌ™3zõÕWUSS£—^zIùùù’¤uëÖé‰'žÐ Aƒ ;€›GóæÍUSS£ÁƒkĈÆñUÑ£G*T_yå())IR]ÓžŸŸ¯¾}û*))I«W¯ÖòåËuç�w^òY@S£˜˜c|UÅÅÅ)..Îßô|ŒÁ�røðaåççkĈúùÏ.IÚ±c‡l6›þíßþM±±±7nœ***´oß>Ãn¸ö¼¦P=yò¤$©oß¾îlß¾}ºõÖ[Ý£­(îéÓ§ÝkàzñšBÕ¨  @ß}÷�ºuëæÎÊËË%‰ÛýÜ^ó ÕððpIRvv¶Úµk§¿ÿýï’¤ØØX÷šmÛ¶y¬þ•ššmÚ´I_~ù¥òóóÕ¦MÝwß}êÕ«—q)`Š×œP W·nÝôÞ{ïéÙgŸÕæÍ›Õ¹sguèÐAÕÕÕš?¾V­Z¥Î�;«}ûöÆí€‡êêjÍ�;W©©©²ÛíJHHйsçôÊ+¯èðáÃÆå€)^S¨JÒóÏ?¯>}ú¨¶¶V�;wÖøñãe±XT]]­ÌÌLuïÞ]¿úÕ¯ŒÛ€ ¬[·NûöíÓ‹/¾¨ñãÇk̘1š>}ºÂÃÕššj\˜â5·üKR«V­ôüóÏcùûûë�7ÞP‹-Œ#à¢>ûì3ÅÇÇ+22Ò�Y­Výá�Óél°0Ï« ÕKñññ¡L…i§N�RQQ‘FŒ¡’’íÞ½[Ç�Wxx¸úôéÃÿKøÁE¡ \޲²2IÒÙ³gõ_ÿõ_:þ¼üýýuæÌ­^½Z/¼ð‚ÚµkgØõýòòòŒÀ Øü<×BTT”1®¯z†*p5Ôª+V¬Ð�!CôÖ[oéõ×_פI“töìY½õÖ[†€9œPE“ãë[÷¿u‡”’’âΣ££5xð`­[·NEEE qÏÌào»àû•À€Ÿ§ ±sž)TÅçÿcŒ/É/j lÝ�11 U49Í›7—$uëÖÍ0‘:wî,I*,,¼ìBÀyî´ª¾H3Æ—äÓ"˜Bµ‰á–49¡¡¡òõõUUU•q¤óçÏK’üýý àûqBMŽ¿¿¿zôè¡�;wªººZ6›Í=ËÊÊ’ÝnWûöíì¸1>ÿüs­_¿^Ç�—ËåRxx¸RRRtÇw¸×œ9sFï½÷žvïÞ­sçÎ)((Hýû÷×È‘#eµZµlÙ2>|X'Nt? ±Þܹs¢GyD ,PÇŽÕ½{w­^½ZyyyjÕª••’’"‹ÅâÞWXX¨•+WêÀ�ª¨¨Pûöí5lØ0ÅÆÆJ’öìÙ£U«Vi„ Z·n�vìØ¡òòrÅÆÆêÉ'ŸT~~¾RSSµÿ~(99Y#GŽ”Åb1ýy½'TÑ$�9Rß}÷�^~ùeíÙ³GÔ’%K´cÇ 2DÆ-×Õ'Ÿ|¢E‹Éb±èÞ{ïÕСCUVV¦yóæ©°°P’är¹4kÖ,effªÿþ=z´"""´fÍ­X±B’Ô®];íÝ»W999 /¯‚‚mß¾]ááá’¤#GŽhçÎ�zõÕWuË-·èÞ{ï•���RSSõÿ÷î}‡Ò‹/¾¨}ûö©W¯^ºë®»TRR¢W^yEÛ·o—T÷Rð¼¼<ÍŸ?__}õ•ú÷ï¯îÝ»ëÓO?ÕÂ… 5sæL•——ë�P`` ÒÒÒôé§ŸJ2ÿy½'TÑ$EEEé¿ø…V¯^­Y³fÉår©U«V3fŒ† f\pÝ¥§§+((H¿ûÝïÜ'5cccõâ‹/jÿþý Uaa¡ ôðÃë'?ù‰$ièСš3g޶nݪqãÆ©oß¾Z²d‰222Ô»wo÷õ³²²äçç§¾}ûº³¯¾úJ¿þõ¯Ýë† ¦gŸ}Vºûî»%Iï¾û®|||4}út9IÒˆ#ôË_þRkÖ¬ñ8=kµZ5mÚ4Y­VIÒÔ©S•‘‘¡‡zÈÝÁÜÿýzæ™g´wï^%''_ÖçõFªh²âââ§òòrUVV*((ȸà†ùéOª   �ÛÞ+**$Iååå’¤ýèG²Z­ÊÈÈP||¼û%Ûÿñÿá~ŒÝnW¿~ý”™™éñøÃ¬¬,ÅÅÅ©Y³fªîQbÚívÝzë­*..–$•––jÏž=zà�Üeª$h„ ***’Ëårç<ð€»L•êNŸ\¯¼òŠ~úÓŸªyóæÚ´i“f̘¡W_}Uµµµ’êضm[mÛ¶MRÝiÏÀÀ@EGG7¼ÔŦQý Y»Ýn˜\]f?¯7úׂ®º¢¢"¥¦¦jàÀ�zúé§e±X$IgΜñXçr¹ät:¦””¥¤¤¨¬¬L‹/ÖöíÛµ{÷n÷-üIIIZ»v­ÎŸ?¯¬¬, 0@>>—wž288X’Ü�hhçÎ�úúë¯5jÔ(ã蹟÷FðþO41ÇŽ“$õêÕË]¦JRnn®ûŸ%iûöízôÑG•——çÎZµj¥!C†H’¾ûî;wž˜˜¨ªª*¥§§ëСCJLLtÏÌ SË–-õ�üÃãY©µµµzë­·´cÇ�g¦^‰«ñyoN¨×YÛ¶m%IëׯWëÖ­åã㣽{÷jݺu²X,:zô¨Îž=«îÝ»+ @o¾ù¦zè!…††ª¬¬Lï¿ÿ¾¬V«Ç3Mƒ‚‚tûí·+55U‘‘‘jß¾½{f–ÍfÓðáÃõÎ;ïhþüùºçž{T^^®Í›7«´´TcÆŒ1nùÁ®Æç½(T“|ÛF)ð…�Æø’,~c}XX˜zè!­^½ZS§N•$EFFjòäÉZºt©6oÞ¬®]»ê®»îÒ /¼ ¿þõ¯š3gŽÇþ &\𒩤¤$íÞ½ûŠN{ÞÿýòóóSjjª233%Õ½ôûg?ûÙ]÷b®Æç½Þ,û÷ïwIu‚¸¥SëžÝÔØ|ÜýׯÈë� Ä5Iÿ¼m RUU•JJJ¨€€I’ÓéTII‰‚ƒƒ=PVV¦o¿ýV-[¶T`` ;ohóæÍZ¼x±^{íµK®¹¥¥¥ª­­½à³\-Wûó^Kõ�]à„*pƒØív………yd>>>r8™T÷ìÔV­Zc·ÚÚZ­_¿^½{÷¾jådPP�1ºj®Åç½(T€FnÞ¼y*..ÖÑ£GõôÓOÇ^§±}Þ†(T€FÎn·+$$D?ü°:tè`{�Æöy¢P¹gŸ}ÖyµÆöyò1€‹£P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BLò5.ÎétêüùóÆø’|}}åëKw1Ó§O—ÓéÔ´iÓŒ#¯ÆMÀ¤3gÎèã�?6Æ—­èèhcŒFŒ[þÀ$N¨7À_þòùûû«ÿþzï½÷tøða©gÏž3fŒìv»$iÙ²e:|ø°&NœxÁãæÎ�«��=òÈ#Z°`�:v쨨¨(­^½ZPhh¨F�­èèh­ZµJ™™™*--U§N�ôè£�*22RÇŽÓ›o¾©ääd%''{\çÎ�Z»v­žzê)…‡‡kË–-úè£�tìØ1ùùù©S§NzðÁÕ¥K÷§Ó©5kÖ(;;[G�Uhh¨† ¦²²2mß¾]S¦Lið;4>œPn€üü|mß¾]³fÍ’Ýn×< °°0­_¿^/½ô’ª««%IíÚµÓÞ½{•““㱿  @Û·oWxx¸$éÈ‘#ÊÌÌÔܹsåp8tß}÷©´´T ,ÐüùóµqãFõìÙSÉÉÉÊËËÓœ9sär¹¦Ó§OkãÆ�×—¤Í›7«´´TaaaÊÌÌÔÂ… e·Û5jÔ( 4Hùùùš={¶JJJÜ{þøÇ?*--M·Ür‹RRR¡E‹iýúõúúë¯\½qâ„*pƒœ:uJ£G�Öˆ#ÜYzzºþçþG›7oÖàÁƒÕ·o_-Y²DêÝ»·{]VV–üüüÔ·o_wvðàAM™2E=zô�$uíÚU³gÏÖž={ôÚk¯¹O½6kÖLkÖ¬Qqq±BBB”””¤U«V©¨¨H!!!’¤ªª*åäähÈ�!²X,ÊÊÊ’Ýn×”)SäãSwN³_¿~š4i’víÚ¥Aƒ)''G;vìÐСC5vìØº%)>>^óæÍ“Ífsg�'T€¤yóæ:t¨G6hÐ 9}öÙg’$»Ý®~ýúiÇŽîS«R]¡§fÍš¹³®]»ºËTIŠˆˆ�$õíÛ×]¦6ÌËËË%I”ÅbQFF†{MNNŽªªª4pà@IRpp°ªªª”žž®ÚÚZIRdd¤þüç?kÀ€’¤-[¶È××WÇw_Gª+T;uêä‘5VªÀ Ò®]» NmZ,uêÔI'NœpgÉÉɪ¨¨pßöâÄ ¸ËÎz���_×?s5,,Ì#·Z­_ë¶Ûnó(T³²²Ô¥K…††J’~üã+<<\K–,ÑÓO?­W_}Uëׯ—$ùûûK’ŠŠŠäp8Ô¼ys÷uêuîÜÙ5JÜò€·x¯1jæÿ’ŸŸŸ1’$Ùl6UUUÉårÉb±(**JmÛ¶Õ¶mÛ§¬¬,*::Ú¸õKJJÒÂ… UXX(‡Ã¡ììl=ôÐCî¹ÃáÐÌ™3µsçNeggkïÞ½ÊÎÎÖ{ï½§ &¨gÏžª®®ö8 ÛÐ¥þ]N¨7Haa¡1’Tw588X‹Å�%%%);;[çÏŸWVV– à~–éÕЧO(##C¹¹¹ª®®VBB‚{^[[+«Õªøøx=óÌ3š?¾^xáUWWkÅŠ’¤6mÚ¨¸¸ØýH€†ž¸m̮ޟ8€ËròäI÷müõŽ;¦ƒê¶ÛnóÈÝÏ0=tè�=æWÊÏÏO ÊÈÈÐ_|¡^½z©E‹îùäÉ“õòË/7Ø!ÅÆÆ***Jß}÷�$)&&FåååÚºu«ÇºS§Ni÷îÝYcÅ-ÿÀ b³Ùô§?ýIcÇŽUçÎ�uâÄ -_¾\6›MÆ óX¤Ûo¿]©©©ŠŒŒTûöí=æWCRR’6mÚ¤ââb�?Þc§÷ß_+V¬P¿~ýäçç§½{÷êÀ�î—R%&&êƒ>ÐâÅ‹uîÜ9uëÖMÅÅÅz÷ÝweµZåt:=®ÙQ¨&ÙívuéÒÅ_RPP�1òЭ[7õìÙSË–-See¥,‹Ú·o¯_ýêWr8ÆåJJJÒîÝ»¯úéÔz]ºtQxx¸¾ýö[õêÕËc6jÔ(UTThÆ úðÃ%Õ zì±Ç$Õ½ìjÚ´izã�7´|ùrÕÔÔÈf³éî»ï–ÅbѦM›^²Q¢PLjÞ¼¹âãã�ñ6l˜î¿ÿ~)((HÍš53.q«¬¬”���ú÷ïoiΜ9ÆH­ZµÒòåË�±âãã/š»\.UUU©ÿþ²Z­3«ÕªÇ{LcÇŽUii©œN§‚‚‚.x U«V­ô›ßüFÕÕÕ*))‘Ãá�ÕjÕÒ¥K=ÖM�:ÕãëÆ‚B¸Á|}}nŒ=ÔÖÖjýúõêÝ»·�ã«"++K§N�Ò]wÝe¹Ùl6…„„ã Øl6µmÛÖ7zª€—›7ožŠ‹‹uôèQ=ýôÓÆñÛºu«233•››«øøxEFF—àÿ£Pn€;ï¼S~~~Æø¢ìv»BBBôðëC‡ÆñkÞ¼¹jjj4xð`�1Â8¾*zôè!›ÍfŒ Uà2dˆ1º¤gŸ}Ö]U111Љ‰1ÆWU\\œâââŒq£ãc G¡ &Q¨€Iª`…*˜D¡ &Q¨€Iª`…*˜D¡ &ù—Pù�”?˘^Zë¡u¿®ÀôéÓåt:5mÚ4ã¨IùòË/5cÆ Mš4IÑÑÑÆ±× P̪>)¾aL/ͯíªaaar:�Æ7…*àÅž|òIc„ˆB¸A>ÿüs­_¿^Ç�—ËåRxx¸RRRtÇw¸×üõ¯•ÓéÔO<áÎN�:¥÷Þ{OйsçÔ±cGÝsÏ=Š‹‹s¯Y°`�:v쨨¨(­^½ZPhh¨F�­èèh­ZµJ™™™*--U§N�ôè£�*22RÇŽÓ›o¾©ääd%''»¯'I;wîÔÚµkõÔSO)<<\[¶lÑG}¤cÇŽÉÏÏO�:uÒƒ>¨.]º¸÷8�N­Y³FÙÙÙ:zô¨BCC5lØ0•••iûöíš2eJƒßÁûñR*àøä“O´hÑ"Y,Ý{ï½:t¨ÊÊÊ4oÞ<º×¨  Àýu~~¾&Ož¬;v(&&FƒÖéÓ§5wî\}ôÑGîuGŽQff¦æÎ�+‡Ã¡ûî»O¥¥¥Z°`�æÏŸ¯�7ªgÏžJNNV^^žæÌ™#—Ë¥°°0�>}Z7nt_«ÞæÍ›UZZª°°0effjáÂ…²Ûí5j” ¤üü|Íž=[%%%î=üã•––¦[n¹E)))ŠˆˆÐ¢E‹´~ýz}ýõ× ®Þ8pB¸ÒÓÓ¤ßýîwòõ­«ébccõâ‹/jÿþý 5쨳lÙ2�?^3fÌPxx¸$)%%E³fÍÒÊ•+Õ¿J’<¨)S¦¨G�’¤®]»jöìÙÚ³g�^{í5ÙívIR³fÍ´fÍ+$$DIIIZµj•ŠŠŠ"IªªªRNNކ "‹Å¢¬¬,ÙívM™2E>>uç6ûõë§I“&i×®]4h�rrr´cÇ :TcÇŽU½øøxÍ›7O6›Í�5œPn€Ÿþô§zá…Üeª$UTTH’ÊËËÝYC¥¥¥ÊÍÍÕÝwßí.S%Éf³i̘1:þ¼¶nÝêλvíê.S%)""B’Ô·o_w™Ú0¯ÿ}(‹Å¢ŒŒ ÷šœœUUUiàÀ�’¤àà`UUU)==]µµµ’¤ÈÈHýùÏÖ€$I[¶l‘¯¯¯†TW¨vêÔÉ#k,8¡ ܱ±±:yò¤ÒÓÓuäÈåççëСCÆeNœ8!I-#;vì(‹Åâ^#É}Rµ^}yæ‘[­V�¯ƒƒƒuÛm·)##Ã]†fee©K—.?þñ�•��­%K–håÊ•êÞ½»¢££•�� IRQQ‘‡š7oî¾v½Î�;{<Ê ±à„*p¬Y³FÏ?ÿ¼þþ÷¿Kª»]~âĉ†Už*++%Éãti=›Í&UUUG?HRR’¾ù檦¦FÙÙÙJLLtχfΜ©çž{NwÜq‡òóóõöÛoë¹çžÓž={$IÕÕÕý¬’äççgŒN¨×YQQ‘RSS5pà@=ýôÓ²X,’¤3gÎVzªžiÃS¨õNž<©ÚÚZ÷š+Õ§O(##C:tPuuµÜóÚÚZY­VÅÇÇ+>>^’´k×.Í™3G+V¬ÐŒ3Ô¦Måææº×6t±‡Æ€ªÀuvìØ1IR¯^½Üeª$åææºÿùbÚ¶m+‡Ã¡M›6©¦¦Æc¶~ýzIRtt´GþCùùù)!!Aúâ‹/Ô«W/µhÑÂ=Ÿ}ÔµkW�=W"))I›6mRqq±Æ�ï1‹‹‹Óû￯+V¨_¿~òóóÓÞ½{uàÀ÷K©õÁhñâÅ:wuë¦ââb½ûZ­r:�×l (T³ZÄH …ÆôÒ¬ž…h½°°0=ôÐCZ½zµ¦N�*IŠŒŒÔäÉ“µtéRmÞ¼Y]»vÕ]wÝeØ)õïß_v»]ûÛß4kÖ,IR³fÍtÿý÷k̘1†ÕW¦K—. ×·ß~«^½zyÌF�¥ŠŠ mذA~ø¡¤ºç¸&$$è±Ç“TWO›6Mo¼ñ†–/_®ššÙl6Ý}÷ݲX,Ú´iSÃK6 –ýû÷»$)**Ê87ȸÅ{�Q£0?œ1j>îþkcäõƆ?bŒ‡$—1¸©UUU©¤¤D��� �$9�N•””(88ØãqsöìYUTT˜ZûC¸\.ýò—¿TïÞ½õøã�Ç’ê^Æpqª¸)œ={V¿ùÍo´zõjã0�B7…Å‹ëØ±cª¬¬4ŽÓ(TÑä}öÙg:yò¤Zµje—…BMZqq±–-[¦gžyFV«Õ8. …*š,§Ó©×_]÷߿ڵkg—Í×MÅÚµkU]]­¡C‡G?H^^ž1—‰Ÿ§àZˆŠŠ2FÀ5à U4IÔ|À­þ¸ª8¡Š&§ªªJ¯¿þºRRR®ê­þüm€ëê³½Æhøy €ÆŽBMNzzºNž<©ÊÊJ¥¦¦ºóŠŠ íß¿_©©©8p BBBì¾…*š—Ë¥fÍšé“O>ñÈ«ªªôÍ7ßèĉêÙ³'…*.…*šœ¡C‡^ôETãÇ�×€4vìXã0…—R€Iª`·ü㦱páBc\N¨€Iª`…*˜D¡ &Q¨€Iª`…*˜D¡ &Q¨€Iª`…*˜äk  )Z¾|¹1jÆŽkŒÀ Ä U0‰BLâ–ðfŸZŒ‰Äûa IDAT÷Krš N¨€IœPpYJ§ö6F�C÷_€ËÆ U0‰BL¢P“(TÀ$ U0‰BLò5®Ÿq‹÷#¯7ßÜD(Tn´O-ƤqHr Éã–0‰BLâ–pQ¥S{#¯4=ÛÀUE¡ šŒåË—£Fal¸1à­¸åL¢P“(TÀ$ U0‰BL¢P“(TÀ$_c®®q‹÷£Fa¾1pBÌ¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BLò5@SQPP >ø@GŽ‘¯¯¯:w”—¦pBMÒ®]»4eÊ;vLñññêÙ³§²³³õÛßþVG�5.L¡PE“ãt:õ—¿üE;vÔK/½¤Ñ£Gë‘GÑÌ™3%I©©©†€9ªhrNŸ>­3gÎèÎ;ï”Õjuç���Šˆˆà„*~0 U49þþþ?~¼bcc=r§Ó©¢¢"9�0‹—R¡ÉiÞ¼¹ `ŒµnÝ:•••©ÿþÆ` …*š<—Ë¥Õ«W+--M}úôQbb¢q‰)yyyÆ\&~ž7¾×�›‡·|¿GEE#àš¡PE“V\\¬×_]yyyºçž{ôè£�Êb±—¦P¨¢Éúâ‹/´hÑ"µlÙR'NÔí·ßn\rYøÛ.×Õg{� Ð$ðóô"ø~GÄ÷úEð½Ž&ŠïwÜŒ(TÑ$åäähþüùêÝ»·žyæ5kÖ̸¸lªhrjkkµdÉuëÖM¿øÅ/dµZ�K€„BMξ}ûtâÄ Ý~ûíÊÌÌ4ŽÕªU+õìÙÓß‹BMΡC‡$I6lІ  Óºç»P¨à‡ PE“ó“ŸüD?ùÉOŒ1pÅ|Œàâ(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BL¢P“(TÀ$ U0‰BLò5¼Û¸Å{�Q£ðΓ=Œ@£Ã U0‰BL¢P“(TÀ$ U0‰BLò5€:Ë—/7F�ÂØ±c�¸J8¡ &Q¨€IÜòMͧcÒ8$¹Œ ^‡ª`…*˜D¡ &Q¨€I¼” ÀuQ:µ·1ò~ÝmLÀMŽª`…*˜D¡ &Q¨€Iª`…*˜D¡ &Q¨€Iª`…*˜D¡ &Q¨€Iª`…*˜D¡ &Q¨€Iª`…*˜D¡ &ù ©(..VZZšŽ9"IêСƒFŽ©��Ï…€IœPE“TPP ßþö·úꫯ«ØØX}õÕWš8q¢òóó�ËS(TÑ$-]ºT~~~š9s¦~øa=üðÚ9s¦ìv»þö·¿—¦P¨¢É©ªªÒÞ½{Õ§OµhÑÂ�·hÑB}úôÑW_}¥ÊÊÊ;s(TÑäœ:uJN§SaaaÆ‘ÂÂÂär¹têÔ)ãø^ªhr***$I†É?³òòrÃø~¾Æhìjjj¤ÿÇÞ½ÇUUå�ÿwQ@n Šš‚wE B ÓRÓI“Èl¦t¬&sú6Ž­)­,25µ)5ËÒÑrÆòF¨¥fyK0H@@M¯*"Ê�Ãùýq~gÇ9Ü6HŠú~>=±ö{¯³÷v¿Ï^gíµ×4�Ù’ßËt:�Ù’ú¥¥¥™Ýï ¾3Óö Ì‹š½ûÍ îi¤šÝšIŽ5’ë·Ö�˜ï’ëw�;1ß%×o­;2ß%׫¹sîÌ|—\¿ÅšI¾ûúúš ñ‡‘ªâ®ciih¨;V«2–iµZ³%uk.�©B!„B!„¨N~·‹[éμE&DZ´hÀõë×Í–ü^VõeU !w¼„¸»a’ëBÜÝ$×…¸wH¾ qo�ÎTq«ÉUq×qqqA«Õ’™™i¾ˆ¬¬,,,,pqq1_$„B!„B!D½¤CUÜu¬­­ñññ!>>žk×®)å>|lllª¬!„B!„B!„:Ò¡*îJcÆŒ¡¤¤„Å‹ó믿røða/^Lqq1cÆŒ1B!„B!„B™CUÜ•úõëÇäÉ“Y¿~=‹/ s«¾øâ‹ôïßß,Z!„B!„Bu¤CUܵ†ÊСCÉÍÍ s« !„B!„Bq3¤CUÜõ¤#U!„B!„B4™CUqW˜3gaaaæÅ͹sç8wîœy±â69vìÏ<ó )))拚„ä¼ÍSQQÏ<ó Û¶m3_T/Ék!n�µk×2iÒ$óâ:Ý̵]ò[ÑÒ¡*„¸+xzzâééi^Ü,ü÷¿ÿåË/¿4/BÜ¥$ç…¸ûH^ q÷’üB4†<ò/„¸+¼øâ‹æEB!„B!„MN:T…w…¯¾úŠÊÊJþö·¿°lÙ2:uêD·nÝØ¼y3iii´jÕŠàà`BBBÐh4äçç³qãF’’’(,,¤uëÖ 8�ÐÐP´Z­R^^ëÖ­ãĉhµZzöìÉØ±cñðð >>žmÛ¶ñÖ[oMtt4\¸p�ŒŒ ôz=aaaôíÛ—'žxB©W¡^tt4?üð™™™X[[sß}÷1nÜ8ºtéb·oß>öïßOVV;v$((ˆaÆ™ÄÔDÍz/^dÓ¦M¤§§S\\Œ··7�?þ8~~~;vŒððpÉy!àèÑ£|ûí·¼üòËÊ5 ;;›åË—JŸ>}HKKcݺuLš4‰èèh)(( sçÎ<öØcôîÝ»J­†Gx¿ýö[Nžù$�<òçÏŸgÁ‚J‚á±½Õ«WãææÆèÑ£±´´äË/¿dË–-Uj«NÍz§OŸföìÙœ8q‚~ýú1tèPrssùðÃ9|ø0ÖÖÖ’óB4PAAiii”––š”—––’––Æõë×(,,$--�/¾ø‚˜˜üüü:t(—.]bÑ¢E:tHY÷ĉ„……qêÔ)ÈСCIOOgÑ¢EJŒÑ¾}ûøüóÏÑh4<ú裌=š‚‚–,YÂÅ‹ëÍk5ßBˆš-^¼˜ˆˆ ¡]»v|þùç$$$˜Ä©i‹×¤¾ü¬+¿û™Bˆ{‡ŒPBܵ~ûí7^{í5ú÷ïÀã�?ÎË/¿LLL ?ü0/^$##ƒ¿üå/Œ3€Ñ£GóÑGqðàA&L˜À×_���|ð¶¶¶Œ9’™3gÁK/½dø@àÿû3fÌÀßß_)›3g:�Ž©S§*eBˆ†‰‹‹ÃÆÆ†·ß~ Ãýà   f͚ő#Gxä‘G8uê»víâùçŸ7¶nÝ:¾ýö[‚ƒƒquuUÊ�Ô®·aÃ,,,˜3gnnn<ñÄL›6�-[¶0oÞ<|||$ç…ø1wî\œœœÃµ=,,Œo¾ù†€€,--Y¿~=666Ì™3GÉùÐÐPæÍ›ÇåË—«VÇž={hݺ5ï¾û.––†ŸF~~~Ìž=›ÔÔT† Rk^«ýîBT—œœL||<£G�æ™gžQÊY²d VVVJYCÚâFjòÓÇǧÖünÌg !î-2BUq×òòòR:SlllèÞ½;W¯^ÀÙÙ­VKLLŒÉ¬üã,X°0L päÈFŽ©4¦\\\2d111”——+åÁÁÁ&�©Bˆ¦áêêJii){öìA§ÓСC¾øâ  Àþýûqppà‘G©º*!!!€aÊ€š¨Y///�£G�òÐC)�©ööö̘1ƒáÇ£×ë•r!ÄcĈJg*€ƒƒ�?þ¸’£ÙÙÙœåÅ�n&?û™Bˆ{‹ŒPBܵÜÝÝÍ‹°²²¢¢¢;;;Æ�ǦM›˜>}:ÞÞÞôèÑzöì ÷صk¿üò‹RS,))!//O)óóó«!„h*Æ #!!�5kÖ°iÓ&ºuëFïÞ½0`€òc'++‹²²2Þyç³µÁ‚ .˜êÖ»té€É�<£îݻӽ{wób!Ä sçÎæEJÙ¥K—”ùÏ;uêT5¨y]???rrrسggÏžåüùóœ>}Ú<¬Fj¾;„5»|ù2nnn´hÑÂ|�;wV¦òjh[ÜèfòSígÊ\ªBÜÛ¤CUqת:Ú¤6cÇŽ% €¸¸8RRRØ»w/?þø#ýû÷gúôéÊœnÎÎÎ8;;›¬Û¦M|}}•\�!NÑôÜÜܘ?>‰‰‰$$$püøqظq#3fÌ W¯^”––beeE›6mÌW§M›6tèÐÁ¼0ÌÕXßzÆk666fBˆ[©êcÀFÆë}ii©rÓÔÚÚºjH­e[¶l!""ºwïNPPãÇ�gþüùæ¡Õ¨ùîBÔ¬¼¼¼ÖkjÕ\mh[Üèfò³±Ÿ)„¸·ÔßÛ „w)½^Oee%žžž„„„BAA«V­âðáÃ$%%áååÀàÁƒúè#Ö¯_ϼyóhÛ¶-ׯ_ç•W^1[ÊÊÊ”¹WÍ©Y/330¼yÜ\bb"'OžäÉ'ŸTFÇ !Ô1vJ˜O™‘ŸŸoò·Ñ¥K—ðññ1)3NÛãêêª<�rñâŪ!€aݪ._¾Lxx8ƒfÊ”)ʶÔöÙæÔ|w!jæîîNJJŠr}¯ªj®zzzõ·ÅsrrL–ÝL~ªýL!Ľ­öo!„¸Ë>|˜‰'’––¦”µjÕŠ‘#GpýúuÜÜÜpww¯ö¸À¢E‹˜1cFµ�æ4M½1Bˆº½ùæ›,\¸Ð¤ÌÏÏ___å-à½{÷&''‡ÔÔT“¸´´4ž{î9"##MÊ�Ô¬çééIË–-ùå—_LòY§Ó±zõjâãã•„’óB¨×²eKΜ9cRþ믿šüm´wïÞjù‰V«¥k×®´iÓ777öï̷߯l´wï^“¿�7Júõëg2Ú,%%Eù£šòZÍw‡¢f}ûö¥¨¨ˆƒš”_¹r…¤¤$åïÆ¶Å’ŸæùÝØÏBÜ[´S§N ÃËBˆšåææ’'ÍYdd$z½ž!C††9�ZµjÅ€Lâbbb¸ví#GޤU«VìÛ·�ãÇ�+ÿ¶™™™lÙ²…ÜÜ\ž}öYhÙ²%;vìàÂ… 888põêUvîÜItt4!!!tëÖ�3gÎ���À¨Q£ªÍuøðaΞ=K«V­¨¨¨ uëÖ&ËEó!¹Þ|]½z•¨¨(ÊÊÊppp ¨¨ˆØØXöïßÏý÷ß�¿¿?íÚµSÊÐh4$''³~ýz´Z-Ï?ÿ<ÖÖÖäääpàÀ‚ƒƒñððPµž��Z­–ÈÈH233qttäüùólذ�Ó§Oóì³Ï*�JÎ7’ë͇½½=»ví"##ƒV­ZQVVÆO?ýDbb"………âííÍ¥K—8xð œ={ ضm‘‘‘<üðÃ<øàƒXXXТE ~þùgNž<‰££#ׯ_ç‡~ 22�NGïÞ½éÚµ+z½ž]»vqíÚ5¼¼¼ÈÏÏ'::šˆˆÊÊÊhݺ5]»vÅÚںƼVóÝQÓ4âÖ’|ožÚ·oÏ¡C‡ˆŽŽV®±iii|þù甕•¡×ëyâ‰'Ðh4ªÚâ�¹¶óÓ<¿]\\T}¦h^$×Å­b<×äÙT!Ä=«eË–¼þúë|õÕW|ôÑGJ¹§§'3fÌP4h|óÍ7ÄÆÆ¢Ñhèܹ3&L`Ô¨QÊzµyä‘G8sæ ÿùÏxðÁéÒ¥‹yˆ¢O>ù$ÅÅÅìÚµ‹mÛ¶†¹ À¤I“Ðjµ¼ûZµŠ5kÖP^^NË–-ñóócôèÑ888T­R¡v½?ýéOX[[Nll,`è úë_ÿJpp°RŸä¼ê988ðÒK/±jÕ*–-[†F£ÁÇLJW_}•×_Ý<œ^x�½{÷òÁ†LŽ3†§Ÿ~Z‰»²²’ÜÜ\Õ9®ö»CÜz’ïÍ_yy9¹¹¹¸¹¹U›OÕ\cÚâ7›Ÿ�ùLqëI®‹[Åx®ÉU!„ÀÐ@RÓHR#„øãXYYáááa^\�V«UgNízò¿MËÂÂBUîÕuó³*5¹jcc£¼„ÆÈÂÂ777“²º¨ýîBTgeeE›6mÌ‹kÔ˜¶øÍægc>Sq÷“—R !„B!„B!„JÒ¡*„B!„hÖÜÝÝ=z´ª§B!„4yä_!„BѬyyyñÌ3Ϙ !„BÜ2BU!„B!„B!T’ª·€ñ `âÎ'ÿ–BÜ$×…¸7H® qï�|BÑ”d„êL.ÜB!„B!„BÜ=d„ê-âëëk^$î ÆŽqùwâî&¹.ĽAr]ˆ{‡ä»÷Ì&n5¡*„B!„B!„*I‡ªB!„B!„B¨$ªB!„B!„B¡’t¨ !„B!„B!„JÒ¡*„B!„B!„*YšÑÔŽ;Ƽyó˜5k½{÷6_\«9sæPYYIXXX�7µsçÎСC³%¢±öíÛÇO?ýÄÂ… ÍÕH¯×³wï^bccÉÎÎÆÝÝ�€€†ŽF£QâÊËËÙºu+ÇŽ#??www†JPPP£â***سg111\½zwwwüýý1b¿ßwR[ÀéÓ§‰ŽŽæøñãèõzzôèAhh( Žkêã"DSùâ‹/(,,4/fðàÁ˜×H§Ó1sæLÆŒÃàÁƒÍ+ê‹ËÈÈàûï¿çìÙ³XZZÒ¹sgBBBpuu °°�/¾øÂl-SÏ?ÿ<­[· ß {÷î娱cœ?wwwF�E¿~ýLÖQ—žžÎŽ;8þh²®Ú8sÛ·ogÇŽ,]ºkkk¥¼¼¼œï¿ÿž£G�’ŸŸOÛ¶m1b~~~UÖ6UW»%99™Ý»w›ciiÉ´iÓ”¿Õ^³Õî¯Ú6ŠMAíyYµ9§6GÔžû�ÝæÚ¾;ÔæºÚíS»¿�Ý!Äí£�:uj€‹‹‹Ù"Ñrss�{ûøæääpàÀ‚ƒƒñðð0_\«ÈÈHôz=C† àäÉ“888àïïoØD>ùä:t¨ù"ùwl„sçÎñÕW_Q\\ÌèÑ£Í×hÅŠlݺ•N�:áççGqq1;vì ''GéœÑétÌ™3‡„„úöíË}÷ÝÇŋٹs'z½žž={6(N¯×³dÉvïÞM÷îÝéÕ«×®]c÷îÝdddÔ úÀÐ[°`ôîÝ+++:Ĺsç8p`ƒãšò¸ˆºI®«WTTIJeËpssÃÑÑkkkå¿:ЦMóUªÑétlܸ‘øøxúôéÃ}÷ÝgÔwäÈæÏŸÀý÷ß�‹‹ qqqüøã�øûûÓªU+ÊËË9|ø°ÉzF.\àÌ™3Œ9;;;ÊËËY¼x1 ]»vtíÚ• .°cÇú÷ï�³³3`øñ¨&nß¾}|üñÇ´jÕ Z´hÁ�ˆŠŠbðàÁʹ´´4Þ{ï=JKK0`NNN=z”;vзo_¥>£úŽ‹¨�äºz7s}©¬¬äƒ> **ŠîݻӳgOòóóÙ¶myyyJ›Nmœ¹3gÎðé§ŸR\\ÌØ±c±´4Œ)..föìÙ¤¤¤Ð·o_|||¸té[·nÅÎγšêo·ìÚµ‹#GŽÐ¡C“ï;;;;“(j®Ùj÷WmEÔMò]µçeM’sjrDí¹ßØm®í»ÔåºÚíuûÛØý¦$×Å­b<×d„êm´óh.Û“ ÿuñïÐ’lk^|ÏyñÅÍ‹D3´xñb²²²ÈÌÌÀÑÑÑ,¢f©©©DEE1nÜ8BCC•ò-[¶ÎСCéÖ­QQQ¤§§óþûïÓ¹sgÀШyóÍ7Ù±c¡¡¡hµZÕqçÎ�#>>žñãÇ¢|îŠ+ˆŒŒäÂ… ´k×Nu}¥¥¥,Y²„   ¦L™¢ÜQ>xð Ÿ~ú)ééétéÒEu\S!šÊùóçèΆÜ,ˆ��gûöíœ;wŽ’’óÅ 5q•••¬\¹’N�:1{ölå<ì±ÇxõÕW gúôé´hÑ‚W_}ÕlmC'Êûï¿ÏË/¿¬ŒNݾ};'NœàÝwßUžZ7n3gÎ$<<œ×_]u\~~>kÖ¬!((ˆ©S§>9r$³fÍbãÆ�¼ð lÛ¶ æÍ›‡­­­÷ꫯ²mÛ6edŒšã"DS¹™ë‹qôö¿þõ/“NÇuëÖ±}ûvF�EûöíUÇUUVVÆòåËéÝ»7GŽ1YÎÅ‹™?¾²^h÷Î} IDAThh(«V­bÓ¦Màææ¨o·œ?ž>}ú0eÊóE µ×lµû«¶�"DSP{^ÖDmΩ͵ç~c¶¹®ïP—ëj·Oíþ6f?„·Ÿ<'r•–ë¹V\QïEe:“õ–-[ÆöíÛIKKcÁ‚¼øâ‹Ìž=›äädôz=ááá¼öÚk¼ð Ì�;Wy”=33“°°0öïßoR@bb"aaaJc2::š·Þz‹çž{Ž—^z‰ �žžn²Nee%¼õÖ[Lš4‰™3grðàA~üñGæÎ�kkN¯×³cÇÞyçžþy¦M›ÆÚµk¹qã†y¨â«¯¾bõêÕ&eyyy|úé§üãÿ`Ú´i¬\¹’Ë—/+Ë�=JXX¬_¿žýë_¼øâ‹ÌŸ?_Ù×cÇŽƹsçÈÈÈ ,,Œ-[¶(uˆ†ñ÷÷gÔ¨Q¼øâ‹øúúš/®UBB@µÂÆ¿�Ë322ðôôT~Ôh4zôè�N§£¼¼¼AqFæw2�?´Œw¬ÕÖ÷믿RRR“O>iòxÎÀ�Y»v­²¾Ú¸¦>.B4•óçÏcoo�»»»ù¢zc›0aO?ý´ùb…š¸«W¯’ŸŸÏƒ>hÒ©ãääD»ví¸páB•hSW®\aáÂ…„„„0`À¥<**ŠÀÀ@“)`´Z-‹-2é”UwæÌ***LF�´iÓ†®]»oRngg§t¦‚¡sÇÊÊÊdôŒšã"DS¹™ëË™3gptt¬6F¯^½ÃèpPWÕºuëèØ±#÷ß¿ù"RSSi×®]µˆ‡zˆ²²2RRR”2µí–sçÎѱcGóbj¯Ù ÝßúÚ(B4…†ž—U©Í9µ9bT߹ߘm®ë»ÔåºQ}Û§v³BˆÛO®Âw ³gÏ’��ÍöíÛ ÀÇLJ}ûö±lÙ2zõêűcÇ8p ìÙ³‡�>úˆ¥K—âééÉÕ«WÙ½{·ò½ÑþýûÉËËÃÓÓ“ØØX>ûì3ºuëÆ“O>IQQ‘‘‘,X°€… *ŽÅ‹�ŸŸ÷ß?™™™|þù縺º’——gR¿¹åË—M¯^½=z4§N�b×®]œµûÛÐýB4Õ¿õÄáÔ©S¼ýöÛôèÑ,XÀÑ£GùôÓO±±± #]¶lÙBvv6<ôÐC|ûí·\¾|YyD³´´”äädFމF£!..Þ~ûmeBí   f͚ő#Gxä‘GHNN&>>žÑ£GóÌ3Ï6  dÉ’%XYY)eæRSS‰ŽŽæOú&LPÊ#""ˆˆˆ %%¥Úݹš|ýõרÙÙñÁ˜<9sæL"""x饗”Øk×®1wî\e»<== '==�®]»âããÜ9sÐét&�dŠ[§¨¨¨ÖN{{{ŠŠŠÌ‹‰ŽŽ&..Ž£G�âííÍ+¯¼bÔ§Ñh˜={6sçÎeêÔ©¸»»sþüy<==™5kV•Z~WW}ׯ_ÇÙÙ™¯¾úн{÷âââB~~>ß~û-£F�bâĉ Šû#�‹7ãüùóœ={–ÌÌLºvíÊÕ«Wùî»ïˆŒŒdîܹÊãó´-Z0hÐ ób¶oßNAAAµ‘¡FéééÄÅÅ1eÊ“Î΂‚nܸÁ[o½EYY¶¶¶äçç³yóf^ýuÚ·o¯:®m[Ô=ÉÉÉ&?ÆŠ‹‹9yò$z½ž«W¯âêêJpp0ùùù,[¶ŒˆˆŠŠŠÈÏÏç¹çž«ñ‡–·ZC¯/Uo]¿~�m۶Ѷm[:uê¨�3–¯\¹’^x¡ÖàmÛ¶%>>žüü|œœœ”òäädÀÐiÒÆ)NV¯^�··7ÎÎΤ¤¤Mhh(ãÆ�Ô_³ÕîocÚ(B4–Úó²&jsNmލ=÷²Íj¾;ÔæºÚíS»¿ Ù!Dó!�üß¡|||”ÎT@™?é�P:S«–¿¬ŒF£!&&F‰INN¦´´T¹CïêêJii){öìQFÖtèÐ�/¾øBùÑ�¥¥%cÇŽUêC‡j}/ňŒŒÄÒÒÒd¾0Ìw7yòd“‹pmòóó9rä#GŽ4áââÂ�!Cˆ‰‰1ymÔ¨Q&�¼}ûö �ŠŠæA§ÓÕú¶Z�F£œ‹USVV†�� /^䨱cæ!@ýq±±±äää`gg‡££#ÎÎÎ\ºt‰¨¨(ôz½I,Ô]_AA§N�âØ±c|üñÇ|òÉ'üç?ÿ! €�;w* KµqäqâfTVVÈ'Ÿ|ÂôéÓ™3gÓ¦M#??Ÿo¾ùÆ<ü–ÑëõDDD°aÃ6`Æ ¸ººòàƒš”;Jׯ_ÏÈ‘#Y½z5Ë—/gÖ¬YܸqC™zFmœ——=zô`÷îÝìß¿Ÿòòreº#c_¹r…˜˜´Z-¸»»£Õj‰ŒŒäÒ¥KJ¼·ËÍ^_ŒÓOV»™QU]q+W®¤gÏžµ>® 0lØ0ÊËËYºt)999TTTpèÐ!vî܉µµu�×κжm[^{í5Þÿ}f̘ÁÇŒ¯¯/ß}÷�ò8nc®ÙP÷þ6´�"DS©ë¼4§6ç’#�9÷ëÚf5ßjsÔm_Cö·ªºöCÑ|ÈÕ;”y§£ñ OOO“ró/_WWWzöìILLŒÒG—.]”Q4Æ #!!�5kÖ°iÓ&ºuëFïÞ½0`€ÒyyùòeÜÜÜhÑ¢…R·QçÎ�ÉÈÈ0/V×5¿3hkk[m~™Údee†·0þòË/&Ë ())1}`þÂcçjm1qëiµZ***Ì‹¨¨¨¨v.ƒá\56àV®\ÉÒ¥K™3g]ºtQÏÚµk6l“&MB«Õ¢ÓéøòË/Y¿~=®®®&s+ÖWŸqý‰'*ç��� “'OæÈ‘#8p€>}ú4(î�:.BÜŒyóæ™ñÀЯ_?ÍÝÙÙÙ,_¾œ´´4FŒÁĉMæ(®wüøqÆŒS-‡Œ×ÓŽ;šÜøëÝ»7Çgûöí\¾|Yuœ‡‡“'Oæã�?fåÊ•¬^½�N‡¯¯/£F�"<<œV­ZðñÇ“““üyóðöö~iÖ¿ÿýoþýï+Ÿ#ÄíÐØë‹^¯ç§Ÿ~bÆ ´jÕŠ·Þz«Æuê‹ûùçŸ9uê‹-ª²Vu=zôàé§Ÿ&<<œiÓ¦¡Õj©¬¬äÿþïÿøüóÏ•œSkðàÁ&S€atÙ¤I“xë­·HJJ¢]»v ¾f×·¿�i£q³ê;/k¢6çÔæHCÏýú¶Yíw‡Ú\W»}j÷ר¾ýB4/Ò¡zz衇øì³Ï¸xñ"nnn$$$ðç?ÿYYîææÆüùóILL$!!�ãÇ�“��ÀÆ�™1c½zõ¢¼¼Üd$lUÖÖÖæE&ŠŠŠêœoK�ÒÒRœ��«ÍIÓ¦M|}}M~D›_¬Dóãàà@aa!z½ÞäßN¯×SXX¨tÀ`mmmrYYYñì³ÏMrr2]ºtQ—””„••'NTέVËsÏ=Ç/¿ü±cÇ0`€êúŒ7ÌçstppÀÃÃCe¦6®©�‹´N�:‘˜˜H^^Þ-{ì /zûüóÏiÙ²%3gΤOŸ>æ!Šƒ†`sµå&n‚aN3ã›Àë‹óððÀÃùsçrâÄ 233騱#>>>¬_¿+++ììì¸víçÎ�#44TéLÃ"£F�"""â–S!àæ¯/%%%,Y²„””~øaþò—¿Ôø¬š¸µk×rß}÷±sçN¥ììÙ³€á­ÙÊÍù±cÇDZZ•••ôíÛ[[[JJJÜ¡Zooo4�r£_í5Ôí¯Ú6ŠMEÍyY59§6Grî«Ùæ†|wÔÄ<×ÕnŸÚýuû!„h^¤Cõ€½½=111tìØ‘òòr“ƘN÷ûË1Œ/Á8rä}ôëׯgÞ¼y¸»»“’’¢ÄVUßc‰nnnœ8q¢Ú…¥´´”ï¾ûŽnݺÕùC~‰[Ó]D�NGEE666¤§§›,Í—»»;åååÊ|¿F999”••)oŸ={6ÎÎ΄……)1-[¶D«ÕråÊ•Ååää`mm]íq+++Z¶lÙàú¼½½ILL¤´´;;;“ØÒÒR¥Fm\S!šBFF‘‘‘<úè£Õ^”pýúuÀ0¢ãVINNæ“O>¡ÿþüýﯖSæ:„««k�SÔ´mÛKKKåÆ]Ueee€á‰ µq:�ެ¬,œœœèÝ»7½{÷VâNž<©tžæää ×ëk¼Yi|¤t¨ŠÛáf®/z½ž?ü�³gÏò¯ýËdáªÔÆY[[“‘‘aò$”qЧ}ûöÑ¥K†ÊÕ«W)**ÂËËËäÚiœN§êM 5vìØ��� Æ 3)7v”¿ïÔ^³Õî¯Ú6ŠMAíyYµ9§6GÔžûj·Yíw‡Ú\W»}j÷Wí~!š—š'ôw5kkk @LL ¿þú+ýúõ3¹;öæ›o²páÂ*k€ŸŸ¾¾¾Êå¾}ûRTT¤Œò1ºrå III&eæ|}}¹qãFµGB8ÀÖ­[)))1)¯‰››îîîÕ÷X´h3fÌ@¯¯yn�Úh4š¯#¯¼¼\iÈôêÕ 04jª2þݳgOÀ0áÉ“'«½P"99�NGǎ׾}{ 9|ø°I\FFyyyJPm}ýû÷ SiTuæÌrrr”QcéÒ¥<òÈ#XYYGJJ þóŸkœ7¯.öööœ8q‚ýû÷Ó®]»Zl‹›§×ë™4i�FyyM÷îÝéÒ¥ ?þø#ÖÖÖôéÓ‡äädvîÜIçÎ�•GHHIII,Y²„qãÆáääDjj*ááḹ¹)/˜Q7lØ0öîÝËÚµk¹páýúõ#;;›õë×cooψ#T_—.]ð÷÷gݺuèt:zôèÁåË—ùú믱··gÔ¨Q Škêã"DShÓ¦ ~~~|ÿý÷XZZÒ¯_? Ù²e W®\á�7ÞPb·lÙÂæÍ›yê©§xâ‰'ªÔÒ4Nœ8Á¥K—èÓ§±±±æ‹iÕª•òƒ spëõújsŽWʼyóX¸p!!!!ØÙÙqàÀâãã;v¬2BEmÜÀ�Ù³gÿûßÿ R^Üåìì¬\Wmmm>|8»víâã�?fРA¸¸¸pàÀâââ5jÔMO—#Dc¨½¾Ôtmß¿?NNNT» †ÎGGGÕqjÁ—_~ÉèÑ£ñòòâàÁƒ:tˆñãÇW{ºª>#FŒ 66–>ø€§žzŠÖ­[sôèQÂÃÃñññQ^r£öš­vÕ¶Q„h jÏËšr]mΩ͵ç¾ÚmVKm®«Ý>µûÛÔû!„¸5¤Cõ6 öq¤{Ûú‰liÛ°FŸ]ºtÁËË‹k×®UëÀ|òÉ')..f×®]lÛ¶ 0<¾0`À&Mšæˆ cÅŠ¬[·ŽŠŠ ¬¬¬xøá‡Ñh4ìÝ»·j•&lll˜3gŽÉË9ÀpïoûVV†FÕgРAXXXðÍ7ß‹F£¡sçÎL˜0AéŒjˆGy„3gÎðŸÿü‡|P:To1�FÃÿýßÿ±jÕ*6oÞLDD�†Þ½{3yòd¥ƒÜ××—éÓ§óõ×_³`ÁeÝþýûóì³Ï*jãÜÝÝ™9s&k×®%""‚ˆˆÀ�#ÿüç?quumP}�†©S§²jÕ*¶nÝÊÚµkÑjµtîÜ™Y³f)sH5$®)�‹MA£ÑðÊ+¯°nÝ:6oÞLxx8`x\¾¾ùK›ÚéÓ§ÃK wíÚe¶Ô�U;T333è»ÚøúúòÏþ“Í›7óÁ ×ëiÕªO?ý4�?þxƒã&Nœˆ^¯g÷îÝüøã�XXXЩS'¦N�jòéÙgŸÅÎÎŽŸ~úIõbkkËO<ñ‡tF ¡Fc¯/z½ž3gÎPZZʧŸ~j¾0<ÕªU+Uq éLpwwgÆŒ¬Y³†+V†) žzê)“—È©Uõ_jcccàAƒ˜0a‚r-VsÍV{\U·Q„¸Y 9/k¢6çÔ䈱¾úÎý›Ý暨Íu5Ûêö÷�Ø!Ä­¡IMMÕÃïÃ×EÓJKKšßñÕëõL›6�þýûóÜsÏ™/ �räååQYYIëÖ­kœ× q¹¹¹¸¹¹¡ÕjY»v-{öìá¿ÿý¯yh5åååäääкuë›ySPPF"ýšë¿ãݪ´´”ÜÜ\\\\j=ïnܸÁõë×quu­³#^m\II ¹¹¹´nݺÎdžÕÖ§×ëÉÎΦuëÖM×ÔÇET'¹ÞpäääàääTgÞÜ©ŠŠŠ())©wîR5qååå\¹r¥Îk*¾rssÑëõ¸ºº*?àDÓ‘\oœ;ñú’ŸŸOyyyµùž«  €ââbÜÝÝëÌMµ×lµÔ¶QDu’ï·–ÚœS›#·ëÜW›ëj·OíþŠÆ“\·Šñ\“ª÷¨¸¸8®\¹RçÛ ­¬¬L&Ï®�••mÚ´1/VÅÊʪÎÇ.Õú£:RÅíacc£ê¼ppp0™ÿ·6jãlmmëµf¤¶>�F£*‡ÔÆ5õq¢)XZZÒ¶m[ó⻆½½}­#ðªRgee¥êXi4y&š¥;ñúâääd^tSZµj¥ªÝ©öš­–Ú6Š·›ÚœS›#·ëÜW›ëj·Oíþ !îÒ¡z�9xð ±±±¤¤¤H‡ÌC„B!„B!„µ�Õ{L‹-¨¨¨`øðáØ|l=zô¸cB!„B!„¢!¤CõÓ·o_úöík^ܤüýýñ÷÷7/B!„B!„âŽga^ „B!„B!„¢f2Bõ1¾LÜÙäßQˆ{ƒäº÷Éu!î’ïB!š’ŒPB!„B!„B•d„ê-âëëk^$î Æ;Úòï(ÄÝMr]ˆ{ƒäº÷Éw!î 2 ]Üj2BU!„B!„B!T’U!„B!„B!„PI:T…B!„B!„B%éPB!„B!„B•¤CU!„B!„B!T²4/Bˆ›¡×ëÙ»w/±±±dggãîîN@@ÇG£Ñ˜‡Wsúôi¢££9~ü8z½ž=zŠƒƒCƒâvïÞMrr²É:UuíÚ•Ñ£G+×WŸ¹}ûöñÓO?±páBóETTT°gÏbbb¸zõ*îîîøûû3bÄ,,~¿�¥öX•——³uëVŽ;F~~>îîî :”   jÇ´¡û!ÄÍHOOgÇŽœ?;;;|}}yê©§°³³3­&##ƒï¿ÿž³gÏbiiIçÎ� ÁÕÕ€ÂÂB¾øâ ³µL=ÿüó´nÝÚ¼˜7nðÞ{ï1`ÀBCC�†×÷Å_PXXhƒ& À¼¨ùs�²³³‰ˆˆàìÙ³tìØ‘ÐÐP<<ýôSÒÓÓéÒ¥‹ê¸áÇ3|øðª�^¯gÅŠÜwß}<óÌ3€úÏX¼x1YYYdffàèèh¨¸ŠsçÎÏøñã QÊW¬XAdd$.\ ]»vª�UTTééé¼ÿþûtîÜ0ìÇ›o¾ÉŽ; E«Õ6h?„¸Yùùù¬Y³†   ¦N�ª”�9’Y³f±qãF^xá…*kü®²²’•+WÒ©S'fÏž�V«à±ÇãÕW_%<<œéÓ§Ó¢E ^}õU³µ 9öþûïóòË/×8 3**ŠœœZµjeRÞ�úŒ�œÏ?ÿ|�#H«ªísÖ®]‹µµ5óçÏÇÁÁ0 f̘1¼öÚküïÿãí·ß6[£öú²B4µ×¡ú”••±|ùrz÷îÍ‘#GÌ«ºÆVUW}jÛ`È÷>}ú0eÊ”*ÑÕ©Ù>µ×vP÷¹Muì…PãfÏ759RU]9 M[ŸÚvrxx8/^dþüù´o߀ÐÐPV­ZŦM›ÀÍÍMu¨ËuP'„h>¤CõuàÀ~úé'²²²ÐëõxyyÂý÷߯Ääçç³qãF’’’(,,¤uëÖ 8P¹~óÍ7œ9s†™3gbiiz*|üñÇxxxðì³Ï²lÙ2:uêD·nÝØ¼y3iii´jÕŠàà`BBBLÿ¸xñ"›6m"==�ââb¼½½yüñÇ•GŽ=Ê·ß~ËŒ3ؾ};ñññáççÇ‹/¾Èùóç '55{{{† Bhh(�FõöŠÛ'!!€¡C‡š”:”ððpjíPýõ×_)))áÉ'Ÿ49§È< <*¯6®&›6mâøñãÌ›7 aõùûûÓ·o_ÀÐè¼|ù²²Ìœ‹‹‹É߯F•ñÜU{¬222ðôôT¶�†=zpáÂÊËËÑjµ Ú!nÖ™3g¨¨¨`àÀ�&åmÚ´¡k×®ÄÇÇ×Ú¡zõêUòóó«ý0srr¢]»vuŽP½rå .$$$„˜/&;;›o¾ù†·ß~› ê¿Y[}çÏŸÇÞÞww÷*ѵ«ësKKK9~ü¸2RÍÈÁÁ�€€º²] IDATöíÛGII ¶¶¶Ê²ºê«Imû!DSP{ªÏºuëèØ±#Ý»w¯ÖÙ »ÆBýõ™«© †›ýúõ«Y35Û§öÚê>·©Ž½jÜìù¦&Gªª/‡›²>µíäÔÔTÚµk§t’=ôÐCìÛ·�””~øaÕq .×A}œ¢ù�Õ;о}ûXµj�;wæÑGÅÒÒ’ÈÈH–,Y‡~HÛ¶mÑëõ|ðÁäääððÃãääDjj*[¶l¡´´” &о}{vìØArr2ýû÷WêÏÈÈàðáüôÒKœ={–üü|¶oßN¿~ýxôÑG‰��%<<GGGåbqúôiæÏŸ�••ØØØLJ~ÈôéÓ¹ÿþû)(( --�O>ù„ÒÒRÈ… ˆŒŒ¤´´”””¼¼¼xì±ÇHLL$""† ¢z{Åí“““ƒ­­-ÎÎÎ&åNNNØÛÛ›:�®Æå¶¶¶¼òÊ+tíÚÕ¤¼²²’Ë—/Ó±cG“òª¾þúk¬­­5j”ù"*++Y¾|9úÓŸªýÀ©Mmõ�;wŽ:ðóÏ?Gee%�;wf̘1ØÛÛ›ÄÖ÷¹W®\¡²²OOOóExzz¢×ë¹rå íÚµ꯯&µí‡MAíu¨.GŽ!..ŽE‹g¾Pw�5RS_Uµµòòò¸qã¬\¹’¬¬,ÜÜÜ4hPµyÕlŸÚk»ÚÏmŠc/„Z7{¾©É#59Ü”õ©m'WVVV¸¿·wrssõqjs]mœ¢y©þ- š½={öкukÞ}÷]å‹ÜÏÏ�Ù³g“ššJÛ¶m¹xñ"üå/a̘1Œ=š�>úˆƒ2aÂxàÖ¬YCLLŒIe\\ÖÖÖ<ðÀJÙo¿ýÆk¯½¦Ä=þøã¼üòËÄÄÄ(ª6lÀ‚9sæ(£ñžxâ ¦M›Æ–-[LFÏjµZ”‹Î;ï¼CLL þóŸyüñÇøÓŸþÄßÿþwedOC¶WÜEEEÕ:Œìíí)**2/V\¿~ggg¾úê+öîÝ‹‹‹ ùùù|ûí·Œ5Љ'6(ÎÜúõëqww'88ؤ¼±õÕF£Ñ0{ölæÎ�ËÔ©Sqwwçüùóxzz2kÖ,%®1Ç*::š¸¸8Ž=Š··7¯¼ò⬩÷Cˆº´mÛ0ÌGVµ¡_\\ÌÉ“'Ñëõ\½zUyÁTU-Z´`РAæÅlß¾�‚‚‚j£^�ÒÓÓ‰‹‹cÊ”)5vÔ~÷Ýw”——›¼l®.uÕwþüyΞ=Kff&]»våêÕ«|÷ÝwDFF2wî\“GêëûÜââbÀ�׿ŒeUó½¾úÌÕµB4µº®Cµ¹~ý:+W®ä…^0¥ÝX�©¯¶6€qz�Õ«Wãíí�³³3)))DGGʸqãLâë£öÚÞ˜Ïm̱¢±þÈó­19\5õ©m'·mÛ–øøxòóóqrrRÖONN Ÿ ‰S›ëjã„Í‹t¨Þ�Æ�OëÖ­MjÎÎÎhµZbbb Tæ€ûÇ?þAii)666Kyy9VVV€¡ƒÒßßßä-Í^^^&�˜666tïÞ�ììlÀpÑ8zô(�=ö˜Ò™ †ÆãŒ3¸|ù²ÉÄà�=ö˜É¿öíÛsêÔ)|ðA¥ÌÖÖåå& Ù^q{ètºZ-×h4ʨ¶špöìY®_¿®LáPZZÊòåËÙ¹s'}ûö¥OŸ>ªãªúí·ßHNNfòäÉÕ:S_}bccÉÉÉÁÑÑGGGœ��¹téQQQŒ=Z9 =VÅÅÅ”••accÃÅ‹9vì˜r#á�Ø!jãååE�=ؽ{7ÞÞÞ 4ˆëׯ³zõj%¦¦s¸&z½žÍ›7A@@@µ£ 6àêêjr�0:uêßÿ=sæÌ©–㵩«¾ÊÊJ™2eŠòªØØXå…sÆycÕ|nEE`ÈksÆ2ã±RSŸ¹ºöCˆ¦V×u¨6+W®¤gÏž&7ÖoFC뫯 жm[ž{î9z÷î ÚÒ‹-â»ï¾#((H=®†Úk{c>·1Ç^ˆÆú#Ï·†æp}ÔÔ§¶�úè#¥ ƒ&Mš„N§#))©JtýÔ^Ûó¹ =öBÜŒ?ê|kL×Em}jÛÉ=zôàé§ŸæäÉ“L›6�矞O?ý”_| ¥m£6Nm®«�B4/2Bõ´eË"""ððð {÷î1~üxæÏŸo7vìXˆ‹‹#%%…½{÷òã�?Ò¿¦OŸŽV«Å××—6mÚpèÐ!üýý‰‹‹ÃÉÉÉä˪wlš3Ž�µ±ù}’ÿ?‚Úí·‡ƒƒ………èõz“N½^Oaaa­�á€á1` Ú¼Šxxx(�öjãŒÊËËùõ×_éÝ»w��á5´¾ú$%%aeeÅĉ•NZ­–çž{Ž_~ù…cÇŽ1`ÀÕǪ  kkk“y«¬¬¬xöÙg‰ŽŽ&99™.]º4ù~QæÎ�ˉ'ÈÌ̤cÇŽøøø°~ýz¬¬¬ê}jà×_åóÏ?§eË–Ìœ9³ÎÔ �šæöìÙCNN%%%„‡‡+åÅÅŤ¦¦ÎàÁƒ•PPw}uéÔ©‰‰‰äååqèÐ!UŸkÌÍëׯ+1FÆ2‡[ºB4„ÚëPMÖ®]Ë}÷ÝÇÎ�;•²³gφö¬‡‡Gµ8Õ¥¡õÕר�··7�†¬¬,óEuR{m¯�ùçÞ̱¢¡nÅùÖЮ�ÚúÒN;v,AAA¤¥¥QYYIß¾}±µµ¥¤¤Ääf±Ú¸š˜çzmÔÆ !n�º{ÉD³sùòeåGÕ”)S”ÆZ~~¾Iœ^¯W^‚BHH¬ZµŠÃ‡“””¤<ÂÿÐCñÝwßQVVF\\ƒ ªõq¥ÚçÉ3NPUbb"'OžäÉ'Ÿ4_Ô(M±½â�áîîNyy9ÙÙÙ&?úsrr(++«q¤³‘··7‰‰‰”––Vëˆ)--ÅÑѱAqFGŽ¡¤¤„ÀÀ@“r£†ÖWŸœœ¬­­«�“VVV´lÙ’+W®ê�ÕìÙ³qvv&,,L‰hÙ²%Z­V©¯©÷Cˆºèt:²²²”ZUoj�“òÒÒRÎ�;Ç¥K—èÕ«—IžÕU_FF‘‘‘<úè£&SØÀï öööª?·sçÎhµZ233Mâ²²²°°°ÀÅÅEu}j÷Cˆ¦¢ö:Tkkk222ÈÈÈPÊÊËËÃKV»téҠΓ†ÖW_`ÇŽØØØ0lØ0“rc§hC:aAýµ]íçÞ̱¢¡nÅùÖЮ�ÚúÔ¶“¯^½JQQ^^^&9l|:ÒØ¾Q§6×ÕÆ !šéP½Ã�õë×ÏäÎwJJŠòÿ‡fñâÅ„……áëë ¿9r$‡6)̦M›Ø³g§OŸæ¥—^R–©åééIË–-ùå—_xôÑGMæ…[½z5-Z´`üøñfk5NSl¯øcôêÕ‹­[·²oß>þò—¿(寂ž={*eÆÆŽq.ÜþýûóÝwßLj#”¸3gÎ�““£¼¨Fmœ‘ññ¤Úî¨7´¾ú´oßž¤¤$>l2j,##ƒ¼¼<¥>µÇÊËË‹¤¤$òòòL¦ÄHNNF§ÓѱcG é÷CˆºXXXðÞ{ïѱãÿ×Þ�ÇE]í� Ã&¢"*æ˜äN¸æÖUo–šV73½}óÚ}øÓ²Å\¿YiiÝÜ35*o¸F¨™ænˆ_PTpË]PAQd†ùý1�ùÄ ÛAGA{?9Ÿ÷|ÎøœeΜóù4bòäÉZzRRgÏžeÈ�!ZšÑh¤  ½^�ƒƒF£‘¥K—Ò¬Y3ÆŒSæVõÜÜ\K\…Ù¯_¿bà4jÔ(ºt髯¾j•^Öùùí·ßpqqaðàÁZz^^‡¦~ýú¸ºº–+_bcc¹yó¦ö¡-33“ƒâïï�‹‹K¹Îe¿!ìEµ‚¢}û¢E‹´c;wî$44”ùóç—{gSyÏWÖàÂ… 9r„Ž;Z­µÜžÇ2†V¥Ú·«æ[žß½÷ª<×›m]WUÞ:\Õó©Ž“÷îÝËòåË™2e Í›7×âvíÚ…»»»ö²jœj]W�BT.2¡Z�t.î8x˜Ÿ”\‡ª5µÿשS€-[¶P«V-8qâ6l@§Ó‘˜˜ÈíÛ·yüñÇqssãÛo¿å•W^¡nݺdff²nÝ:ôz½UÃïééI«V­§aÆe®,*Ž““/¼ðË–-cÞ¼yôéÓ‡;wîIzz:/¿ü²íKîš=Ê+î�æÍ›ãççÇæÍ›qvv¦U«VÄÇdziÓ&š6mª}�0™L 6 �NÇòåËó‡�   V¬X�Ñh$00�k×®±lÙ2ÜÜÜ´û2ªÆY$&&¢×뵺c«¼ç+K¯^½Ø±caaa$&&Ò¶m[RRRX¹r%nnnÚ NõwB\\sæÌaРAxxxhÛ½½½µÑØû}Q�NGçÎ�Ù¾};?þø#;v$##ƒåË—S³fMºté¢Å®]»–5kÖ0xð` ÀüÁÕ«WiÕªÑÑÑ…ÎjV½zuZ´h¡ýœœœŒÉdÂ××·PÔÝ+ë|uêÔ¡M›6¬_¿GGGÚ¶mKVVk×®åúõë|øá‡¶/)SÿþýùòË/™={6Ï=÷:�Ž�7’��MÿþýmÕ”õ>„°Õ~¨¸¾½¢•5èÓ§ÑÑÑ|þùç <OOOŽ;Fxx8þþþ¥>ä¦8ª}»j¾ª¿{!ìAõz«Œu½,ªãäàà`"""øþûïéׯõêÕcïÞ½ìÛ·�—^zIûX5Nµ®«Æ !*™P­@®_Áµã+¶É¥òõõå•W^aÍš5üïÿþ/ 6dâĉ„……‰¿¿?={ödܸqüðÃ|õÕWV¯÷Ýw‹l½îÞ½;qqq%>]YųÏ>‹³³3áááÚ‡d777^ýõ{:oqìQ^a:�ŽwÞy‡ÐÐPí©Ý:�Ž–-[2bÄ«UÕ¶t:£G�&44”uëÖ†^¯§iÓ¦L˜0A»‘jœErr2>>>%®‚+ïùÊâããÃøñã #""‚ˆˆÀ<�3fŒv{ ÕßU@@cÇŽeÙ²eÚÃçt:íÚµcÈ�!Ú {¿!Ê2tèPL&Û¶mcóæÍ888иqcF�]ê-&Ο?ÀÖ­[Ùºu«ÍQó5_xBÕ²3£^½zZÚ½(ë|:�ŽQ£F±bÅ Ö¬Y£ÝÏ´nݺeÞëµ$mÛ¶eĈ¬\¹’Ù³gæû7¿ùæ›ÚíwÊ«¬÷!„½¨öC•QYc€Âïí‹/¾ÌÏèÒ¥ ¯½öZ©ã–âÜMß^Z¾óï^<|åëMuœìããûï¾ËÒ¥KY¼x1`¾åÁàÁƒ ÑΧw7u½´8!Då¢;uê” dùýrúôiÀþ¿ßÜÜ\ÒÒÒðððÐ:·‚‚ÒÒÒðòò²jt333¹yó&ÕªUÃÃÃCK/,22’ÐÐP,XPbLy¤§§c4‹”Å^ì]޲ܯ¿ã£Ìr�ÖªU«ÜÛwL&)))xzz–º•H5N•½Ï—““CZZžžžEî×T˜êïêöíÛܺu //¯RËgï÷ñW"u½ü ׯ_ÇÓÓ³Ôë÷a”ŸŸOjj*¥ÖáòHKK V­Z6Gă$uýî¨öC£ÌÌL²³³ñññ±ËØUµoWÍ÷QþÝßoRßËïQ¾ÞTÇÉ †"÷S·¥§Z×UãDQR×Ńb¹Öd…êCÊÅÅ¥È6?‡bòêÕ«—º:Íh4²eËÚµkg·ÉÉÂ÷ݱ·ûQ^aÅ]£ªt:�Õ ÞK¢§ÊÞçsuuUZ=¦ú»rww/ó Á`ÿ÷!Diœœœ¨[·ìÛ×<ŒíþÞd"U<ÌTû¡‡QYãåòRíÛUó}”÷¢òy”¯7Õq²êçLÕ8Õº®'„¨x2¡ú7gÎRRRHLLdäÈ‘¶‡+�‡­¼B!„B!„âÑ"ªq...Ô®]›üã4zžú°•W!„B!„B –§€‰‡›ü…øk�º.Ä_ƒÔu!þ:¤¾ !„°'Y¡*„B!„B!„Šd…ê`›$"–o´åï(Ä£Mêº R×…øë�ú.Ä_ƒ¬Bš¬PB!„B!„BE2¡*„B!„B!„ŠdBU!„B!„B!É„ªB!„B!„B(’ U!„B!„B!„Päh› De÷É'ŸPPPÀÔ©Sm‰JÀd2±cÇ¢££IIIÁÇLJàà`z÷î�N§³ ·’ŸŸÏöíÛÙ¿?7nÜÀÇLJ   úô郃ßßÿÄÇdzmÛ¶B¯4sttäí·ß¶J;þ<{öìáĉ˜L&8p îîîZŒÁ``ýúõ;vŒŒŒ êÖ­KŸ>}hÓ¦M¡3AAA¿ýöqqq¤¦¦âããC×®]yê©§J|o;wîdË–-Ìœ9Óö�r¾…•v¾ÂTㄸ_ŒF#ãÇ�§ÿþtëÖÍö0)))DDDpñâE5jÄÀ�©]»¶u`nß¾ÍÇL§N�8p Õ±üü|vìØÁñãǹté>>>ôíÛ—¶mÛZÅ�={–�7réÒ%ªT©B@@ƒ¦J•*Vq—/_fýúõ\¼xGGGš6mJHH^^^Vq…•V¾E‹‘••e•Э[7‚ƒƒµŸï&_!ìÉ`0°nÝ:Ž?NFF>>>ôìÙ“Ž;–ØÿY¨öÙåÉC¥oW�lÛ¶�øøxí5¶š5kF¿~ýõò©öíªc Õ|…°{]oªãP{Ä©¶1¶6lØÀÆ�™;w.ÎÎÎZºJÛ¡WÞ6F¥íBT²Bµ%&&²ÿþ2ÿ�9sÆö¥BTZ‹/æûï¿§jÕªôèÑwww–.]Ê¢E‹lC­˜L&æÎ�ËòåË©]»6=zôÀÙÙ™eË–1oÞ<«Ø¸¸8Μ9ƒ»»»Õ¿jÕªYÅÅÇÇóÉ'ŸpúôiZ·nMýúõ‰ŒŒ´*Kvv6“&MbÓ¦M4jÔˆ®]»’——Ç_|Áo¿ý¦ÅðùçŸóÓO?áííM�=prrâ›o¾áÛo¿Õâ KHH`ݺudffÚRη°ÒÎW˜jœ÷‹ÑhdõêÕ$%%‘——g{˜Ë—/óá‡ròäIÚ´iC›6m8yò$ãÇ�çÒ¥K¶á¥ %))‰œœ«tƒÁÀ¬Y³ÇÅÅ…N�:‘••Å—_~É… ´¸�;wòÑGqëÖ-ºt邟ŸQQQŒ?Þj²óÈ‘#Lž<™¤¤$Ú·oO‹-8tè~ø!‰‰‰Zœ­’Êwç΢¢¢0 EÚ²Âìî6_!ìÅh4òé§ŸjýUçÎ�ÉÍÍeþüùüüó϶áE¨ôÙåÉC¥o‡»�€¹<‡"55UûY¥|ª}»ê˜B5_!ìÁ^×›ê8Ô^q*mŒ­ .°zõjnÞ¼‰Éd²:¦Úv¨ÆǶ�Qm;„•‹¬P­@œ?Þ6¹£Ñˆ¿¿¿m²•ΩS§ˆŠŠbРAV+±Ö®]Kxx8={öäñÇ/ôŠ?%$$ËK/½DHHˆ–¾xñbvíÚEbb"�=ö—.]¢U«VŒ9R‹³•››Ëœ9sèØ±##GŽÔ¾)Þ»w/ ,àìÙ³øùùΕ+Wøì³Ï¨_¿>$44”Ÿ~ú‰àà`¼½½µUn|ð�Õê¶+V°aÃúöí«½~öìÙ$''“””@�5´x Õ|Aí| 'Äýˆ HHH(2�XXXXÎÎÎ|öÙg¸»›W”õïߟ÷ߟü‘É“'Û¼¢xQQQ¤¦¦R½zuÛClذ�?þøƒ�>úˆ† 0hÐ Æ�Oxx8ãÆ�###ƒ¥K—Ò±cGF�­½ö™gža„ ¬^½š7Þxƒ‚‚–,YBãÆ�™2e z½€çž{Ž÷Þ{�ððpÆŽ«½Þ¢´òY&ŽÿùÏ–¸*÷n󞢢¢8{ö,Ÿ~ú)M›6Ì_‚Nœ8‘�72pà@íÚ,ŽJŸ­š‡jß®:éÝ»7½{÷ÖŽƒ9ßŋӤI^}õU@½|ª}»ê˜B5_!ìá^¯7Õq¨½ãTÚ˜ÂòòòX¸p!-[¶äÈ‘#VÇTÛÕ8Õ6FµíBT.²Bõ!´dÉ~üñGΞ=ËôéÓyóÍ77n?þø#¹¹¹ZÜòåË™6mùùù…^m6kÖ,–/_ÀüùóÙ°a§OŸfÆŒ¼ùæ›L™2…øøxL&ááá¼ÿþû¼ñÆL›6�„„’’’˜:u*‘‘‘…Îlvøða¦N�ªu€{öìaÒ¤I >œýë_̘1ƒ³gÏZ½¦  €ˆˆ&MšÄ°aÃ?~<{÷îeóæÍL›6Í*VTN‡ gÏžVé–Ÿ-ÇKS«V-«Ÿ-ƒGÇ?¿ÿIHH Q£FÚÏÅ9pà999¼øâ‹VÛs:wîLXX˜6P}š�>ú'''ê×¯ÏÆ�‰��§]»vÚë/_¾ÌÁƒù׿þÀÅ‹IIIaÆ ãïïÏÎ�;™?>-Z´àøñãtîܶoßÎW_}ÅܹsñõõåÆ�lÛ¶�=zh爌Œ$==___¢££ùúë¯yüñÇyñŹsç»víbƌ̜9S›@›={6±±±´iÓ†'Ÿ|’¤¤$¾ùæ¼¼¼HOO·:¿¨œRSSquu¥fÍšV鸹¹iÛZŠÓ°aCj׮ͶmÛ ÂÍÍ�´´4¢¢¢hРuêÔ ==�Û·oãîîÎ’%KHNNÆÛÛ›.]ºXÝcèØ±cx{{ãååE||<§N�B¯×hµJ¶  Àj²ÖÂ2y‘––½§¡Å•+W€?'~Ákqúôi®]»¦ýl¡š/¨�Ôㄸ_êׯ¯}¸~ý:«W¯¶‰0§àëëk{___L&ׯ_×V¤§  €… òì³Ïùðæ<®]»Æ€HKK#..ŽäädêÕ«g5‘k4�?ë]az½žŒŒ ŒF#®®®Œ5ŠfÍšYÅpíÚµ"äÊ*˜?6lØ�ßÿ�˜˜ hÚ´)ýû÷ÇÍÍ  Üù q?¼þúë¶IdddM‹-puuµ=¬Qí³UóPíÛïv<’��Àúõë1b„ÕvaÕò©öíªc Õ|…°‡{½ÞTÇ¡öŒSmc,Ž9BLL _|ñ111¶‡•Ìë¦d IDATÛÕ8[%µ1ªm‡¢r)ZkÅCáúõë <˜hiÛ·oçûï¿'22’Þ½{Ó¡C–.]Êþýû­&Tcbbpvv¦C‡ZÚ¹sç˜ùäÞ~ûm222´[òØ*-_!„ììlòòòpqqáÊ•+?~Ü6ÄŠjŸ]Xiy¨öíw39yò$ñññ<ÿüó¥ÖÝÒÊw·}»Ê˜¢´|…°·‡åz+O³dÉžxâ‰b·ú[¨¶ªq…•ÖÆÜmÛ!„¨XÅ·¢Ò«_¿~‘U›:�Ž&MšpõêU-­G�dggkÌ«W¯rùòe«-`ÞžP˜eË�í–LÛÆßËË‹'žxÂjB5&&???måO¯^½¨W¯K—.eäÈ‘üç?ÿaË–-€y;#Àµk×ðöö¦jÕªÚy, ßÇGTnz½¾Ø{ö‚yÅöú),66–°°0zöìÉܹs?~»°ÒòPíÛïf<‰£££Õ®â”V¾òöíåS”–¯öö°\oªmÌï¿ÿιsç6l˜WÕ¶C5®°ÒÚ˜ò¶BˆÊA¶ü?¤ +ÌÉɉÜÜ\L&:�Ž€€êԩþ}û "&&«Nç^uïÞ�¯¿þš+W®àííÍ¡C‡xå•W´ãÞÞÞ|öÙg>|˜C‡qâÄ :ÄêÕ«y÷ÝwiÑ¢ƒÁj%la%½WQù¸»»“••¥]&“‰¬¬¬·×ÄÅÅáääÄСC­+Ççÿþïÿ8~ü8�:u²yÕŸ4h€N§#990¯âŠÜ{ÐÝÝ�Úµk[}ñð /бcGNŸ>MAA­[·ÆÕÕ•œœ«LNNsæÌáèÑ£<ýôÓüãÿ(q»� Õ|…x”Xêæ­[·lŽü™VR[±}ûvRSSÉÉÉ!<<\KÏÎÎæÔ©S„‡‡Ó­[·ë?üù%Ý•+W¨]»6µk×fÚ´iüñÇ$%%ѨQ#üýýY¹r%NNNV»98À7ß|CµjÕ?~<­ZµÒŽ�zù,AÅiܸ1‡&==OOO ì|…¸Ÿ233qvv¶º¢““C† aÏž=ÄÇÇãççWèe³í³Uó(©nÛöíå� 8@Ë–-‹í×UËê}»Ê˜¢<ù q¯¥ëͶ� £I“&lÚ´I‹¹xñ"k×®¥víÚôìÙS¹íP�³(«�õ¶CQyÈ„êCÊrÓz[W¯^ÅËË˪aïÞ½;¿üò yyyÄÄÄÐ¥K—·(Ü�àà`ÜÜÜØ¿?�5Â`0XM|�Fôz=íÛ·§}ûö€ù†à_}õ+W®dúôéøøøpôèQ-¶°Â_¢róññÁ`0h÷صHMM%//¯Ô§Y§¦¦âìì\äÚtrr¢Zµj\¿~€�7âââB¯^½¬â,ƒË ¥Aƒ>|˜ÜÜ\« 0¯"«Q£`~Šö�;w¨W¯žU™-«º4h˜H_~ù%/^äƒ>(öF÷塚¯�šZµj¡×ëIJJ²=Drr2Úà m™L&ªT©ÂÎ�;­ÒsssIHHàêÕ«´hÑ‚Æ�ãèèHnn®U@^^`Þ!a4INNÖ¾h,üeã™3g¬êa||<óæÍ£]»v¼õÖ[EÚP/_^^»víâïÿ»ÕíàÏIeK[¦’¯÷Ó”)S¨Y³&S§NµJ¯V­z½^럋£Úg«æ¡Ú·—wLýúõµ _•|…¸ŸêÕ«Ç™3gŠ<%>>£ÑH£F�´4Û¾]µÏVÍCµo/Ïxж2—´úNµ|ª}»ê˜B5_!ì¡<×›m]¯(ªmÌ¢E‹Šôͯ½öóçÏgܸq€zÛ¡gQV£Úv!*Y¡Z�¼½½‹4¶Å±½¿)˜WíÍ�;—W_}•¦M›rõêUV¬X�““Ï?ÿ¼U¬§§'­Zµ"<<œ† Þ—o¸ºwïÎŽ;HII)òÈ   Ö­[ÇÊ•+騱#ÎÎΜ8q‚³gÏj¥êÚµ+ëׯ'44”¬¬,š5kFJJ «V­B¯×k¦EåÖ¼ysüüüؼy3ÎÎδjÕŠøøx6mÚDÓ¦MµëÝd21lØ0t:�öð•^½z±cÇÂÂÂHLL¤mÛ¶¤¤¤°råJÜÜÜèÓ§}úô!::šÏ?ÿœÁƒãééɱcÇÇßß_»Ñ¼ŸŸAAA¬X±£ÑH`` ×®]cÙ²e¸¹¹i÷G &""‚ï¿ÿž~ýúQ¯^=öîÝ˾}ûx饗´Ó‘‘‘xxx�™™YdâÌ'Ëʪù ñ(êß¿?_~ù%³gÏæ¹çžC§Ó±qãF²³³éß¿¿·víZÖ¬YÃàÁƒ0`@¡3”màÀ�LŸ>�™3gB•*Uؽ{7±±±¼ð ڪ•Î�;³}ûv~üñG:vì¨=ªfÍšZõÇpõêUZµjEtttáló}V-®TÔ©S‡6mÚ°~ýziÛ¶-YYY¬]»–ëׯóá‡öÏWˆ»B\\sæÌaРAxxxh·°ðööæ©§žŠïÛUûlÕ¥¨¤t:ï¼ó¡¡¡¬Y³†ˆˆt:-[¶dĈV·¢°åããÃøñã #""‚ˆˆÀ|m�3///ÀüMóرcY¶l_|ñ`~ E—.]xíµ×´[5Õ¾½¼ã‘ääd|||Jœ´P-ŸJß^ž1…j¾BØÃÃx½©¶1ªTÛÕ8‹²Ú•¶CQùèN�:e‚?—à û:}ú4`ßßïäÉ“qsscâĉäççsíÚ5<==KÝ Ihh( ,(vÅë½2™L¼ýöÛ´k׎áÇÛÌ[CÒÓÓ)((ÀÓӳćP ÒÒÒðööF¯×ÆöíÛùïÿkúÀÜ�¿ã£.77—´´4jÕªUâߺ$999¤¥¥•y]gff’�����O‘�Ka&“‰””<==qrr²=¬ÉÈÈÀ`0û¥ÄýTQùŠ¢¤®?xiii€ùÞª÷Ë�;wÈÉÉÑòTƒÁÀõë×KíŸî‡üü|RSSñðð(µ½ö%uýîܾ}›[·náååUjZÕ>[5Õ¾ý^Æ#ÅQ-Ÿ½ûvÕ|EQRßËïa¼ÞTÛUªm‡jœ*{·%R×Ńb¹Öd…êCÎÑÑ‘zõêÙ&[1�lÙ²…víÚÝ—ÉT0ßËêúõëôìÙÓö�ÆÉÉIiE®““S‰Û!ÄÃÃÅÅ___Ûd%®®®e^×`ÞîªòÔK�N§tíݯúQ–ŠÊWˆÊà~N¤Z¸¹¹•¹²ÆÉɉºuëÚ&ßwŽŽŽ’¯wÃÝݽÈÓ«U©öÙªy¨öí÷2)ŽjùìÝ·«æ+„=<Œ×›j£JµíP�Seï¶CqÿÈ„ê#nΜ9¤¤¤�˜˜ÈÈ‘#mß³½{÷ÍÑ£Giß¾= 6´ B!„B!„â‘!ª¡§žz gggÛäb¹¸¸P»vmþñ�Ðè>< ´jÕªäççÓ»wïr?0DU``àC³ÕD!„B!„B<ÚdBõ!ôÌ3ÏØ&•èßÿþ·m’]µnÝšÖ­[Û&ÛUPPAAA¶ÉB!„B!„B ¶Iâ!bùF[þŽB<Ú¤® ñ× u]ˆ¿©ïBü5È*tñ É U!„B!„B!„P$ªB!„B!„B¡H&T…B!„B!„B‘L¨ !„B!„B!„"™PB!„B!„BE޶ âÑñÉ'ŸPPPÀÔ©Sm=RŽ?ÎôéÓ™0a-[¶´=,0“ÉÄŽ;ˆŽŽ&%%‚ƒƒéÝ»7:�Î6\³mÛ6âããm“5Íš5£_¿~äçç³}ûvöïßÏ�7ðññ!((ˆ>}úààðç÷Dªe‰��gÛ¶mÚÏŽŽŽ¼ýöÛ¶Éìܹ“-[¶0sæLÛCV>Õ8!ìÍh42~üxú÷ïO·nÝlsùòeÖ¯_ÏÅ‹qtt¤iÓ¦„„„àååeZÄÙ³gùõ×_¹|ù2...ðüóÏS«V­rÅeee±hÑ"«×Øúç?ÿ‰§§§m2·oßæã�?¦S§N 8(ÿù,¿ƒóçÏãääDýúõ0`¾¾¾V¯±wœö`0X·nÇ�'##zöìIÇŽKíÛ‹³aÃ6nÜÈܹsqvv¶:vþüyöìÙÉ'0™L2pà@ÜÝÝ�ò�TûDÕ8[%½{�TÏ'„=ÜK]/((à·ß~#..ŽÔÔT|||èÚµ+O=õT‰¯-m<}?ëˆ=ò-¬´óÝMù ä6FQy”\ƒÅCÏ××W>X‰nñâÅ|ÿý÷T­Z•=zàîîÎÒ¥KËœx(‰ÑhäСC¤¦¦æAÎܹsY¾|9µkצG�8;;³lÙ2æÍ›gõZÕ²ÄÅÅqæÌÜÝÝ­þU«VÍ*Î"!!�uëÖ‘™™i{¨B˧'„=�FV¯^MRRyyy¶‡9rä“'O&))‰öíÛÓ¢E :ć~Hbb¢m¸•ƒ2uêTnÞ¼I—.]hРûöícÊ”)¤§§—;®$‰‰‰ÄÅÅÙ&kBCCIJJ"''ÇöP±lÏwúôi&OžÌùóçéÔ©���œ>}š &pþüùû'„=�F>ýôS6mÚD£F�èܹ3¹¹¹ÌŸ?ŸŸþÙ6¼T.\`õêÕܼy“Édu,>>žO>ù„Ó§OÓºukêׯOddd‘>±8¶cPïUã +é}Ø{ Pžó q¯î¥®ðùçŸóÓO?áííM�=prrâ›o¾áÛo¿µ JOÃý«#öÈ·°ÒÎw7僒Û!Då"+T+Ò¥épñ#ÛÔ¢¼_‚æ+lSËôæ›oÚ& q_�:uЍ¨( ¤­âX»v-áááôìٓǼÐ+þÔ»woz÷îm•f2™X¼x1Mš4áÕW_̃–ØØX^zé%BBB´ØÅ‹³k×.yì±ÇÊU–K—.ѪU+FŽ©ÅgöìÙ$''“””@�5l"*¶|ªqBØCll,6l !!¡Ä‰Æ‚‚–,YBãÆ�™2e z½€çž{Ž÷Þ{�ððpÆŽkóª?-]º”Ö­[óþûïk«B<ȬY³ˆŠŠÒê˜J\ÕªUyï½÷´s[$$$ðé§Ÿòïÿ»ØÕ©QQQ¤¦¦R½zu«ôòœï×_ÅÅÅ…éÓ§ãêê À3Ï<Ã{ï½Ç¯¿þª­‚³wœöÅÙ³gùôÓOiÚ´)`îŸ'NœÈÆ�8p V·K“——ÇÂ… iÙ²%Gޱ:–››Ëœ9sèØ±##GŽÔêñÞ½{Y°`gÏžÅÏÏOy¬ê}¢jœEiïÃÞcÕó a÷R×wìØÁñãÇùàƒhÛ¶­–¾bÅ 6lØ@ß¾}©_¿> 6ž¾uÄžù‚ÚùÊS>‹ÒÚ!Då"ªÉd“Ñ6µ(S�m »wïfË–-$''c2™¨W¯!!!<ùä“ZÌ?ü@AAÿó?ÿ£¥]¿~�Õ«WsöìY²²²hܸ1}úô!((H‹™?>�7& €5kÖpöìYêÖ­ËàÁƒiÙ²%?ÿü3ÑÑѤ§§Ó¤I†JÆ IJJâÛo¿¥G�ôèÑC;ÀáÇùå—_1bõêÕcÏž=üöÛo$%%áììL“&M4h~~~Úk X»v-‡"11‘ºuëòüóÏ“™™ÉÁƒ™Õ8!ìÁ²•¯k׮ܺu‹Õ«WÛ†pãÆ 222Š|óððà±Ç+u…jFF999tèÐÁj‹]`` ÙÙÙåŠ+Îõë×™9s&!!!têÔÉö0))),_¾œÉ“'3cÆ ÛÃE”v¾*Uªh“Ÿ`þàåä䤵 öŽâ^]¾|___m‚@§ÓHbb"ƒ¡ÄI–ÂV¬XA£F�hÞ¼y‘I‚�““Ë/¾hU�;wîL‡JÝ[ÒXAµOT�³(í}XØs eŸO{¸—º~áÂjÔ¨Q¤.µhÑ‚ 6�˜˜¨M¨ªŒ§ïG±w¾*ç³P)Ÿ…J#„¨ŠÖ`Qéíܹ“ÐÐPš6mÊßÿþwÙµksæÌáË/¿¤nݺ€¹S,(øs2öÒ¥KL›6�üü|ºuëFÕªU9pà³fÍbÈ�!<ûì³\¼x‘””6lØ@pp0þþþìܹ“ùóçÓ¢E Ž?NçÎ�qpp`ûöí|õÕWÌ�;___nܸÁ¶mÛŠL¨FFF’žžŽ¯¯/ÑÑÑ|ýõ×<þøã¼øâ‹Ü¹s‡]»v1cÆ fΜ©u8³gÏ&66–6mÚðä“O’””Ä7ß|ƒ———ÒNñ०¦âêêJÍš5­Ò=<:uêDAA[·n%''‡öíÛß·8!ìáõ×_·M"##ƒèèhZ´ha5±_’#GŽÃ_|ALLŒíaŽ;†··7^^^ÄÇÇsêÔ)ôz=���¥~[ÒXAµOT�³(ë}Ø{  z>!ìá^êz·nÝŠío¯\¹ü9�jãéûQGì™/¨�¯<僲Û!Då"ª¡íÛ·ãééÉG}¤}«Õ¦M¦L™Â©S§´ U[Ë—/'//�éÓ§S¯^=BBB´ûÝtîÜÎ�;ÇäÉ“µ>þþþ̘1ƒcÇŽ±`Ám@•*UX»v-)))Ô®]›îÝ»óóÏ?síÚ5j×® ˜·qÅÇÇóÌ3Ï Ó鈉‰ÁÅÅ…É“'k+:vìÈ„ 8räûÛ߈��'66–~ýúYmßjß¾=sæÌÁÉÉIK•Ç�;wpss³MÀÍÍ�;wîØ&—håÊ•Ú ¸Ât:S¦LaÚ´iŒ=.]º„¯¯/&LÐâTËréÒ%¾ûî;4h@Íš59zô({öìaàÀ� 4¨ðKËTQåS�âAªZµ*]ºt±MfÆ dffÒ¹sgÛCÅÊÉÉá‡~ 11‘K—.1jÔ(« å�óC¬bbb9rd±+n~ùå ƒö€›²”v¾®]»’‘‘Áüù󉈈àÎ�;ddd0|øp« öŽÂÞöìÙCLL ÇŽ£AƒŒ5Ê6¤ˆ[·n±dÉÞxã ÜÝÍ—²uëÖ-jÖ¬É?üÀŽ;¨U«üüóÏôíÛ—¡C‡Ú¾(y¬ Ú'ªÆ�Úû°÷@õ|BØ[yëzq_|ܺu‹_ý•ºuëÒ¸qcÛÃ¥ª¨:¢š¯ªò”O¥�BT.%ÖK/½Ä¸q㬶X¶4–Ôȧ§§sôèQž~úim2ÀÉɉ—_~™¼¼<öîÝ«¥ûûûk“©€vo—:Xm§²¤[òíÖ­:�Žýû÷k1ñññäææjßâyyy‘››ËöíÛ1Í·xïÙ³GGG^xáí<`žPmÒ¤‰Uš¨<ŒFc‰Ûòt:�ö÷.ËÉ“'‰��çùçŸ/21Mjj*UªT¡F�Ô¬Y“«W¯¥Ý¸]µ,™™™Ô­[—÷ߟO?ý”wß}—Y³fÀ/¿üRê–ä’TDùTㄨH&“‰ˆˆV­ZEppp‘I€’€�IDAT�’˜L&nß¾�^¯§  €ƒrëÖ-Û0å8€U«VáååÅSO=e{ˆsçα~ýzÞzë­bÛ â”v¾ëׯ³ÿ~ôz=îîîøøø ×ëÙµkW¯^½oqBØ[vv6yyy¸¸¸påÊŽ?nRÄ’%Kxâ‰'¬nKe+33“sçÎqüøqf͚żyóøöÛo fÓ¦MÄÇÇÛ¾¤Ô±‚jŸ¨jïì;µó aowS× KJJbêÔ©deeûEcY*ªŽ”'_UªåSmc„•‡¬P}µiÓ†ÔÔT¶oßÎÅ‹¹téR™Oõµ|À*n2²qãÆèt:«a–•ª–É[___«tÛÎÑËË‹'žx‚ýû÷k“¡111øùùi+g{õêÅ¡C‡Xºt)?ýô�?þ8-[¶¤S§NÚV’k×®áííMÕªUµs[4mڔ˗/Û&‹J@¯×“ŸŸo› @~~~‘ë¥$‘‘‘8::Ò¡CÛCÄÆÆF¯^½6lz½£ÑÈ÷ßÏÊ•+ñòò¢S§NÊeéÖ­›Õ–0=lØ0&MšD\\\‘›Å—¦¢Ê§'DEIIIaáÂ…œ>}š>}ú0tèPtº?ï•Xš*UªðÁæ• Ó¦Mã믿füøñw—’’‰'èß¿‘v)77—… ¢¼Õ¿´óÌš5‹ÔÔT¦OŸNƒ €?^õŸÿü‡ÿüç?÷%N{ëÕ«½zõÂ`0°dÉæÎ�Ë'Ÿ|RâJðßÿ�sçÎñÅ_زbé+‡ªíprqqaĈ9r„Ý»wÓªU+«×”6VPíUãT߇½ÇªçÂÞÊ[×-L&[¶laÕªUT¯^�I“&•ùšâTTQÍW•jùTÛ!DåRü×/¢R[»v-cÇŽeÓ¦M€y»¼í‡E[–'0Û>ØÌ«TÈÍ͵=tWºwïNBBW®\!??ŸC‡Y­Bòööæ³Ï>ã�wÞáÉ'ŸäÒ¥Kü÷¿ÿå�wÞáØ±c †bË àììl›$* www²²²Š|l2™ÈÊÊRÚ¾b08pà-[¶,vËM\\NNN :TÔèõz†Ž“““ö ú½–¥Aƒèt:’““m•ª²•O5NˆûéÀ�Œ?ž7n0~üx†^懒ÜÜ\nÞ¼Y¤ŽøùùѵkWþøã ƒr\a–ÅMÄlß¾�ÔÔTrrr×þeggsêÔ)ÂÃËÜ'­´óݼy“„„žyæmòÌ;3úöíKrr2ééév�Â^233µq¤…““C† Ì;‘J†¯¯/›6mÒêRll,`Ïþþûï€ùö @‘û-»»»S»ví"+¯Ë+”DµO´�S}ö¨žO{¸—ºæÏ›3gÎ$,,Œ®]»2cÆŒ»šL…Š«#ªùªR-Ÿj#„¨\d…êCæÚµk„‡‡Ó­[7FŽ©­îÉÈȰ‰´fù¶ßv@ æ›o�F-æ^ãææÆþýûiÔ¨ƒÁê›A£Ñˆ^¯§}ûöÚÃ3Ž9ÂW_}ÅÊ•+™>}:>>>=zT‹-¬¸÷ * ƒvO]‹ÔÔTòòòðññ)]¼#GŽ”ú`•ÔÔTœ��‹lÇqrr¢Zµj\¿~P/ËÆ�qqq¡W¯^Z   ¦ÊóA *®|ªqB¼¼¼ŠÝµa2™¨R¥ ;wî´JÏÍÍ%!!�«W¯Ò¢E «z[ÚùRSS1™LÅ~Ihy£eÔžq…߯÷bÊ”)Ô¬Y“©S§Z¥W«V ½^¯õkÅqvvæòåËV;‹,_pìܹ???zöìIƒ 8|ø0¹¹¹EÚˆÜÜ\jÔ¨a•VÖXAµOT�S}ö¨žO{¸—ºn2™øòË/¹xñ"|ðA±u+�Šª#ªùªR-Ÿj#„¨\d…êC&)) €¶mÛZm•��âææÆ¾}ûؼy3ÁÁÁøûû[½æ^tïÞ�;v�’’Rä©�AAA¬[·Ž•+WÒ±cGœ��9qâgÏžÕJÕµkWÖ¯_Ohh(YYY4kÖŒ””V­Z…^o~؈¨|š7oŽŸŸ›7oÆÙÙ™V­ZϦM›hÚ´©61™L 6 �NÇòå˭Α˜˜ˆ^¯×®u[½zõbÇŽ„……‘˜˜HÛ¶mIIIaåÊ•¸¹¹Ñ§O@½,}úô!::šÏ?ÿœÁƒãééɱcÇÇßß¿Ü7†¯¨ò©Æ ñ ýñÇ\½z•V­Zm{˜êÕ«k^Ö®]Ëš5k¥¨$t:ï¼ó¡¡¡¬Y³†ˆˆt:-[¶dĈV«ªK’œœ¬=±º8>>>Œ?ž°°0"""ˆˆˆÌ×ܘ1cðòòÔËÀرcY¶l™v#xºtéÂk¯½¦TæÂ*ª|ªqB|¸íaÀ¼U$==�‚‚<==‹½˜ãÒÒÒðööF¯×ÆöíÛùïÿkzßܯ¿ã£Ìr�ÖªU«Ä¿í½ÊÉÉ!-- OOÏR·«–%33“ììl|||ìR/*ª|ªq¢(©ë•“Ñh$55WWW<<Q”Ô÷ò{u]UEÕÕ|UÙ»|¢(©ëâA±\k²Bõ!åââRd{¡ƒƒÞÞÞVi%qww/r[{Љ‰áúõëÅ®”µprrRz–““S‰[ºDåUÜ5jo®®®¥® ³P-KõêÕ‹¬‚¹U>Õ8!¥mí-L5®¢èt:¥Õ(öŽÂ^î÷øÌ×µÊøP•jŸ¨§ÊÞcÕó a¢®«ª¨:¢š¯*{—OQñdBUØÕÞ½{‰ŽŽæèÑ£´oßž† Ú†!„B!„BñÐ’ UaWU«V%??ŸÞ½{ß·û¸VøÖ!„B!„Bñ×$ªÂ®Z·nMëÖ­m“í*((ˆ   Ûd!„B!„B!î;Û!„B!„B!„Å“ªˆå)`âá&G!þ¤® ñ× u]ˆ¿©ïB!ìIV¨Þg¶IB!„B!„ÂŽdþE=3.1", "mypy", "rich", "ruff", "stubgen-pyx>=0.2.18", ] test = [ "pytest", "mysqlclient", "pymysql >=0.8.1", "aiomysql", # Breaking change in 0.23.* # https://github.com/pytest-dev/pytest-asyncio/issues/706 "pytest-asyncio >=0.21.2,<0.22", "pytest-mock", "pytest-xdist", "cryptography", "uvloop; sys_platform != 'win32' and implementation_name == 'cpython'", ] [tool.poetry.build] generate-setup-file = true script = "build_cython.py" [build-system] build-backend = "poetry.core.masonry.api" requires = ["poetry-core", "setuptools", "cython>=3.1"] # freethreading_compatible directive [tool.cibuildwheel] # The modules declare freethreading_compatible, and CI gates on 3.14t, so ship # wheels for free-threaded builds too — otherwise those users compile from sdist. enable = ["cpython-freethreading"] [tool.pytest.ini_options] asyncio_mode = 'auto' [tool.mypy] pretty = true ignore_missing_imports = true warn_unused_ignores = true # TODO: Uncomment next line after type hints improved #check_untyped_defs = true [tool.ruff] line-length = 100 [tool.ruff.lint] extend-select = [ "FA", # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up "RUF100", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf ] ignore = ["UP031", "UP032"] # https://docs.astral.sh/ruff/rules/printf-string-formatting/ [tool.ruff.lint.per-file-ignores] # Stubs are generated from the .pyx sources by `make stubs`, so they mirror # whatever the sources spell, and re-exporting constant modules by star import # is how a stub is meant to forward them. "*.pyi" = ["F403", "F405", "UP006", "UP035", "UP045"] python-asyncmy-0.2.15/scripts/000077500000000000000000000000001525423006000162655ustar00rootroot00000000000000python-asyncmy-0.2.15/scripts/gen_stubs.py000066400000000000000000000163161525423006000206370ustar00rootroot00000000000000"""Regenerate the .pyi stubs for the Cython modules. stubgen-pyx parses the .pyx sources, so unlike mypy's stubgen it recovers real signatures for cdef classes instead of (*args, **kwargs). It gets a few things wrong that this script patches afterwards, so regeneration stays a single reproducible command: make stubs Run `make stubtest` after regenerating; it compares every stub against the compiled module and fails on drift. """ from __future__ import annotations import re import subprocess import sys from pathlib import Path PACKAGE = Path(__file__).resolve().parent.parent / "asyncmy" # The .pyx sources carry no return annotations, so every generated signature # would end in an implicit Any and callers would get no attribute checking on # what they get back. Annotating the sources instead would hand the types to # Cython's annotation_typing and change codegen, so the public surface is # annotated here. {file: {function: return type}} RETURN_TYPES = { "connection.pyi": { "connect": "_ConnectionContextManager[Connection]", "cursor": "Cursor", "ensure_closed": "None", "close": "None", "begin": "None", "commit": "None", "rollback": "None", "ping": "None", "select_db": "None", "set_charset": "None", "get_server_info": "str", "get_host_info": "str", "get_proto_info": "int", "thread_id": "int", "affected_rows": "int", "insert_id": "int", "get_transaction_status": "bool", "get_autocommit": "bool", "escape_string": "str", "__aenter__": "Self", }, "cursors.pyi": { "execute": "int", "executemany": "int | None", "fetchone": "Any", "fetchall": "list[Any]", "fetchmany": "list[Any]", "nextset": "bool | None", "close": "None", "mogrify": "str", # scroll is deliberately absent: Cursor.scroll is sync while # SSCursor.scroll is `async def`, so no single return type is true for # both and annotating it would only assert the inconsistency louder. "__aenter__": "Self", "__await__": "Generator[Any, None, Self]", }, "pool.pyi": { "create_pool": "_PoolContextManager[Pool]", "acquire": "_PoolAcquireContextManager[Connection]", "release": "Any", "clear": "None", "close": "None", "terminate": "None", "wait_closed": "None", "__aenter__": "Self", }, } # Imports the annotations above need, appended after the generated imports. EXTRA_IMPORTS = { "connection.pyi": ( "from typing_extensions import Self\n" "from asyncmy.contexts import _ConnectionContextManager\n" ), "cursors.pyi": ( "from collections.abc import Generator\n" "from typing import Any\n" "from typing_extensions import Self\n" ), "pool.pyi": ( "from typing import Any\n" "from typing_extensions import Self\n" "from asyncmy.contexts import _PoolAcquireContextManager, _PoolContextManager\n" ), } # (file, before, after, reason) PATCHES = [ ( "converters.pyi", "from cpython import datetime\n", "import datetime\n", "`cimport`ed cpython.datetime is Cython-only; the stub must name the real module", ), ( "converters.pyi", "conversions = ...", "conversions: dict", "the initialiser copies a cdef dict, which stubgen-pyx cannot evaluate", ), ( # Anchored to __init__, not to `class Connection:` — inserting straight # after the class line would push the docstring below a statement and # stop it being a docstring at all. "connection.pyi", " def __init__(self, *, user=None", " # Injected onto the instance by BinLogStream\n" " # (replication/binlogstream.py), hence declared but not defined.\n" " _get_table_information: Any\n\n" " def __init__(self, *, user=None", "replication attaches this attribute at runtime", ), ] def apply_return_types(path: Path, returns: dict[str, str]) -> list[str]: """Give each named def an explicit return type, leaving parameters alone.""" source = path.read_text() missing = [] for name, return_type in returns.items(): # ` async def fetchone(self):` -> ` async def fetchone(self) -> Any:` # Matches whatever parameter list is currently generated, so a signature # change does not silently skip the annotation. pattern = re.compile( r"^(?P[ ]*)(?Pasync )?def (?P%s)\((?P.*)\)(?P[ ]*->[^:]+)?:" % re.escape(name), re.MULTILINE, ) source, count = pattern.subn( lambda m: ( "%s%sdef %s(%s) -> %s:" % ( m.group("indent"), m.group("async") or "", m.group("name"), m.group("params"), return_type, ) ), source, ) if not count: missing.append(name) path.write_text(source) return missing def main() -> int: subprocess.run(["stubgen-pyx", str(PACKAGE)], check=True) for filename, before, after, reason in PATCHES: path = PACKAGE / filename source = path.read_text() if before not in source: print( f"warning: {filename} no longer contains the text patched for " f"'{reason}'. stubgen-pyx may have fixed it upstream — drop the " f"entry from {Path(__file__).name} if so.", file=sys.stderr, ) continue path.write_text(source.replace(before, after, 1)) print(f"patched {filename}: {reason}") failed = False for filename, returns in RETURN_TYPES.items(): path = PACKAGE / filename source = path.read_text() extra = EXTRA_IMPORTS.get(filename, "") if extra and extra not in source: # After the generated header comment, before the first declaration. lines = source.splitlines(keepends=True) insert_at = 1 if lines and lines[0].startswith("#") else 0 source = "".join(lines[:insert_at]) + extra + "".join(lines[insert_at:]) path.write_text(source) missing = apply_return_types(path, returns) print(f"annotated {filename}: {len(returns) - len(missing)} return types") if missing: failed = True print( f"error: {filename} has no def for {sorted(missing)} — the API " f"changed, so update RETURN_TYPES in {Path(__file__).name}", file=sys.stderr, ) # Format here so the generated files satisfy `make check` as-is, rather # than every regeneration leaving the tree dirty. stubs = sorted(str(p) for p in PACKAGE.glob("*.pyi")) subprocess.run(["ruff", "format", "--quiet", *stubs], check=True) subprocess.run(["ruff", "check", "--quiet", "--fix", *stubs], check=False) print(f"formatted {len(stubs)} stub files") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main()) python-asyncmy-0.2.15/stubtest_allowlist.txt000066400000000000000000000011131525423006000213020ustar00rootroot00000000000000# Entries stubtest cannot check meaningfully. Keep this list short and # explained — it is the escape hatch, not the norm. # `from typing import Any` is a module member at runtime, so stubtest compares # typing.Any against itself and trips over its metaclass. Not our code. asyncmy.contexts.Any asyncmy.contexts.Any.__new__ asyncmy.replication.binlogstream.Any asyncmy.replication.binlogstream.Any.__new__ # Cython gives extension-type constructors a (*args, **kwargs) runtime # signature. The stub carries the real one, which is what callers need. asyncmy.charset.Charsets.__init__ python-asyncmy-0.2.15/tests/000077500000000000000000000000001525423006000157405ustar00rootroot00000000000000python-asyncmy-0.2.15/tests/__init__.py000066400000000000000000000000001525423006000200370ustar00rootroot00000000000000python-asyncmy-0.2.15/tests/test_autocommit.py000066400000000000000000000006541525423006000215370ustar00rootroot00000000000000import pytest @pytest.mark.asyncio async def test_autocommit(connection): await connection.autocommit(True) cursor = connection.cursor() await cursor.execute("SELECT @@autocommit;") assert await cursor.fetchone() == (1,) await cursor.close() await connection.autocommit(False) cursor = connection.cursor() await cursor.execute("SELECT @@autocommit;") assert await cursor.fetchone() == (0,) python-asyncmy-0.2.15/tests/test_connection.py000066400000000000000000000206501525423006000215130ustar00rootroot00000000000000import asyncio import re import ssl as ssl_module import pytest from asyncmy.connection import Connection from asyncmy.connection import _MySQLProtocol # type: ignore[attr-defined] # Private protocol helper. from asyncmy.constants.CR import CR_SERVER_GONE_ERROR, CR_SERVER_LOST from asyncmy.errors import OperationalError from conftest import connection_kwargs @pytest.mark.asyncio async def test_connect(): connection = Connection(**connection_kwargs) await connection.connect() assert connection._connected assert re.match( r"\d+\.\d+\.\d+([^0-9].*)?", connection.get_server_info(), ) assert connection.get_proto_info() == 10 assert connection.get_host_info() != "Not Connected" await connection.ensure_closed() @pytest.mark.asyncio async def test_read_timeout(): with pytest.raises(OperationalError): connection = Connection(read_timeout=1, **connection_kwargs) await connection.connect() async with connection.cursor() as cursor: await cursor.execute("DO SLEEP(3)") @pytest.mark.asyncio @pytest.mark.parametrize("event", ["data", "eof", "error"]) async def test_read_timeout_does_not_lose_early_notification(mocker, event): loop = asyncio.get_running_loop() conn = Connection(read_timeout=0.1) proto = _MySQLProtocol(loop) conn._proto = proto wait_for = asyncio.wait_for async def scheduled_wait_for(awaitable, timeout): # Python 3.9/3.10 schedule the coroutine as a separate task. Keep # that scheduling gap deterministic on newer Python versions too. return await wait_for(asyncio.ensure_future(awaitable), timeout) mocker.patch("asyncio.wait_for", side_effect=scheduled_wait_for) if event == "data": loop.call_soon(proto.buffer_updated, 4) await conn._fill_buffer(4) assert proto.length - proto.pos == 4 else: if event == "eof": loop.call_soon(proto.eof_received) else: loop.call_soon(proto.connection_lost, OSError("peer disconnected")) with pytest.raises(OperationalError) as caught: await conn._fill_buffer(4) assert caught.value.args[0] == CR_SERVER_LOST assert "read timeout" not in str(caught.value) @pytest.mark.asyncio async def test_read_timeout_waits_for_remaining_bytes(): loop = asyncio.get_running_loop() conn = Connection(read_timeout=1) proto = _MySQLProtocol(loop) conn._proto = proto proto.buffer_updated(2) read = asyncio.create_task(conn._fill_buffer(4)) try: await asyncio.sleep(0) assert not read.done() proto.buffer_updated(2) await asyncio.wait_for(read, timeout=5) finally: read.cancel() await asyncio.gather(read, return_exceptions=True) @pytest.mark.asyncio @pytest.mark.parametrize("failure", ["oserror", "closed_runtime", "unrelated_runtime"]) async def test_ping_write_errors(mocker, failure): conn = Connection() transport = mocker.Mock(spec=asyncio.Transport) transport.is_closing.return_value = False conn._transport = transport conn._connected = True error = BrokenPipeError("broken pipe") if failure == "oserror" else RuntimeError("write failed") def fail_write(data): if failure == "closed_runtime": transport.is_closing.return_value = True raise error transport.write.side_effect = fail_write try: if failure == "unrelated_runtime": with pytest.raises(RuntimeError) as caught: await conn.ping(reconnect=False) assert caught.value is error transport.close.assert_not_called() else: with pytest.raises(OperationalError) as caught: await conn.ping(reconnect=False) assert caught.value.args[0] == CR_SERVER_GONE_ERROR assert caught.value.__cause__ is error assert conn._transport is None assert not conn.connected transport.close.assert_called_once() finally: conn.close() @pytest.mark.parametrize("reconnect", [False, True]) def test_ping_closed_uvloop_transport(reconnect): uvloop = pytest.importorskip("uvloop") async def check(): conn = Connection(**connection_kwargs) await conn.connect() try: transport = conn._transport transport.close() await conn._proto.wait_closed() assert transport.is_closing() if reconnect: await conn.ping(reconnect=True) assert conn.connected assert conn._transport is not transport async with conn.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) else: with pytest.raises(OperationalError) as caught: await conn.ping(reconnect=False) assert caught.value.args[0] == CR_SERVER_GONE_ERROR assert not conn.connected assert conn._transport is None finally: await conn.ensure_closed() loop = uvloop.new_event_loop() try: loop.run_until_complete(check()) finally: loop.close() @pytest.mark.asyncio async def test_ssl_true_builds_a_context(): """`ssl=True` must actually enable TLS, not silently fall back to plaintext.""" connection = Connection(ssl=True) assert isinstance(connection._ssl_context, ssl_module.SSLContext) @pytest.mark.asyncio async def test_ssl_context_is_passed_through(): context = ssl_module.create_default_context() assert Connection(ssl=context)._ssl_context is context @pytest.mark.asyncio @pytest.mark.parametrize("value", [None, False]) async def test_ssl_disabled(value): assert Connection(ssl=value)._ssl_context is None @pytest.mark.asyncio async def test_ssl_rejects_unusable_value(): """A CA path passed as a bare string used to disable TLS silently.""" with pytest.raises(ValueError): Connection(ssl="/path/to/ca.pem") @pytest.mark.asyncio async def test_ping_reconnect_refreshes_password(mocker): class ReconnectAttempt(Exception): pass password_creator = mocker.Mock(return_value="fresh-password") connection = Connection(password_creator=password_creator) connection._connected = True mocker.patch.object( connection, "_execute_command", side_effect=OperationalError("connection lost"), ) mocker.patch.object( connection._loop, "create_connection", side_effect=ReconnectAttempt, ) with pytest.raises(ReconnectAttempt): await connection.ping(reconnect=True) password_creator.assert_called_once_with() @pytest.mark.asyncio async def test_transaction(connection): await connection.begin() await connection.query( """INSERT INTO test.asyncmy(`decimal`, date, datetime, `float`, string, `tinyint`) VALUES (%s,'%s','%s',%s,'%s',%s)""" % ( 1, "2020-08-08", "2020-08-08 00:00:00", 1, "1", 1, ), True, ) await connection.rollback() @pytest.mark.asyncio async def test_tls_connection_is_marked_secure(): """caching_sha2_password full auth sends the password in the clear over a secure channel; if _secure stays False it takes the RSA branch and the server rejects it with 1045.""" kwargs = {k: v for k, v in connection_kwargs.items() if k != "ssl"} connection = Connection(ssl=True, **kwargs) try: await connection.connect() except OperationalError: pytest.skip("server does not accept TLS connections") assert connection._secure await connection.ensure_closed() @pytest.mark.asyncio async def test_full_auth_over_tls(): """Regression test: clearing the server's auth cache forces full auth.""" admin = Connection(**connection_kwargs) await admin.connect() try: await admin.query("FLUSH PRIVILEGES") except Exception: await admin.ensure_closed() pytest.skip("cannot flush the server auth cache") await admin.ensure_closed() kwargs = {k: v for k, v in connection_kwargs.items() if k != "ssl"} connection = Connection(ssl=True, **kwargs) try: await connection.connect() except OperationalError as e: if e.args[0] == 1045: raise pytest.skip("server does not accept TLS connections") await connection.ensure_closed() python-asyncmy-0.2.15/tests/test_converters.py000066400000000000000000000025611525423006000215470ustar00rootroot00000000000000import datetime from asyncmy.converters import convert_datetime, escape_item, escape_str class CustomDate(datetime.date): pass def test_escape_item(): assert escape_item("\\\n\r\032\"'foobar\0", "utf-8") == "'\\\\\\n\\r\\Z\\\"\\'foobar\\0'" assert escape_item(datetime.date(2023, 6, 2), "utf-8") == "'2023-06-02'" assert escape_item(CustomDate(2023, 6, 2), "utf-8") == "'2023-06-02'" def test_escape_str(): assert escape_str("\\\n\r\032\"'foobar\0") == "'\\\\\\n\\r\\Z\\\"\\'foobar\\0'" # The encoder for the str type is a default encoder, # so it should accept values that are not strings as well. assert escape_str(datetime.date(2023, 6, 2)) == "'2023-06-02'" assert escape_str(CustomDate(2023, 6, 2)) == "'2023-06-02'" def test_convert_datetime(): assert convert_datetime("2023-06-02T23:06:20") == datetime.datetime(2023, 6, 2, 23, 6, 20) assert convert_datetime("2023-06-02 23:06:20") == datetime.datetime(2023, 6, 2, 23, 6, 20) # invalid datetime should be returned as str assert convert_datetime("0000-00-00 00:00:00") == "0000-00-00 00:00:00" def test_escape_unsigned_big_int(): # unsigned BIGINT max used to overflow the C `long long` parameter (#35) assert escape_item(2**64 - 1, "utf-8") == str(2**64 - 1) assert escape_item(-(2**63), "utf-8") == str(-(2**63)) assert escape_item(0, "utf-8") == str(0) python-asyncmy-0.2.15/tests/test_cursor.py000066400000000000000000000141601525423006000206700ustar00rootroot00000000000000import datetime from decimal import Decimal from enum import Enum import pytest from asyncmy.cursors import DictCursor, RE_INSERT_VALUES @pytest.mark.asyncio async def test_fetchone(connection): async with connection.cursor() as cursor: await cursor.execute("SELECT 1") ret = await cursor.fetchone() assert ret == (1,) @pytest.mark.asyncio async def test_fetchall(connection): async with connection.cursor() as cursor: await cursor.execute("SELECT 1") ret = await cursor.fetchall() assert ret == ((1,),) @pytest.mark.asyncio async def test_dict_cursor(connection): async with connection.cursor(cursor=DictCursor) as cursor: await cursor.execute("SELECT 1") ret = await cursor.fetchall() assert ret == [{"1": 1}] @pytest.mark.asyncio async def test_insert(connection): async with connection.cursor(cursor=DictCursor) as cursor: rows = await cursor.execute( """INSERT INTO test.asyncmy(id,`decimal`, date, datetime, time, `float`, string, `tinyint`) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)""", ( 1, 1, "2020-08-08", "2020-08-08 00:00:00", "00:00:00", 1, "1", 1, ), ) assert rows == 1 await cursor.execute("select * from test.asyncmy where id = %s", (cursor.lastrowid,)) result = await cursor.fetchall() assert result == [ { "id": 1, "decimal": Decimal("1.00"), "date": datetime.date(2020, 8, 8), "datetime": datetime.datetime(2020, 8, 8, 0, 0), "time": datetime.timedelta(hours=0, minutes=0, seconds=0), "float": 1.0, "string": "1", "tinyint": 1, } ] @pytest.mark.asyncio async def test_delete(connection): async with connection.cursor() as cursor: rows = await cursor.execute("delete from test.asyncmy where id = -1") assert rows == 0 @pytest.mark.asyncio async def test_executemany(connection): async with connection.cursor(cursor=DictCursor) as cursor: rows = await cursor.executemany( """INSERT INTO test.asyncmy(`decimal`, date, datetime, time, `float`, string, `tinyint`) VALUES (%s,%s,%s,%s,%s,%s,%s)""", [ ( 1, "2020-08-08", "2020-08-08 00:00:00", "00:00:00", 1, "1", 1, ), ( 1, "2020-08-08", "2020-08-08 00:00:00", "00:00:00", 1, "1", 1, ), ], ) assert rows == 2 @pytest.mark.asyncio async def test_table_ddl(connection): async with connection.cursor() as cursor: await cursor.execute("drop table if exists test.alter_table") create_table_sql = """ CREATE TABLE test.alter_table ( `id` int primary key auto_increment ) """ await cursor.execute(create_table_sql) add_column_sql = "alter table test.alter_table add column c varchar(20)" await cursor.execute(add_column_sql) await cursor.execute("drop table test.alter_table") class EnumValue(str, Enum): VALUE = "1" @pytest.mark.asyncio async def test_insert_enum(connection): async with connection.cursor(cursor=DictCursor) as cursor: rows = await cursor.execute( """INSERT INTO test.asyncmy(id, `decimal`, date, datetime, time, `float`, string, `tinyint`) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)""", ( -1, 1, "2020-08-08", "2020-08-08 00:00:00", "00:00:00", 1, EnumValue.VALUE, 1, ), ) assert rows == 1 def test_executemany_regex(): query = "INSERT INTO foo (bar, baz) VALUES (%s, %s)" match = RE_INSERT_VALUES.match(query) assert match is not None assert match.group(1) == "INSERT INTO foo (bar, baz) VALUES " assert match.group(2) == "(%s, %s)" assert match.group(3) == "" # Deprecated VALUES syntax query = "INSERT INTO foo (bar, baz) VALUES (%s, %s) ON DUPLICATE KEY UPDATE baz=VALUES(baz)" match = RE_INSERT_VALUES.match(query) assert match is not None assert match.group(1) == "INSERT INTO foo (bar, baz) VALUES " assert match.group(2) == "(%s, %s)" assert match.group(3) == " ON DUPLICATE KEY UPDATE baz=VALUES(baz)" # The new syntax query = "INSERT INTO foo (bar, baz) VALUES (%s, %s) AS new ON DUPLICATE KEY UPDATE baz=new.baz" match = RE_INSERT_VALUES.match(query) assert match is not None assert match.group(1) == "INSERT INTO foo (bar, baz) VALUES " assert match.group(2) == "(%s, %s)" assert match.group(3) == " AS new ON DUPLICATE KEY UPDATE baz=new.baz" # Test REPLACE VALUES query = "REPLACE INTO foo (bar, baz, aa, bb) VALUES (%s, %s, %s, %s)" match = RE_INSERT_VALUES.match(query) assert match is not None assert match.group(1) == "REPLACE INTO foo (bar, baz, aa, bb) VALUES " assert match.group(2) == "(%s, %s, %s, %s)" assert match.group(3) == "" # Test invalid queries query = "INSERT INTO foo (bar, baz) VALUES (%s, %s), (%s, %s)" match = RE_INSERT_VALUES.match(query) assert match is None @pytest.mark.asyncio async def test_cursor_is_awaitable(connection): """aiomysql's conn.cursor() is a coroutine; `await` must work here too.""" cursor = await connection.cursor() await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await cursor.close() @pytest.mark.asyncio async def test_awaited_cursor_honours_cursor_class(connection): cursor = await connection.cursor(DictCursor) await cursor.execute("SELECT 1 AS one") assert await cursor.fetchone() == {"one": 1} await cursor.close() python-asyncmy-0.2.15/tests/test_dbapi.py000066400000000000000000000013271525423006000204330ustar00rootroot00000000000000import asyncmy def test_pep249_globals(): assert asyncmy.apilevel == "2.0" assert asyncmy.threadsafety == 1 assert asyncmy.paramstyle == "pyformat" def test_exceptions_exported(): """PEP 249 requires the exception classes on the module.""" for name in ( "Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError", "IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError", ): assert issubclass(getattr(asyncmy, name), Exception), name assert issubclass(asyncmy.OperationalError, asyncmy.DatabaseError) assert issubclass(asyncmy.DatabaseError, asyncmy.Error) python-asyncmy-0.2.15/tests/test_errors.py000066400000000000000000000023761525423006000206750ustar00rootroot00000000000000from __future__ import annotations import struct import pytest from asyncmy import errors def _err_packet(errno: int, sqlstate: str | None, message: str) -> bytes: """Build an ERR_Packet body as the server sends it.""" packet = b"\xff" + struct.pack(" int: total = 0 for _ in range(iterations): # escape_str walks the shared escape table; escape_int and the # sequence path go through the shared encoders dict. total += len(escape_item("a 'quoted' \\ text", "utf8mb4")) total += len(escape_item(2**64 - 1, "utf8mb4")) total += len(escape_item(("a", 1), "utf8mb4")) # str() rather than .second/.seconds: these converters return the # input unchanged for illegal values, so their type is `object`. total += len(str(convert_datetime("2026-06-11 12:34:56"))) total += len(str(convert_timedelta("12:34:56"))) packet = MysqlPacket(packet_data, "utf8mb4") total += len(packet.read_length_coded_string()) total += len(packet.read_length_coded_string()) total += packet.read_length_encoded_integer() return total with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(worker, [1000] * 4)) # Same deterministic input in every thread: divergence would mean the # shared tables were being mutated underneath. assert results == [results[0]] * 4 python-asyncmy-0.2.15/tests/test_gtid.py000066400000000000000000000012411525423006000202760ustar00rootroot00000000000000from asyncmy.replication.gtid import Gtid, GtidSet GTID = "57b70f4e-20d3-11e5-a393-4a63946f7eac:1-56" OTHER = "4350f323-7565-4e59-8763-4b1b83a0ce0e:1-20" def test_gtid_is_hashable(): """GtidSet takes a set of Gtid, so Gtid must survive defining __eq__.""" assert hash(Gtid(GTID)) == hash(Gtid(GTID)) assert len({Gtid(GTID), Gtid(GTID)}) == 1 def test_distinct_gtids_stay_distinct(): assert len({Gtid(GTID), Gtid(OTHER)}) == 2 def test_gtid_set_accepts_a_set(): gtid_set = GtidSet({Gtid(GTID), Gtid(OTHER)}) assert Gtid(GTID) in gtid_set def test_gtid_set_is_hashable(): assert hash(GtidSet({Gtid(GTID)})) == hash(GtidSet({Gtid(GTID)})) python-asyncmy-0.2.15/tests/test_password_creator.py000066400000000000000000000071171525423006000227400ustar00rootroot00000000000000import asyncio import pytest import asyncmy from asyncmy import connect from asyncmy.connection import Connection from conftest import connection_kwargs PASSWORD = connection_kwargs["password"] NO_PASSWORD = {k: v for k, v in connection_kwargs.items() if k != "password"} @pytest.mark.asyncio async def test_sync_creator(): connection = await connect(password_creator=lambda: PASSWORD, **NO_PASSWORD) async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await connection.ensure_closed() @pytest.mark.asyncio async def test_async_creator(): async def creator(): return PASSWORD connection = await connect(password_creator=creator, **NO_PASSWORD) async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await connection.ensure_closed() @pytest.mark.asyncio async def test_creator_returning_an_awaitable(): """A creator wrapping an async client may hand back a Task, not a coroutine.""" async def fetch(): return PASSWORD connection = await connect( password_creator=lambda: asyncio.ensure_future(fetch()), **NO_PASSWORD ) await connection.ensure_closed() @pytest.mark.asyncio async def test_creator_returning_bytes(): connection = await connect(password_creator=lambda: PASSWORD.encode(), **NO_PASSWORD) await connection.ensure_closed() @pytest.mark.asyncio @pytest.mark.parametrize("value", [None, 1234, object()]) async def test_creator_returning_an_unusable_value(value): connection = Connection(password_creator=lambda: value, **NO_PASSWORD) with pytest.raises(ValueError, match="must return str or bytes"): await connection.connect() @pytest.mark.asyncio async def test_creator_takes_precedence_over_password(): connection = Connection( password="wrong-password", password_creator=lambda: PASSWORD, **NO_PASSWORD ) await connection.connect() assert connection._connected await connection.ensure_closed() @pytest.mark.asyncio async def test_creator_called_once_per_connection(): """The point of the hook: it runs for every connection, not just the first.""" calls = [] def creator(): calls.append(1) return PASSWORD for _ in range(3): connection = await connect(password_creator=creator, **NO_PASSWORD) await connection.ensure_closed() assert len(calls) == 3 @pytest.mark.asyncio async def test_pool_refreshes_credentials_per_connection(): calls = [] def creator(): calls.append(1) return PASSWORD pool = await asyncmy.create_pool(minsize=2, maxsize=2, password_creator=creator, **NO_PASSWORD) async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute("SELECT 1") pool.close() await pool.wait_closed() assert len(calls) >= 2 @pytest.mark.asyncio async def test_ping_reconnect_refreshes_credentials(): """A pooled connection that reconnects must not reuse an expired token.""" passwords = [PASSWORD, PASSWORD] def creator(): return passwords.pop(0) connection = await connect(password_creator=creator, **NO_PASSWORD) assert len(passwords) == 1 # Kill the connection underneath so the ping fails and reconnects. connection._transport.close() await connection.ping(reconnect=True) assert passwords == [] async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await connection.ensure_closed() python-asyncmy-0.2.15/tests/test_pool.py000066400000000000000000000133171525423006000203270ustar00rootroot00000000000000import asyncio import pytest from conftest import connection_kwargs import asyncmy from asyncmy.connection import Connection @pytest.mark.asyncio async def test_pool(pool): assert pool.minsize == 1 assert pool.maxsize == 10 assert pool.size == 1 assert pool.freesize == 1 @pytest.mark.asyncio async def test_pool_cursor(pool): async with pool.acquire() as conn: async with conn.cursor() as cursor: await cursor.execute("SELECT 1") ret = await cursor.fetchone() assert ret == (1,) @pytest.mark.asyncio async def test_acquire(pool): conn = await pool.acquire() assert isinstance(conn, Connection) assert pool.freesize == 0 assert pool.size == 1 assert conn.connected await pool.release(conn) assert pool.freesize == 1 assert pool.size == 1 @pytest.mark.asyncio async def test_wait_closed_wakes_on_in_transaction_release(): """Releasing the last connection while it is inside a transaction must wake a parked wait_closed(), see #154.""" pool = await asyncmy.create_pool(minsize=0, maxsize=4, autocommit=True, **connection_kwargs) conn = await pool.acquire() await conn.begin() async with conn.cursor() as cursor: await cursor.execute("SELECT 1") await cursor.fetchall() pool.close() waiter = asyncio.create_task(pool.wait_closed()) await asyncio.sleep(0.1) # let wait_closed() park on the condition assert not waiter.done() await pool.release(conn) await asyncio.wait_for(waiter, timeout=5) assert pool.size == 0 @pytest.mark.asyncio async def test_wait_closed_wakes_on_disconnected_release(): """Releasing a no-longer-connected connection must also wake wait_closed().""" pool = await asyncmy.create_pool(minsize=0, maxsize=4, autocommit=True, **connection_kwargs) conn = await pool.acquire() conn.close() pool.close() waiter = asyncio.create_task(pool.wait_closed()) await asyncio.sleep(0.1) assert not waiter.done() await pool.release(conn) await asyncio.wait_for(waiter, timeout=5) assert pool.size == 0 @pytest.mark.asyncio async def test_terminate_wakes_every_wait_closed_waiter(): """terminate() must wake every wait_closed() parked on the condition, not just one of them, see #157. Unlike release(), which frees exactly one connection and so should only wake one waiter, terminate() invalidates the wait condition for all of them at once (size drops to 0 immediately), so it needs notify_all() rather than notify(). """ pool = await asyncmy.create_pool(minsize=1, maxsize=3, **connection_kwargs) conn = await pool.acquire() # held, never released pool.close() waiters = [asyncio.create_task(pool.wait_closed()) for _ in range(3)] await asyncio.sleep(0.1) # let them all park on the condition assert all(not w.done() for w in waiters) pool.terminate() done, pending = await asyncio.wait(waiters, timeout=5) assert not pending, f"{len(pending)} waiter(s) never woke up after terminate()" assert len(done) == 3 for waiter in done: waiter.result() del conn @pytest.mark.asyncio async def test_cancel_wait_closed_closes_in_flight_connection(mocker): pool = await asyncmy.create_pool(minsize=2, maxsize=2, **connection_kwargs) victim = pool._free[0] transport = victim._transport entered = asyncio.Event() async def stalled_close(): entered.set() await asyncio.Future() mocker.patch.object(victim, "ensure_closed", side_effect=stalled_close) pool.close() waiter = asyncio.create_task(pool.wait_closed()) try: await asyncio.wait_for(entered.wait(), timeout=5) waiter.cancel() with pytest.raises(asyncio.CancelledError): await waiter pool.terminate() await asyncio.wait_for(pool.wait_closed(), timeout=5) assert pool.size == 0 assert pool._closed assert transport.is_closing() assert victim._transport is None finally: victim.close() waiter.cancel() await asyncio.gather(waiter, return_exceptions=True) pool.terminate() await pool.wait_closed() @pytest.mark.asyncio @pytest.mark.parametrize("cancel_first", [False, True]) async def test_concurrent_wait_closed_waits_for_in_flight_close(mocker, cancel_first): pool = await asyncmy.create_pool(minsize=1, maxsize=1, **connection_kwargs) conn = pool._free[0] original_close = conn.ensure_closed entered = asyncio.Event() proceed = asyncio.Event() async def slow_close(): entered.set() await proceed.wait() await original_close() close = mocker.patch.object(conn, "ensure_closed", side_effect=slow_close) pool.close() first = asyncio.create_task(pool.wait_closed()) waiters = [first] try: await asyncio.wait_for(entered.wait(), timeout=5) waiters.extend(asyncio.create_task(pool.wait_closed()) for _ in range(2)) await asyncio.sleep(0) assert all(not waiter.done() for waiter in waiters) assert not pool._closed if cancel_first: first.cancel() with pytest.raises(asyncio.CancelledError): await first else: proceed.set() await asyncio.wait_for(asyncio.gather(*waiters[1:]), timeout=5) if not cancel_first: await first assert conn._transport is None assert pool.size == 0 assert pool._closed close.assert_awaited_once() finally: proceed.set() for waiter in waiters: waiter.cancel() await asyncio.gather(*waiters, return_exceptions=True) conn.close() pool.terminate() await pool.wait_closed() python-asyncmy-0.2.15/tests/test_prepared.py000066400000000000000000000115241525423006000211560ustar00rootroot00000000000000import datetime from decimal import Decimal import pytest from asyncmy import errors @pytest.mark.asyncio async def test_prepared_type_roundtrip(connection): stmt = await connection.prepare("SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?") args = ( None, True, -12345, 2**63 - 1, 2**64 - 1, 3.25, "中文 with 'quotes' and \\backslash\0nul", b"\x00\x01\xff binary", datetime.datetime(2021, 3, 4, 5, 6, 7, 123456), datetime.date(2020, 2, 29), datetime.timedelta(hours=-25, minutes=-6, seconds=-17), Decimal("12345.6789"), ) result = await stmt.execute(args) row = result.rows[0] assert row[0] is None assert row[1] == 1 assert row[2] == -12345 assert row[3] == 2**63 - 1 assert row[4] == 2**64 - 1 assert row[5] == 3.25 assert row[6] == args[6] assert row[7] == args[7] assert str(row[8]).startswith("2021-03-04 05:06:07.123456") assert str(row[9]) == "2020-02-29" assert row[10] == datetime.timedelta(hours=-25, minutes=-6, seconds=-17) assert row[11] == Decimal("12345.6789") await stmt.close() @pytest.mark.asyncio async def test_prepared_table_roundtrip(connection): async with connection.cursor() as cur: await cur.execute("DROP TABLE IF EXISTS test.bin_prepared") await cur.execute( """CREATE TABLE test.bin_prepared ( id INT PRIMARY KEY AUTO_INCREMENT, ti TINYINT, tiu TINYINT UNSIGNED, si SMALLINT, mi MEDIUMINT, i INT, bi BIGINT, biu BIGINT UNSIGNED, yr YEAR, f FLOAT, d DOUBLE, dec_ DECIMAL(20,6), s VARCHAR(100), b VARBINARY(100), t TEXT, bl BLOB, dt DATETIME(6), da DATE, tm TIME(6), js JSON )""" ) try: ins = await connection.prepare( "INSERT INTO test.bin_prepared" " (ti,tiu,si,mi,i,bi,biu,yr,f,d,dec_,s,b,t,bl,dt,da,tm,js)" " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" ) result = await ins.execute( ( -128, 255, -32768, 8388607, -2147483648, -(2**63), 2**64 - 1, 2024, 1.5, 2.25, Decimal("999999999999.123456"), "varchar 值", b"\x00\xff", "text value", b"blob\x01", datetime.datetime(2021, 12, 31, 23, 59, 59, 999999), datetime.date(1999, 1, 1), datetime.timedelta(hours=100, minutes=30, seconds=15, microseconds=250000), '{"k": [1, 2, "v"]}', ) ) assert result.affected_rows == 1 assert result.insert_id == 1 # NULL bitmap covering all 19 parameters / 20 result columns result = await ins.execute((None,) * 19) assert result.insert_id == 2 await ins.close() async with await connection.prepare("SELECT * FROM test.bin_prepared ORDER BY id") as sel: result = await sel.execute() r1, r2 = result.rows assert r1[1:9] == (-128, 255, -32768, 8388607, -2147483648, -(2**63), 2**64 - 1, 2024) assert r1[9] == 1.5 and r1[10] == 2.25 assert r1[11] == Decimal("999999999999.123456") assert r1[12] == "varchar 值" and r1[13] == b"\x00\xff" assert r1[14] == "text value" and r1[15] == b"blob\x01" assert r1[16] == datetime.datetime(2021, 12, 31, 23, 59, 59, 999999) assert r1[17] == datetime.date(1999, 1, 1) assert r1[18] == datetime.timedelta(hours=100, minutes=30, seconds=15, microseconds=250000) assert all(v is None for v in r2[1:]) assert result.description[1][0] == "ti" finally: async with connection.cursor() as cur: await cur.execute("DROP TABLE IF EXISTS test.bin_prepared") @pytest.mark.asyncio async def test_prepared_reuse_and_interleave(connection): stmt = await connection.prepare("SELECT ? + ?") for i in range(100): result = await stmt.execute((i, 1)) assert result.rows[0][0] == i + 1 # text protocol interleaved on the same connection if i % 10 == 0: async with connection.cursor() as cur: await cur.execute("SELECT 1") assert (await cur.fetchone()) == (1,) await stmt.close() @pytest.mark.asyncio async def test_prepared_errors(connection): with pytest.raises(errors.ProgrammingError): await connection.prepare("SELECT * FROM test.no_such_table_xyz") stmt = await connection.prepare("SELECT ?") with pytest.raises(errors.ProgrammingError): await stmt.execute((1, 2)) # parameter count mismatch await stmt.close() with pytest.raises(errors.ProgrammingError): await stmt.execute((1,)) # closed statement python-asyncmy-0.2.15/tests/test_query_callback.py000066400000000000000000000066511525423006000223420ustar00rootroot00000000000000import pytest import asyncmy from asyncmy import connect from asyncmy.errors import ProgrammingError from conftest import connection_kwargs async def _connect(**extra): # connection_kwargs already carries echo, so extra must win over it. return await connect(**{**connection_kwargs, **extra}) @pytest.mark.asyncio async def test_callback_receives_query_and_duration(): calls = [] connection = await _connect(query_callback=lambda c, q, ms: calls.append((c, q, ms))) async with connection.cursor() as cursor: await cursor.execute("SELECT 1") await connection.ensure_closed() assert len(calls) == 1 cursor_arg, query, elapsed = calls[0] assert query == "SELECT 1" assert isinstance(elapsed, float) assert elapsed >= 0 assert isinstance(cursor_arg, asyncmy.cursors.Cursor) @pytest.mark.asyncio async def test_callback_measures_slow_queries(): calls = [] connection = await _connect(query_callback=lambda c, q, ms: calls.append((q, ms))) async with connection.cursor() as cursor: await cursor.execute("SELECT 1") await cursor.execute("DO SLEEP(0.2)") await connection.ensure_closed() durations = dict((q, ms) for q, ms in calls) assert durations["DO SLEEP(0.2)"] >= 200 assert durations["SELECT 1"] < durations["DO SLEEP(0.2)"] @pytest.mark.asyncio async def test_executemany_reports_once(): """The caller wrote one executemany; report it as one statement.""" calls = [] connection = await _connect(query_callback=lambda c, q, ms: calls.append(q)) async with connection.cursor() as cursor: await cursor.execute("CREATE TEMPORARY TABLE test.cb_test (a int)") await cursor.executemany("INSERT INTO test.cb_test VALUES (%s)", [(1,), (2,), (3,)]) await connection.ensure_closed() assert sum(1 for q in calls if q.startswith("INSERT")) == 1 @pytest.mark.asyncio async def test_failed_statement_is_not_reported(): """Like echo, which only logs after a statement succeeds.""" calls = [] connection = await _connect(query_callback=lambda c, q, ms: calls.append(q)) async with connection.cursor() as cursor: with pytest.raises(ProgrammingError): await cursor.execute("SELECT 1 FROM") # syntax error await connection.ensure_closed() assert calls == [] @pytest.mark.asyncio async def test_no_callback_by_default(): connection = await _connect() async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await connection.ensure_closed() @pytest.mark.asyncio async def test_echo_and_callback_are_independent(caplog): calls = [] connection = await _connect(echo=True, query_callback=lambda c, q, ms: calls.append(q)) with caplog.at_level("INFO", logger="asyncmy"): async with connection.cursor() as cursor: await cursor.execute("SELECT 1") await connection.ensure_closed() assert calls == ["SELECT 1"] assert any("SELECT 1" in record.message for record in caplog.records) @pytest.mark.asyncio async def test_pool_passes_callback_to_connections(): calls = [] pool = await asyncmy.create_pool( **connection_kwargs, query_callback=lambda c, q, ms: calls.append(q) ) async with pool.acquire() as connection, connection.cursor() as cursor: await cursor.execute("SELECT 1") pool.close() await pool.wait_closed() assert calls == ["SELECT 1"] python-asyncmy-0.2.15/tests/test_sock.py000066400000000000000000000101761525423006000203150ustar00rootroot00000000000000import asyncio import datetime import socket import ssl import pytest from asyncmy import connect from asyncmy.errors import OperationalError from conftest import connection_kwargs HOST = connection_kwargs["host"] PORT = connection_kwargs["port"] AUTH = {k: v for k, v in connection_kwargs.items() if k not in ("host", "port")} def _connected_sock(host=HOST, port=PORT): sock = socket.create_connection((host, port)) sock.setblocking(False) return sock @pytest.mark.asyncio async def test_connect_with_sock(): connection = await connect(sock=_connected_sock(), **AUTH) async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) await connection.ensure_closed() @pytest.mark.asyncio async def test_sock_cannot_be_reconnected(): """A used socket must fail loudly rather than silently dial host/port.""" connection = await connect(sock=_connected_sock(), **AUTH) connection.close() connection._connected = False with pytest.raises(OperationalError, match="Cannot reconnect"): await connection.connect() def _self_signed(tmp_path): from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID key = rsa.generate_private_key(public_exponent=65537, key_size=2048) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")]) now = datetime.datetime.now(datetime.timezone.utc) cert = ( x509.CertificateBuilder() .subject_name(name) .issuer_name(name) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - datetime.timedelta(minutes=5)) .not_valid_after(now + datetime.timedelta(hours=1)) .sign(key, hashes.SHA256()) ) cert_file = tmp_path / "cert.pem" key_file = tmp_path / "key.pem" cert_file.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) key_file.write_bytes( key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption(), ) ) return cert_file, key_file @pytest.mark.asyncio async def test_sock_with_externally_established_tls(tmp_path): """The Cloud SQL shape: a TLS proxy in front of MySQL, so TLS is set up by the caller and the in-protocol STARTTLS upgrade must be skipped.""" cert_file, key_file = _self_signed(tmp_path) async def pipe(reader, writer): try: while data := await reader.read(65536): writer.write(data) await writer.drain() except (ConnectionError, asyncio.CancelledError): pass finally: writer.close() async def handle(client_reader, client_writer): upstream_reader, upstream_writer = await asyncio.open_connection(HOST, PORT) await asyncio.gather( pipe(client_reader, upstream_writer), pipe(upstream_reader, client_writer) ) server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) server_ctx.load_cert_chain(cert_file, key_file) server = await asyncio.start_server(handle, "127.0.0.1", 0, ssl=server_ctx) proxy_port = server.sockets[0].getsockname()[1] async with server: client_ctx = ssl.create_default_context(cafile=str(cert_file)) client_ctx.check_hostname = False connection = await connect( sock=_connected_sock("127.0.0.1", proxy_port), ssl=client_ctx, host="localhost", **AUTH, ) assert connection._secure async with connection.cursor() as cursor: await cursor.execute("SELECT 1") assert await cursor.fetchone() == (1,) # Empty because the driver never ran STARTTLS: the encryption # lives between us and the proxy, not the proxy and the server. await cursor.execute("SHOW STATUS LIKE 'Ssl_cipher'") assert (await cursor.fetchone())[1] == "" await connection.ensure_closed() python-asyncmy-0.2.15/tests/test_stmt_cache.py000066400000000000000000000041311525423006000214620ustar00rootroot00000000000000import datetime from decimal import Decimal import pytest import asyncmy from conftest import connection_kwargs @pytest.mark.asyncio async def test_transparent_stmt_cache(): conn = await asyncmy.connect(stmt_cache_size=8, **connection_kwargs) try: async with conn.cursor() as cur: await cur.execute("SELECT %s + %s", (40, 2)) assert (await cur.fetchone()) == (42,) assert len(conn._stmt_cache) == 1 # cache hit await cur.execute("SELECT %s + %s", (1, 2)) assert (await cur.fetchone()) == (3,) assert len(conn._stmt_cache) == 1 # %% literal passthrough await cur.execute("SELECT %s LIKE '%%bc'", ("abc",)) assert (await cur.fetchone()) == (1,) # dict args fall back to the text protocol await cur.execute("SELECT %(a)s + 1", {"a": 9}) assert (await cur.fetchone()) == (10,) # multi-statement is unpreparable: silent text fallback await cur.execute("SELECT %s; SELECT 2", (1,)) assert (await cur.fetchone()) == (1,) assert await cur.nextset() assert (await cur.fetchall()) == ((2,),) # typed round-trip through the transparent binary path await cur.execute( "SELECT CAST(%s AS DATETIME(6)), CAST(%s AS DECIMAL(10,2))", (datetime.datetime(2022, 5, 6, 7, 8, 9, 100), Decimal("3.14")), ) assert (await cur.fetchone()) == ( datetime.datetime(2022, 5, 6, 7, 8, 9, 100), Decimal("3.14"), ) # LRU eviction respects the capacity for k in range(12): await cur.execute(f"SELECT {k} + %s", (1,)) assert len(conn._stmt_cache) <= 8 finally: await conn.ensure_closed() @pytest.mark.asyncio async def test_stmt_cache_disabled_by_default(connection): async with connection.cursor() as cur: await cur.execute("SELECT %s", (1,)) assert (await cur.fetchone()) == (1,) assert len(connection._stmt_cache) == 0 python-asyncmy-0.2.15/uv.lock000066400000000000000000004734501525423006000161170ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", "python_full_version < '3.10'", ] [[package]] name = "aiomysql" version = "0.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymysql" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, ] [[package]] name = "asyncmy" version = "0.2.15" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "cython" }, { name = "mypy" }, { name = "rich" }, { name = "ruff" }, { name = "stubgen-pyx" }, ] test = [ { name = "aiomysql" }, { name = "cryptography" }, { name = "mysqlclient", version = "2.2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mysqlclient", version = "2.2.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pymysql" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-asyncio" }, { name = "pytest-mock" }, { name = "pytest-xdist" }, { name = "uvloop", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ { name = "cython", specifier = ">=3.1" }, { name = "mypy" }, { name = "rich" }, { name = "ruff" }, { name = "stubgen-pyx", specifier = ">=0.2.18" }, ] test = [ { name = "aiomysql" }, { name = "cryptography" }, { name = "mysqlclient" }, { name = "pymysql", specifier = ">=0.8.1" }, { name = "pytest" }, { name = "pytest-asyncio", specifier = ">=0.21.2,<0.22" }, { name = "pytest-mock" }, { name = "pytest-xdist" }, { name = "uvloop", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "cryptography" version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, ] [[package]] name = "cython" version = "3.2.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" }, { url = "https://files.pythonhosted.org/packages/7d/3d/b26f29092c71c36e0462752885bdfb18c23c176af4de953fdae2772a8941/cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845", size = 3370134, upload-time = "2026-01-04T14:14:53.627Z" }, { url = "https://files.pythonhosted.org/packages/56/9e/539fb0d09e4f5251b5b14f8daf77e71fee021527f1013791038234618b6b/cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02", size = 3537552, upload-time = "2026-01-04T14:14:56.852Z" }, { url = "https://files.pythonhosted.org/packages/10/c6/82d19a451c050d1be0f05b1a3302267463d391db548f013ee88b5348a8e9/cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86", size = 2766191, upload-time = "2026-01-04T14:14:58.709Z" }, { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" }, { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" }, { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" }, { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" }, { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" }, { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" }, { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" }, { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" }, { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" }, { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" }, { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" }, { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" }, { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" }, { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" }, { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" }, { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" }, { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" }, { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" }, { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" }, { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" }, { url = "https://files.pythonhosted.org/packages/fc/71/65cff050e6ce74e767910ffeb17f4eb1f8f12785cfec0a095bf3ca628b77/cython-3.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8d685a70bce39acc1d62ec3916d9b724b5ef665b0ce25ae55e1c85ee09747fc", size = 2970488, upload-time = "2026-01-04T14:15:57.013Z" }, { url = "https://files.pythonhosted.org/packages/95/4a/286c52a3566dffb35880b4e27fac20ef6603996040fd1121beec1cad292f/cython-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca578c9cb872c7ecffbe14815dc4590a003bc13339e90b2633540c7e1a252839", size = 3376192, upload-time = "2026-01-04T14:15:58.716Z" }, { url = "https://files.pythonhosted.org/packages/c3/1a/416cc55d567f94b1249a0e007951115e2f7d296eabc345276c3859af76c9/cython-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b84d4e3c875915545f77c88dba65ad3741afd2431e5cdee6c9a20cefe6905647", size = 3544002, upload-time = "2026-01-04T14:16:00.915Z" }, { url = "https://files.pythonhosted.org/packages/b7/1f/d24ee88f58f6c97001ec919cebec9e3aabcbe5489be045ceaf49e983522d/cython-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:fdfdd753ad7e18e5092b413e9f542e8d28b8a08203126090e1c15f7783b7fe57", size = 2772062, upload-time = "2026-01-04T14:16:02.81Z" }, { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "execnet" version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "isort" version = "6.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "importlib-metadata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, ] [[package]] name = "isort" version = "8.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] name = "librt" version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, { url = "https://files.pythonhosted.org/packages/01/1f/c7d8b66a3ca3ca3ed8ded4b32c96ee58a45920ebbbaa934355c74adcc33e/librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac", size = 65990, upload-time = "2026-02-17T16:12:48.972Z" }, { url = "https://files.pythonhosted.org/packages/56/be/ee9ba1730052313d08457f19beaa1b878619978863fba09b40aed5b5c123/librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed", size = 68640, upload-time = "2026-02-17T16:12:50.24Z" }, { url = "https://files.pythonhosted.org/packages/81/27/b7309298b96f7690cec3ceee38004c1a7f60fcd96d952d3ac344a1e3e8b3/librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd", size = 196099, upload-time = "2026-02-17T16:12:52.788Z" }, { url = "https://files.pythonhosted.org/packages/10/48/160a5aacdcb21824b10a52378c39e88c46a29bb31efdaf3910dd1f9b670e/librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851", size = 206663, upload-time = "2026-02-17T16:12:55.017Z" }, { url = "https://files.pythonhosted.org/packages/ee/65/33dd1d8caabb7c6805d87d095b143417dc96b0277c06ffa0508361422c82/librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128", size = 219318, upload-time = "2026-02-17T16:12:56.145Z" }, { url = "https://files.pythonhosted.org/packages/09/d4/353805aa6181c7950a2462bd6e855366eeca21a501f375228d72a51547df/librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac", size = 212191, upload-time = "2026-02-17T16:12:57.326Z" }, { url = "https://files.pythonhosted.org/packages/06/08/725b3f304d61eba56c713c251fb833a06d84bf93381caad5152366f5d2bb/librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551", size = 220672, upload-time = "2026-02-17T16:12:58.497Z" }, { url = "https://files.pythonhosted.org/packages/0e/55/e8cdf04145872b3b97cb9b68287b22d1c08348227063f305aec11a3e6ce7/librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5", size = 216172, upload-time = "2026-02-17T16:12:59.751Z" }, { url = "https://files.pythonhosted.org/packages/8f/d8/23b1c6592d2422dd6829c672f45b1f1c257f219926b0d216fedb572d0184/librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6", size = 214116, upload-time = "2026-02-17T16:13:01.056Z" }, { url = "https://files.pythonhosted.org/packages/c9/92/2b44fd3cc3313f44e43bdbb41343735b568fa675fa351642b408ee48d418/librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed", size = 236664, upload-time = "2026-02-17T16:13:02.314Z" }, { url = "https://files.pythonhosted.org/packages/00/23/92313ecdab80e142d8ea10e8dfa6297694359dbaacc9e81679bdc8cbceb6/librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc", size = 54368, upload-time = "2026-02-17T16:13:03.549Z" }, { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mypy" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "mysqlclient" version = "2.2.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/24/cdaaef42aac7d53c0a01bb638da64961c293b1b6d204efd47400a68029d4/mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22", size = 207748, upload-time = "2025-01-10T11:56:24.357Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3e2de3f93cd60dd63bd229ec3e3b679f682982614bf513d046c2722aa4ce/mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687", size = 207745, upload-time = "2025-01-10T11:56:28.67Z" }, { url = "https://files.pythonhosted.org/packages/bb/b5/2a8a4bcba3440550f358b839638fe8ec9146fa3c9194890b4998a530c926/mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e", size = 208032, upload-time = "2025-01-10T11:56:29.879Z" }, { url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" }, { url = "https://files.pythonhosted.org/packages/0e/e0/524b0777524e0d410f71987f556dd6a0e3273fdb06cd6e91e96afade7220/mysqlclient-2.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:199dab53a224357dd0cb4d78ca0e54018f9cee9bf9ec68d72db50e0a23569076", size = 207857, upload-time = "2025-01-10T11:56:33.666Z" }, { url = "https://files.pythonhosted.org/packages/16/cc/5b1570be9f8597ee41e2a0bd7b62ba861ec2c81898d9449f3d6bfbe15d29/mysqlclient-2.2.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585", size = 207800, upload-time = "2025-01-10T11:56:36.023Z" }, { url = "https://files.pythonhosted.org/packages/20/40/b5d03494c1caa8f4da171db41d8d9d5b0d8959f893761597d97420083362/mysqlclient-2.2.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:977e35244fe6ef44124e9a1c2d1554728a7b76695598e4b92b37dc2130503069", size = 207965, upload-time = "2025-01-10T11:56:37.252Z" }, ] [[package]] name = "mysqlclient" version = "2.2.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/eb/b0/9df076488cb2e536d40ce6dbd4273c1f20a386e31ffe6e7cb613902b3c2a/mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199", size = 92287, upload-time = "2026-02-10T10:58:37.405Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/d0/22b896a2f8f797ee3517a0c1c1fe576cd38768cd25e2a7362a7277725ed4/mysqlclient-2.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:86db31bba7b3480fec2751350e9790e24f016f89af33a87bab7e79f7196474e8", size = 206873, upload-time = "2026-02-10T10:58:40.996Z" }, { url = "https://files.pythonhosted.org/packages/44/f6/6253d116c024aeb11fb193ad14e2bae2bd3380690e885028abf798b1b0b4/mysqlclient-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92", size = 206875, upload-time = "2026-02-10T10:58:42.223Z" }, { url = "https://files.pythonhosted.org/packages/db/1f/7d6b3fbfc8a317805fdb8bbfbc23af99f2090497090e82513daf491a29ce/mysqlclient-2.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93", size = 207171, upload-time = "2026-02-10T10:58:39.827Z" }, { url = "https://files.pythonhosted.org/packages/e6/d5/76e369b0fdccd2eb9ed7d890e4e3e23aa1344fea62f0180d7f1574285e54/mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300", size = 207158, upload-time = "2026-02-10T10:58:38.663Z" }, { url = "https://files.pythonhosted.org/packages/42/44/6ae21b4e8490eb5167d04fa7452c462ffd72a502b96bf531477ef044a380/mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0", size = 211644, upload-time = "2026-02-10T10:58:45.022Z" }, { url = "https://files.pythonhosted.org/packages/66/e5/037d55623be9f681236e04abe12e1290847c06bd48270c3f19ac33493cbf/mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99", size = 213067, upload-time = "2026-02-10T10:58:43.389Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pycparser" version = "2.23" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pymysql" version = "1.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-asyncio" version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/53/57663d99acaac2fcdafdc697e52a9b1b7d6fcf36616281ff9768a44e7ff3/pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45", size = 30656, upload-time = "2024-04-29T13:23:24.738Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9c/ce/1e4b53c213dce25d6e8b163697fbce2d43799d76fa08eea6ad270451c370/pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b", size = 13368, upload-time = "2024-04-29T13:23:23.126Z" }, ] [[package]] name = "pytest-mock" version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "rich" version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "ruff" version = "0.15.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] [[package]] name = "stubgen-pyx" version = "0.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cython" }, { name = "isort", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "isort", version = "8.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/d0/d56fdbd7619c1e6fa3235558d09481ad04d1941d0d42fd6c72518ce17bfd/stubgen_pyx-0.2.18.tar.gz", hash = "sha256:3d4cd9300cee0552c17d0a9a173e59e7844f9eb97cd5dd411d2f772084f3ec70", size = 82125, upload-time = "2026-08-10T22:36:13.18Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/37/00/9502fa4b948121fea23533a2fdf449d692470054d33eb7a2545380bc8950/stubgen_pyx-0.2.18-py3-none-any.whl", hash = "sha256:0c9ea90c3615619b0f47f0b42d2cba46fbbe9d50ffb775dd615fa131fcbd2702", size = 59080, upload-time = "2026-08-10T22:36:12.157Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "uvloop" version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" }, { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" }, { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" }, { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" }, { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" }, { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" }, ] [[package]] name = "zipp" version = "3.23.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ]