pax_global_header00006660000000000000000000000064147232747540014531gustar00rootroot0000000000000052 comment=0183ef4ad17a91cffb7d93d9b2c1d963166005b3 python-asyncmy-0.2.10/000077500000000000000000000000001472327475400146135ustar00rootroot00000000000000python-asyncmy-0.2.10/.github/000077500000000000000000000000001472327475400161535ustar00rootroot00000000000000python-asyncmy-0.2.10/.github/FUNDING.yml000066400000000000000000000000501472327475400177630ustar00rootroot00000000000000custom: ["https://sponsor.long2ice.io"] python-asyncmy-0.2.10/.github/workflows/000077500000000000000000000000001472327475400202105ustar00rootroot00000000000000python-asyncmy-0.2.10/.github/workflows/ci.yml000066400000000000000000000040421472327475400213260ustar00rootroot00000000000000name: ci on: push: branches-ignore: - main pull_request: jobs: 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: matrix: python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-pip- - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install and configure Poetry run: | pip install -U pip poetry poetry config virtualenvs.create false pip3 install wheel setuptools pip --upgrade - name: Run ci run: make ci 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: matrix: python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-pip- - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install and configure Poetry run: | pip install -U pip poetry poetry config virtualenvs.create false pip3 install wheel setuptools pip --upgrade - name: Run ci run: make ci python-asyncmy-0.2.10/.github/workflows/pypi.yml000066400000000000000000000023731472327475400217210ustar00rootroot00000000000000name: pypi on: release: types: - created jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-13, macos-14 ] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 env: CIBW_SKIP: "pp* cp36-* cp37-* *-win32 *-manylinux_i686 *-musllinux_i686" - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl build_sdist: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: abatilo/actions-poetry@v3 - name: Build run: poetry build - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz upload: runs-on: ubuntu-latest needs: [ build_wheels, build_sdist ] steps: - uses: actions/download-artifact@v4 with: name: artifact path: dist - uses: pypa/gh-action-pypi-publish@v1.6.4 with: user: __token__ password: ${{ secrets.pypi_password }} python-asyncmy-0.2.10/.gitignore000066400000000000000000000034461472327475400166120ustar00rootroot00000000000000# 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.10/CHANGELOG.md000066400000000000000000000032361472327475400164300ustar00rootroot00000000000000# ChangeLog ## 0.2 ### 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.10/LICENSE000066400000000000000000000261351472327475400156270ustar00rootroot00000000000000 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.10/Makefile000066400000000000000000000012721472327475400162550ustar00rootroot00000000000000checkfiles = asyncmy/ tests/ examples/ conftest.py build.py py_warn = PYTHONDEVMODE=1 MYSQL_PASS ?= "123456" up: @poetry update deps: @poetry install _style: @isort -src $(checkfiles) @black $(checkfiles) style: deps _style _check: @black --check $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false) @ruff check $(checkfiles) @mypy $(checkfiles) 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 @poetry build benchmark: deps @python benchmark/main.py ci: deps _check _test python-asyncmy-0.2.10/README.md000066400000000000000000000123451472327475400160770ustar00rootroot00000000000000# asyncmy - A fast asyncio MySQL/MariaDB driver [![image](https://img.shields.io/pypi/v/asyncmy.svg?style=flat)](https://pypi.python.org/pypi/asyncmy) [![image](https://img.shields.io/github/license/long2ice/asyncmy)](https://github.com/long2ice/asyncmy) [![pypi](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml) [![ci](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml) ## Introduction `asyncmy` is a fast asyncio MySQL/MariaDB driver, which reuse most of [pymysql](https://github.com/PyMySQL/PyMySQL) and [aiomysql](https://github.com/aio-libs/aiomysql) but rewrite core protocol with [cython](https://cython.org/) to speedup. ## Features - API compatible with [aiomysql](https://github.com/aio-libs/aiomysql). - Faster by [cython](https://cython.org/). - MySQL replication protocol support with `asyncio`. - Tested both MySQL and MariaDB in [CI](https://github.com/long2ice/asyncmy/blob/dev/.github/workflows/ci.yml). ## Benchmark The result comes from [benchmark](./benchmark). > The device is iMac Pro(2017) i9 3.6GHz 48G and MySQL version is 8.0.26. ![benchmark](./images/benchmark.png) ### Conclusion - There is no doubt that `mysqlclient` is the fastest MySQL driver. - All kinds of drivers have a small gap except `select`. - `asyncio` could enhance `insert`. - `asyncmy` performs remarkable when compared to other drivers. ## Install ```shell pip install asyncmy ``` ### Installing on Windows To install asyncmy on Windows, you need to install the tools needed to build it. 1. Download *Microsoft C++ Build Tools* from https://visualstudio.microsoft.com/visual-cpp-build-tools/ 2. Run CMD as Admin (not required but recommended) and navigate to the folder when your installer is downloaded 3. Installer executable should look like this `vs_buildtools__XXXXXXXXX.XXXXXXXXXX.exe`, it will be easier if you rename it to just `vs_buildtools.exe` 4. Run this command (Make sure you have about 5-6GB of free storage) ```shell vs_buildtools.exe --norestart --passive --downloadThenInstall --includeRecommended --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools ``` 5. Wait until the installation is finished 6. After installation will finish, restart your computer 7. Install asyncmy via PIP ```shell pip install asyncmy ``` Now you can uninstall previously installed tools. ## Usage ### Use `connect` `asyncmy` provides a way to connect to MySQL database with simple factory function `asyncmy.connect()`. Use this function if you want just one connection to the database, consider connection pool for multiple connections. ```py import asyncio import os from asyncmy import connect from asyncmy.cursors import DictCursor async def run(): 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(run()) ``` ### Use `pool` `asyncmy` provides connection pool as well as plain Connection objects. ```py import asyncmy import asyncio async def run(): pool = await asyncmy.create_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,) pool.close() await pool.wait_closed() if __name__ == '__main__': asyncio.run(run()) ``` ## Replication `asyncmy` supports MySQL replication protocol like [python-mysql-replication](https://github.com/noplay/python-mysql-replication), but powered by `asyncio`. ```py from asyncmy import connect from asyncmy.replication import BinLogStream import asyncio async def run(): conn = await connect() ctl_conn = await connect() stream = BinLogStream( conn, ctl_conn, 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(run()) ``` ## ThanksTo > asyncmy is build on top of these awesome projects. - [pymysql](https://github/pymysql/PyMySQL), a pure python MySQL client. - [aiomysql](https://github.com/aio-libs/aiomysql), a library for accessing a MySQL database from the asyncio. - [python-mysql-replication](https://github.com/noplay/python-mysql-replication), pure Python Implementation of MySQL replication protocol build on top of PyMYSQL. ## License This project is licensed under the [Apache-2.0](./LICENSE) License. python-asyncmy-0.2.10/asyncmy/000077500000000000000000000000001472327475400162765ustar00rootroot00000000000000python-asyncmy-0.2.10/asyncmy/__init__.py000066400000000000000000000001521472327475400204050ustar00rootroot00000000000000from .connection import Connection, connect # noqa:F401 from .pool import Pool, create_pool # noqa:F401 python-asyncmy-0.2.10/asyncmy/auth.py000066400000000000000000000153101472327475400176110ustar00rootroot00000000000000""" 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.10/asyncmy/charset.pxd000066400000000000000000000000621472327475400204420ustar00rootroot00000000000000cdef dict MB_LENGTH = {8: 1, 33: 3, 88: 2, 91: 2} python-asyncmy-0.2.10/asyncmy/charset.pyx000066400000000000000000000243751472327475400205040ustar00rootroot00000000000000cdef 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.10/asyncmy/connection.pyx000066400000000000000000001451501472327475400212050ustar00rootroot00000000000000# 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 os import socket import sys import warnings from asyncio import StreamReader, StreamWriter from typing import Optional, Type 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) from .constants.CLIENT import (CAPABILITIES, CONNECT_ATTRS, CONNECT_WITH_DB, 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) from .constants.CR import (CR_COMMANDS_OUT_OF_SYNC, CR_CONN_HOST_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_STATUS_AUTOCOMMIT, SERVER_STATUS_IN_TRANS, SERVER_STATUS_NO_BACKSLASH_ESCAPES) from .contexts import _ConnectionContextManager from .structs import B_, BHHB, HBB, 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): # KeyError occurs when there's no entry in OS database for a current user. 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 cdef _pack_int24(int n): return I.pack(n)[:3] # 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 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 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 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 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="", host=None, database=None, unix_socket=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, ssl=None, 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") client_flag |= SSL self._ssl_context = self._create_ssl_ctx(ssl) self._echo = echo 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"" if isinstance(self._password, str): self._password = self._password.encode("latin1") self._db = database self._unix_socket = unix_socket 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._reader: Optional[StreamReader] = None self._writer: Optional[StreamWriter] = None def _create_ssl_ctx(self, sslp): if isinstance(sslp, ssl.SSLContext): return sslp elif not isinstance(sslp, dict): return 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._writer: self._writer.transport.close() self._writer = None self._reader = None @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): """Close connection without QUIT message.""" if self._connected: send_data = i.pack(1) + B.pack(COM_QUIT) self._write_bytes(send_data) await self._writer.drain() self._writer.close() await self._writer.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): transport = self._writer.transport transport.pause_reading() raw_sock = 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) transport.resume_reading() def _set_nodelay(self, value): flag = int(bool(value)) transport = self._writer.transport transport.pause_reading() raw_sock = 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) transport.resume_reading() 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) return self._cursor_cls(self, echo=self._echo) # 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 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: 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 connect(self): if self._connected: return self._reader, self._writer try: if self._unix_socket: self._reader, self._writer = await asyncio.wait_for(asyncio.open_unix_connection(self._unix_socket), timeout=self._connect_timeout, ) self.host_info = "Localhost via UNIX socket" self._secure = True else: while True: try: self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection( self._host, self._port, ), timeout=self._connect_timeout) 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) if not self._unix_socket: 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. data = _pack_int24(len(payload)) + B.pack(self._next_seq_id) + payload self._write_bytes(data) self._next_seq_id = (self._next_seq_id + 1) % 256 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. """ buff = bytearray() while True: packet_header = await self._read_bytes(4) btrl, btrh, packet_number = HBB.unpack(packet_header) bytes_to_read = btrl + (btrh << 16) 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) % 256 recv_data = await self._read_bytes(bytes_to_read) buff.extend(recv_data) # https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html if bytes_to_read == 0xFFFFFF: continue if bytes_to_read < MAX_PACKET_LEN: break packet = packet_type(bytes(buff), 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): try: if self._read_timeout: try: data = await asyncio.wait_for(self._reader.readexactly(num_bytes), self._read_timeout) except asyncio.TimeoutError: await self.ensure_closed() raise else: data = await self._reader.readexactly(num_bytes) except (IOError, OSError, asyncio.TimeoutError) as e: raise errors.OperationalError( CR_SERVER_LOST, "Lost connection to MySQL server during query (%s)" % (e,), ) except asyncio.IncompleteReadError as e: msg = "Lost connection to MySQL server during query" raise errors.OperationalError(CR_SERVER_LOST, msg) from e return data def _write_bytes(self, bytes data): self._writer.write(data) 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, "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) self._write_bytes(prelude + sql[: packet_size - 1]) 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._user is None: raise ValueError("Did not specify a username") charset_id = charset_by_name(self._charset).id if self._ssl_context: # 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 data_received self._writer.transport.pause_reading() # Get the raw socket from the transport raw_sock = self._writer.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._writer.transport.close() # MySQL expects TLS negotiation to happen in the middle of a # TCP connection not at start. Passing in a socket to # open_connection will cause it to negotiate TLS on an existing # connection not initiate a new one. self._reader, self._writer = await asyncio.open_connection( sock=raw_sock, ssl=self._ssl_context, server_hostname=self._host, ) if isinstance(self._user, str): self._user = self._user.encode(self._encoding) 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 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 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 cdef class MySQLResult: cdef: public connection public bytes message public int affected_rows, warning_count, field_count, server_status, unbuffered_active, has_next public list fields, converters public unsigned long insert_id public tuple rows, description def __init__(self, connection: Connection): self.connection = connection 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 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 not packet.is_eof_packet(): return False # TODO: Support DEPRECATE_EOF # 1) Add DEPRECATE_EOF to CAPABILITIES # 2) Mask CAPABILITIES with server_capabilities # 3) if server_capabilities & DEPRECATE_EOF: use OKPacketWrapper instead of EOFPacketWrapper 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 # EOF packet = await self.connection.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.""" rows = [] while True: packet = await self.connection.read_packet() if self._check_packet_is_eof(packet): self.connection = None # release reference to kill cyclic reference. break rows.append(self._read_row_from_packet(packet)) self.affected_rows = len(rows) self.rows = tuple(rows) cdef _read_row_from_packet(self, packet: MysqlPacket): row = [] for encoding, converter in self.converters: try: data = packet.read_length_coded_string() except IndexError: # No more columns in this row # See https://github.com/PyMySQL/PyMySQL/pull/434 break if data is not None: if encoding is not None: data = data.decode(encoding) if converter is not None: data = converter(data) row.append(data) return tuple(row) async def _get_descriptions(self): """Read a column descriptor packet for each column in the result.""" self.fields = [] self.converters = [] 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 self.converters.append((encoding, converter)) 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 await conn.write_packet(chunk) 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 await conn.write_packet(b"") def connect(user=None, password="", host=None, database=None, unix_socket=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, server_public_key=None, ssl=None, db=None, # deprecated ): coro = _connect( user=user, password=password, host=host, database=database, unix_socket=unix_socket, 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, ssl=ssl, db=db, # deprecated ) return _ConnectionContextManager(coro) async def _connect( **kwargs, ) -> Connection: conn = Connection( **kwargs, ) await conn.connect() return conn python-asyncmy-0.2.10/asyncmy/constants/000077500000000000000000000000001472327475400203125ustar00rootroot00000000000000python-asyncmy-0.2.10/asyncmy/constants/CLIENT.py000066400000000000000000000015561472327475400216510ustar00rootroot00000000000000# 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.10/asyncmy/constants/COLUMN.py000066400000000000000000000002011472327475400216520ustar00rootroot00000000000000NULL_COLUMN = 251 UNSIGNED_CHAR_COLUMN = 251 UNSIGNED_SHORT_COLUMN = 252 UNSIGNED_INT24_COLUMN = 253 UNSIGNED_INT64_COLUMN = 254 python-asyncmy-0.2.10/asyncmy/constants/COMMAND.py000066400000000000000000000012471472327475400217460ustar00rootroot00000000000000COM_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.10/asyncmy/constants/CR.py000066400000000000000000000035701472327475400211750ustar00rootroot00000000000000# 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.10/asyncmy/constants/ER.py000066400000000000000000000300101472327475400211640ustar00rootroot00000000000000ERROR_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.10/asyncmy/constants/FIELD_TYPE.py000066400000000000000000000005621472327475400223530ustar00rootroot00000000000000DECIMAL = 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.10/asyncmy/constants/FLAG.py000066400000000000000000000003261472327475400213760ustar00rootroot00000000000000NOT_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.10/asyncmy/constants/SERVER_STATUS.py000066400000000000000000000005151472327475400227760ustar00rootroot00000000000000SERVER_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.10/asyncmy/constants/__init__.py000066400000000000000000000000001472327475400224110ustar00rootroot00000000000000python-asyncmy-0.2.10/asyncmy/contexts.py000066400000000000000000000045361472327475400205270ustar00rootroot00000000000000from collections.abc import Coroutine from typing import Any, Iterator class _ContextManager(Coroutine): __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) -> Any: return self._coro.__await__() async def __aenter__(self) -> Any: 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): async def __aexit__(self, exc_type, exc, tb) -> None: self._obj.close() await self._obj.wait_closed() self._obj = None class _PoolAcquireContextManager(_ContextManager): __slots__ = ("_coro", "_conn", "_pool") def __init__(self, coro, pool) -> None: super().__init__(coro) self._coro = coro self._conn = None self._pool = pool async def __aenter__(self) -> Any: 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): 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.10/asyncmy/converters.pyx000066400000000000000000000226721472327475400212430ustar00rootroot00000000000000import re import time from decimal import Decimal from cpython cimport datetime from .constants.FIELD_TYPE import * from .errors import ProgrammingError 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 in (escape_dict, escape_sequence): val = encoder(val, charset, mapping) else: val = encoder(val, mapping) return val cpdef dict escape_dict(dict val, str charset, mapping: dict = None): n = {} for k, v in val.items(): quoted = escape_item(v, charset, mapping) n[k] = quoted return n 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(long long value, mapping: dict = None): 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("'")] = "\\'" cpdef str escape_string(value, mapping: dict = None): """ escapes *value* without adding quote. Value should be unicode """ return value.translate(_escape_table) cpdef str escape_bytes_prefixed(bytes value, mapping: dict = None): return "_binary'%s'" % value.decode(b"ascii", "surrogateescape").translate(_escape_table) cpdef str escape_bytes(bytes value, mapping: dict = None): return "'%s'" % value.decode(b"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 datetime.datetime convert_datetime(str 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 None: >>> convert_datetime('2007-02-31T23:06:20') is None True >>> convert_datetime('0000-00-00 00:00:00') is None True """ if isinstance(obj, (bytes, bytearray)): obj = obj.decode("ascii") 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 datetime.timedelta convert_timedelta(str 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 None: >>> convert_timedelta('random crap') is None True 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. """ if isinstance(obj, (bytes, 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 datetime.time convert_time(str obj): """Returns a TIME column as a time object: >>> convert_time('15:06:17') datetime.time(15, 6, 17) Illegal values are returned as None: >>> convert_time('-25:06:17') is None True >>> convert_time('random crap') is None True 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. """ if isinstance(obj, (bytes, bytearray)): obj = obj.decode("ascii") 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 datetime.date 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 None: >>> convert_date('2007-02-31') is None True >>> convert_date('0000-00-00') is None True """ if isinstance(obj, (bytes, bytearray)): obj = obj.decode("ascii") 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, dict: escape_dict, 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.10/asyncmy/cursors.pyx000066400000000000000000000420461472327475400205460ustar00rootroot00000000000000import 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*(?:ON DUPLICATE.*)?);?\s*\Z", re.IGNORECASE | re.DOTALL, ) logger = logging.getLogger(__package__) if typing.TYPE_CHECKING: from asyncmy.connection import Connection 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, rowcount, arraysize, _echo public tuple description public connection, _loop, _executed, _result, _rows public unsigned long lastrowid def __init__(self, connection: "Connection", echo: bool = False): 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._loop = self.connection.loop 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 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 query = self.mogrify(query, args) start = time.time() result = await self._query(query) end = time.time() self._executed = query if self._echo: logger.info(f"[{round((end - start) * 1000, 2)}ms] {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) 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 if isinstance(prefix, str): prefix = prefix.encode(encoding) if isinstance(postfix, str): postfix = postfix.encode(encoding) sql = bytearray(prefix) args = iter(args) v = values % escape(next(args), conn) if isinstance(v, str): v = v.encode(encoding, "surrogateescape") sql += v rows = 0 for arg in args: v = values % escape(arg, conn) if isinstance(v, str): v = v.encode(encoding, "surrogateescape") if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: rows += await self.execute(sql + postfix) sql = bytearray(prefix) else: sql += b"," sql += v rows += await self.execute(sql + postfix) 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 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 cpdef fetchone(self): """Fetch the next row.""" self._check_executed() fut = self._loop.create_future() if self._rows is None or self.rownumber >= len(self._rows): fut.set_result(None) return fut result = self._rows[self.rownumber] self.rownumber += 1 fut.set_result(result) return fut cpdef fetchmany(self, size=None): """Fetch several rows.""" self._check_executed() fut = self._loop.create_future() if self._rows is None: fut.set_result([]) return fut end = self.rownumber + (size or self.arraysize) result = self._rows[self.rownumber: end] self.rownumber = min(end, len(self._rows)) fut.set_result(result) return fut cpdef fetchall(self): """Fetch all the rows.""" self._check_executed() fut = self._loop.create_future() if self._rows is None: fut.set_result([]) return fut if self.rownumber: result = self._rows[self.rownumber:] else: result = self._rows self.rownumber = len(self._rows) fut.set_result(result) return fut cpdef 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 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: 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.10/asyncmy/errors.pyx000066400000000000000000000076441472327475400203670ustar00rootroot00000000000000from .constants.ER import * from .structs import H cdef class MySQLError(Exception): """Exception related to operation with MySQL.""" 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): errno = H.unpack(data[1:3])[0] if data[3] == ord("#"): err_val = data[9:].decode("utf-8", "replace") else: 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 raise error_class(errno, err_val) python-asyncmy-0.2.10/asyncmy/optionfile.py000066400000000000000000000006601472327475400210220ustar00rootroot00000000000000from 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.10/asyncmy/pool.pyx000066400000000000000000000143641472327475400200210ustar00rootroot00000000000000import 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 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() 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()") while self._free: conn = self._free.popleft() conn.close() async with self._cond: while self.size > self.freesize: await self._cond.wait() self._closed = True 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._reader.at_eof() or conn._reader.exception(): self._free.pop() conn.close() elif self._recycle > -1 and self._loop.time() - conn.last_usage > self._recycle: self._free.pop() 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) self._cond.notify() finally: self._acquiring -= 1 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) self._cond.notify() finally: self._acquiring -= 1 async def _wakeup(self): async with self._cond: self._cond.notify() 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: conn.close() return fut if self._closing: conn.close() else: self._free.append(conn) fut = self._loop.create_task(self._wakeup()) return fut 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.10/asyncmy/protocol.pyx000066400000000000000000000264761472327475400207200ustar00rootroot00000000000000 from .constants.COLUMN import (NULL_COLUMN, UNSIGNED_CHAR_COLUMN, UNSIGNED_INT24_COLUMN, UNSIGNED_INT64_COLUMN, UNSIGNED_SHORT_COLUMN) from .constants.FIELD_TYPE import VAR_STRING from .constants.SERVER_STATUS import SERVER_MORE_RESULTS_EXISTS from .structs import HB, H, I, Q include "charset.pxd" from . import errors, structs cdef class MysqlPacket: """ Representation of a MySQL response packet. Provides an interface for reading/parsing the packet results. """ cdef: bytes _data int _position def __init__(self, bytes data, str encoding): self._position = 0 self._data = data cpdef bytes get_all_data(self): return self._data cpdef bytes read(self, int size): """ Read the first 'size' bytes in packet and advance cursor past them. :param size: :return: """ cdef bytes result = self._data[self._position: self._position + size] if len(result) != size: error = ( "Result length not requested length:\n" "Expected=%s. Actual=%s. Position: %s. Data Length: %s" % (size, len(result), self._position, len(self._data)) ) raise AssertionError(error) self._position += size return result cpdef bytes read_all(self): """Read all remaining data in the packet. (Subsequent read() will return errors.) """ cdef bytes result = self._data[self._position:] self._position = 0 return result cpdef advance(self, int length): """ Advance the cursor in data buffer 'length' bytes. """ cdef int new_position = self._position + length if new_position < 0 or new_position > len(self._data): raise Exception( "Invalid advance amount (%s) for cursor. " "Position=%s" % (length, new_position) ) self._position = new_position cpdef rewind(self, int position=0): """ Set the position of the data buffer cursor to 'position'. """ if position < 0 or position > len(self._data): raise Exception("Invalid position to rewind cursor to: %s." % position) self._position = position cpdef bytes get_bytes(self, int position, int 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 int read_uint8(self): cdef int result = self._data[self._position] self._position += 1 return result cpdef int read_uint16(self): cdef int result = H.unpack_from(self._data, self._position)[0] self._position += 2 return result cpdef int read_uint24(self): cdef tuple result = HB.unpack_from(self._data, self._position) self._position += 3 return result[0] + (result[1] << 16) cpdef int read_uint32(self): cdef int result = I.unpack_from(self._data, self._position)[0] self._position += 4 return result cpdef unsigned long read_uint64(self): cdef unsigned long result = Q.unpack_from(self._data, self._position)[0] self._position += 8 return result cpdef bytes read_string(self): cdef int end_pos = self._data.find(b"\0", self._position) if end_pos < 0: return None cdef bytes result = self._data[self._position: end_pos] self._position = end_pos + 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 int c = self.read_uint8() if c == NULL_COLUMN: return None if c < UNSIGNED_CHAR_COLUMN: return c elif c == UNSIGNED_SHORT_COLUMN: return self.read_uint16() elif c == UNSIGNED_INT24_COLUMN: return self.read_uint24() elif c == UNSIGNED_INT64_COLUMN: 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".) """ length = self.read_length_encoded_integer() if length is None: return None return self.read(length) cpdef tuple read_struct(self, str fmt): s = getattr(structs, fmt[1:]) result = s.unpack_from(self._data, self._position) self._position += len(result) return tuple(result) cpdef int is_ok_packet(self): # https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html return self._data[0] == 0 and len(self._data) >= 7 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._data[0] == 0xFE and len(self._data) < 9 cpdef int is_auth_switch_request(self): # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest return self._data[0] == 0xFE cpdef int is_extra_auth_data(self): # https://dev.mysql.com/doc/internals/en/successful-authentication.html return self._data[0] == 1 cpdef int is_resultset_packet(self): field_count = self._data[0] return 1 <= field_count <= 250 cpdef int is_load_local_packet(self): return self._data[0] == 0xFB cpdef int is_error_packet(self): return self._data[0] == 0xFF def check_error(self): if self.is_error_packet(): self.raise_for_error() cpdef raise_for_error(self): self.rewind() self.advance(1) # field_count == error (we already know that) errno = self.read_uint16() 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) ( self.charsetnr, self.length, self.type_code, self.flags, self.scale, ) = self.read_struct(" 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.10/asyncmy/replication/constants.py000066400000000000000000000032271472327475400232010ustar00rootroot00000000000000BINLOG_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.10/asyncmy/replication/errors.py000066400000000000000000000003111472327475400224700ustar00rootroot00000000000000class TableMetadataUnavailableError(Exception): """ raise when table metadata is unavailable """ class BinLogNotEnabledError(Exception): """ raise when binlog not enabled """ python-asyncmy-0.2.10/asyncmy/replication/events.py000066400000000000000000000150511472327475400224670ustar00rootroot00000000000000import 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: Union[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: Union[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.10/asyncmy/replication/row_events.py000066400000000000000000000544771472327475400233750ustar00rootroot00000000000000import 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(RowsEvent, self).__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(DeleteRowsEvent, self).__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(WriteRowsEvent, self).__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(UpdateRowsEvent, self).__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(TableMapEvent, self).__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("Ÿï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·nV­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œV9sfé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,XeË–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êêêòØceÓ¦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Òƒ>¨.]º¸÷8N­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×®]4hrrr´cÇ :TcÇŽ­ûP’âãã5oÞ<Ùl6wf´ÿ~mÙ²E÷ß¿ÆçÎÓÒÒ”––¦ÜÜ\õêÕ«ÁŽ‹{çwÔ¬Y3Íš5Kþþþ’¤!C†hâĉJKKÓÏþs÷Úo¿ýVÿýßÿíþ\aaaJMMÕÔ­[7uíÚUÓ§OWmm­þó?ÿÓ½ïjòªªŸ}ö™^{í59N=Úýãïï¯èÿø‡-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]½òòrS¥?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||¼"##KpxU¡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§NRQQ‘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·nvìØ¡òò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ådPP1º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Ö(;;[GUhh¨† ¦²²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éÒÅ_RPP1òЭ[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Òƒ>¨.]º¸÷8N­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×®]4hrrr´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 $9N•””(88ØãqsöìYUTT˜ZûC¸\.ýò—¿TïÞ½õøãÇ’ê^Æpqª¸)œ={V¿ùÍo´zõjã0B7…Å‹ëØ±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§ö6FC÷_€ËÆ 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ñññêÙ³§²³³õÛßþVG5.L¡PE“ãt:õ—¿üE;vÔK/½¤Ñ£Gë‘GÑÌ™3%I©©©†€9ªhrNŸ>­3gÎèÎ;ï”Õju犈ˆà„*~0 U49þþþ?~¼bcc=r§Ó©¢¢"90‹—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µZK€„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Ûãêêª|˜‰'’––¦”µ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††9Zµ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ž<‰££#ׯ_ç‡~ 22NGïÞ½éÚµ+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æPYYIXXX7µ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œ?wwwFE¿~ý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ëÖ±}ûvFEûöí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§Nb×®]œµûÛÐý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~~>ß~û-£Fbâĉ Šû#‹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§ÓÕú¶ZF£œ‹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Ü¡Zooo4r£_í5Ôí¯Ú6ŠMEÍyY59§6Grî«Ùæ†|wÔÄ<×ÕnŸÚýuû!„h^¤Cõ€½½=111tìØ‘òòr“ƘN÷ûË1Œ/Á8rä}ôëׯgÞ¼y¸»»“’’¢ÄVUßc‰nnnœ8q¢Ú…¥´´”ï¾ûŽnݺÕùC~‰[Ó]DNGEE666¤§§›,Í—»»;åååÊ|¿F999”••)oŸ={6ÎÎ΄……)1-[¶D«ÕråÊ•Ååää`mm]íq+++Z¶lÙàú¼½½ILL¤´´;;;“ØÒÒR¥Fm\S!šBFF‘‘‘<úè£Õ^”pýúuÀ0¢ãVINNæ“O>¡ÿþüýﯖSæ:„««kSÔ´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"99NGǎ׾}{ 9|ø°I\FFyyyJPm}ýû÷ SiTuæÌrrr”QcéÒ¥<òÈ#XYYGJJ þóŸkœ7¯.öööœ8q‚ýû÷Ó®]»Zl‹›§×ë™4iFyyM÷îÝéÒ¥ ?þø#ÖÖÖôéÓ‡ää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:To1FÃÿýßÿ±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'¦NjòéÙ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íþ !îÒ¡z9xð ±±±¤¤¤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¾Pw5RS_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àêêjr0: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]WBT.2¡Zt.î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$00k×®±lÙ2ÜÜÜ´û2ªÆY$&&¢×뵺c«¼ç+K¯^½Ø±caaa$&&Ò¶m[RRRX¹r%nnnÚ NõwB\\sæÌaРAxxxhÛ½½½µÑØû}QNGçÎÙ¾};?þø#;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§N8p Õ±üü|vìØÁñãǹté>>>ôíÛ—¶mÛZÅ={–7réÒ%ªT©B@@ƒ¦J•*Vq—/_fýúõ\¼xGGGš6mJHH^^^Vq…•V¾E‹‘••e•Э[7‚ƒƒµŸï&_!ìÉ`0°nÝ:Ž?NFF>>>ôìÙ“Ž;–ØÿY¨öÙåÉC¥oWlÛ¶øø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(2XXXXÎÎÎ|öÙ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||<§NB¯×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-­GdggkÌ«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·¢°åããÃøñã #""‚ˆˆÀ|m3///Àü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õêÙ&[1lÙ²…víÚÝ—ÉT0ßËêúõëôìÙÓöÆÉÉIiE®““S‰Û!ÄÃÃÅÅ___Ûd%®®®e^×`ÞîªòÔKN§tíݯúQ–ŠÊWˆÊà~N¤Z¸¹¹•¹²ÆÉɉºuëÚ&ßwŽŽŽ’¯wÃÝݽÈÓ«U©öÙªy¨öí÷2)ŽjùìÝ·«æ+„=<Œ×›j£JµíPSeï¶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צG8;;³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µ(Sm »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£FhÞ¼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¯\¹ü9jãéû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@ æ›oF-æ^ãææÆþýû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µOTS}ö¨ž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=1.0)"] sa = ["sqlalchemy (>=1.3,<1.4)"] [[package]] name = "black" version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] pycparser = "*" [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "cryptography" version = "42.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "cython" version = "3.0.10" description = "The Cython compiler for writing C extensions in the Python language." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ {file = "Cython-3.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e876272548d73583e90babda94c1299537006cad7a34e515a06c51b41f8657aa"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc377aa33c3309191e617bf675fdbb51ca727acb9dc1aa23fc698d8121f7e23"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:401aba1869a57aba2922ccb656a6320447e55ace42709b504c2f8e8b166f46e1"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:541fbe725d6534a90b93f8c577eb70924d664b227a4631b90a6e0506d1469591"}, {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:86998b01f6a6d48398df8467292c7637e57f7e3a2ca68655367f13f66fed7734"}, {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d092c0ddba7e9e530a5c5be4ac06db8360258acc27675d1fc86294a5dc8994c5"}, {file = "Cython-3.0.10-cp310-cp310-win32.whl", hash = "sha256:3cffb666e649dba23810732497442fb339ee67ba4e0be1f0579991e83fcc2436"}, {file = "Cython-3.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:9ea31184c7b3a728ef1f81fccb161d8948c05aa86c79f63b74fb6f3ddec860ec"}, {file = "Cython-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:051069638abfb076900b0c2bcb6facf545655b3f429e80dd14365192074af5a4"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712760879600907189c7d0d346851525545484e13cd8b787e94bfd293da8ccf0"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38d40fa1324ac47c04483d151f5e092406a147eac88a18aec789cf01c089c3f2"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bd49a3a9fdff65446a3e1c2bfc0ec85c6ce4c3cad27cd4ad7ba150a62b7fb59"}, {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8df79b596633b8295eaa48b1157d796775c2bb078f32267d32f3001b687f2fd"}, {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bcc9795990e525c192bc5c0775e441d7d56d7a7d02210451e9e13c0448dba51b"}, {file = "Cython-3.0.10-cp311-cp311-win32.whl", hash = "sha256:09f2000041db482cad3bfce94e1fa3a4c82b0e57390a164c02566cbbda8c4f12"}, {file = "Cython-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:3919a55ec9b6c7db6f68a004c21c05ed540c40dbe459ced5d801d5a1f326a053"}, {file = "Cython-3.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f2864ab5fcd27a346f0b50f901ebeb8f60b25a60a575ccfd982e7f3e9674914"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:407840c56385b9c085826fe300213e0e76ba15d1d47daf4b58569078ecb94446"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a036d00caa73550a3a976432ef21c1e3fa12637e1616aab32caded35331ae96"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc6a0e7e23a96dec3f3c9d39690d4281beabd5297855140d0d30855f950275e"}, {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5e14a8c6a8157d2b0cdc2e8e3444905d20a0e78e19d2a097e89fb8b04b51f6b"}, {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f8a2b8fa0fd8358bccb5f3304be563c4750aae175100463d212d5ea0ec74cbe0"}, {file = "Cython-3.0.10-cp312-cp312-win32.whl", hash = "sha256:2d29e617fd23cf4b83afe8f93f2966566c9f565918ad1e86a4502fe825cc0a79"}, {file = "Cython-3.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:6c5af936940a38c300977b81598d9c0901158f220a58c177820e17e1774f1cf1"}, {file = "Cython-3.0.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f465443917d5c0f69825fca3b52b64c74ac3de0143b1fff6db8ba5b48c9fb4a"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fadb84193c25641973666e583df8df4e27c52cdc05ddce7c6f6510d690ba34a"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fa9e7786083b6aa61594c16979d621b62e61fcd9c2edd4761641b95c7fb34b2"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4780d0f98ce28191c4d841c4358b5d5e79d96520650910cd59904123821c52d"}, {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:32fbad02d1189be75eb96456d9c73f5548078e5338d8fa153ecb0115b6ee279f"}, {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:90e2f514fc753b55245351305a399463103ec18666150bb1c36779b9862388e9"}, {file = "Cython-3.0.10-cp36-cp36m-win32.whl", hash = "sha256:a9c976e9ec429539a4367cb4b24d15a1e46b925976f4341143f49f5f161171f5"}, {file = "Cython-3.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:a9bb402674788a7f4061aeef8057632ec440123e74ed0fb425308a59afdfa10e"}, {file = "Cython-3.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:206e803598010ecc3813db8748ed685f7beeca6c413f982df9f8a505fce56563"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15b6d397f4ee5ad54e373589522af37935a32863f1b23fa8c6922adf833e28e2"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a181144c2f893ed8e6a994d43d0b96300bc99873f21e3b7334ca26c61c37b680"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74b700d6a793113d03fb54b63bdbadba6365379424bac7c0470605672769260"}, {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:076e9fd4e0ca33c5fa00a7479180dbfb62f17fe928e2909f82da814536e96d2b"}, {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:269f06e6961e8591d56e30b46e1a51b6ccb42cab04c29fa3b30d3e8723485fb4"}, {file = "Cython-3.0.10-cp37-cp37m-win32.whl", hash = "sha256:d4e83a8ceff7af60064da4ccfce0ac82372544dd5392f1b350c34f1b04d0fae6"}, {file = "Cython-3.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:40fac59c3a7fbcd9c25aea64c342c890a5e2270ce64a1525e840807800167799"}, {file = "Cython-3.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f43a58bf2434870d2fc42ac2e9ff8138c9e00c6251468de279d93fa279e9ba3b"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e9a885ec63d3955a08cefc4eec39fefa9fe14989c6e5e2382bd4aeb6bdb9bc3"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acfbe0fff364d54906058fc61f2393f38cd7fa07d344d80923937b87e339adcf"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8adcde00a8a88fab27509b558cd8c2959ab0c70c65d3814cfea8c68b83fa6dcd"}, {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c9c1e3e78909488f3b16fabae02308423fa6369ed96ab1e250807d344cfffd7"}, {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc6e0faf5b57523b073f0cdefadcaef3a51235d519a0594865925cadb3aeadf0"}, {file = "Cython-3.0.10-cp38-cp38-win32.whl", hash = "sha256:35f6ede7c74024ed1982832ae61c9fad7cf60cc3f5b8c6a63bb34e38bc291936"}, {file = "Cython-3.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:950c0c7b770d2a7cec74fb6f5ccc321d0b51d151f48c075c0d0db635a60ba1b5"}, {file = "Cython-3.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:077b61ee789e48700e25d4a16daa4258b8e65167136e457174df400cf9b4feab"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f1f8bba9d8f37c0cffc934792b4ac7c42d0891077127c11deebe9fa0a0f7e4"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d10fc9aa82e5e53a0b7fd118f9771199cddac8feb4a6d8350b7d4109085aa775"}, {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f610964ab252a83e573a427e28b103e2f1dd3c23bee54f32319f9e73c3c5499"}, {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c9c4c4f3ab8f8c02817b0e16e8fa7b8cc880f76e9b63fe9c010e60c1a6c2b13"}, {file = "Cython-3.0.10-cp39-cp39-win32.whl", hash = "sha256:0bac3ccdd4e03924028220c62ae3529e17efa8ca7e9df9330de95de02f582b26"}, {file = "Cython-3.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:81f356c1c8c0885b8435bfc468025f545c5d764aa9c75ab662616dd1193c331e"}, {file = "Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2"}, {file = "Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99"}, ] [[package]] name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "execnet" version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, ] [package.extras] testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "isort" version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] colors = ["colorama (>=0.4.6)"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "mypy" version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "mysqlclient" version = "2.2.4" description = "Python interface to MySQL" optional = false python-versions = ">=3.8" files = [ {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, {file = "mysqlclient-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab"}, {file = "mysqlclient-2.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a"}, {file = "mysqlclient-2.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2"}, {file = "mysqlclient-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54"}, {file = "mysqlclient-2.2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a"}, {file = "mysqlclient-2.2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3"}, {file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"}, ] [[package]] name = "packaging" version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycparser" version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymysql" version = "1.1.1" description = "Pure Python MySQL Driver" optional = false python-versions = ">=3.7" files = [ {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, ] [package.extras] ed25519 = ["PyNaCl (>=1.4.0)"] rsa = ["cryptography"] [[package]] name = "pytest" version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.21.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" files = [ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-xdist" version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, ] [package.dependencies] execnet = ">=2.1" pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] setproctitle = ["setproctitle"] testing = ["filelock"] [[package]] name = "rich" version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" version = "0.4.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.4.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5"}, {file = "ruff-0.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50"}, {file = "ruff-0.4.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef"}, {file = "ruff-0.4.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc"}, {file = "ruff-0.4.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0"}, {file = "ruff-0.4.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259"}, {file = "ruff-0.4.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a"}, {file = "ruff-0.4.6-py3-none-win32.whl", hash = "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062"}, {file = "ruff-0.4.6-py3-none-win_amd64.whl", hash = "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a"}, {file = "ruff-0.4.6-py3-none-win_arm64.whl", hash = "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6"}, {file = "ruff-0.4.6.tar.gz", hash = "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7"}, ] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] name = "uvloop" version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" files = [ {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, ] [package.extras] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "728b125de0941c58cc65485255b601d8ac5d8dd69bbc6d76dd878163555f4691" python-asyncmy-0.2.10/pyproject.toml000066400000000000000000000026721472327475400175360ustar00rootroot00000000000000[tool.poetry] authors = ["long2ice "] description = "A fast asyncio MySQL driver" documentation = "https://github.com/long2ice/asyncmy" homepage = "https://github.com/long2ice/asyncmy" include = ["CHANGELOG.md", "LICENSE", "README.md"] keywords = ["driver", "asyncio", "mysql"] license = "Apache-2.0" name = "asyncmy" packages = [ { include = "asyncmy" }, ] readme = "README.md" repository = "https://github.com/long2ice/asyncmy.git" version = "0.2.10" [tool.poetry.dependencies] python = "^3.8" [tool.poetry.group.dev.dependencies] cython = "*" isort = "*" mypy = "*" rich = "*" ruff = "*" # Remove the follows if migrate black to ruff black = "*" click = "*" pathspec = "*" platformdirs = "*" [tool.poetry.group.test.dependencies] 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" pytest-mock = "*" pytest-xdist = "*" cryptography = "*" uvloop = { version = "*", markers = "sys_platform != 'win32'" } [tool.poetry.build] generate-setup-file = true script = "build.py" [build-system] build-backend = "poetry.core.masonry.api" requires = ["poetry-core", "setuptools", "cython"] [tool.black] line-length = 100 target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] [tool.pytest.ini_options] asyncio_mode = 'auto' [tool.mypy] pretty = true ignore_missing_imports = true [tool.ruff] line-length = 100 python-asyncmy-0.2.10/tests/000077500000000000000000000000001472327475400157555ustar00rootroot00000000000000python-asyncmy-0.2.10/tests/__init__.py000066400000000000000000000000001472327475400200540ustar00rootroot00000000000000python-asyncmy-0.2.10/tests/test_autocommit.py000066400000000000000000000006541472327475400215540ustar00rootroot00000000000000import 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.10/tests/test_connection.py000066400000000000000000000024321472327475400215260ustar00rootroot00000000000000import re import pytest from asyncmy.connection import Connection 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 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() python-asyncmy-0.2.10/tests/test_converters.py000066400000000000000000000013541472327475400215630ustar00rootroot00000000000000import datetime from asyncmy.converters import 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'" python-asyncmy-0.2.10/tests/test_cursor.py000066400000000000000000000100671472327475400207070ustar00rootroot00000000000000import datetime from decimal import Decimal from enum import Enum import pytest from asyncmy.cursors import DictCursor @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 python-asyncmy-0.2.10/tests/test_pool.py000066400000000000000000000014041472327475400203360ustar00rootroot00000000000000import pytest 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