diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..efa9a2ff8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,18 @@ +[run] +branch = True +source = + pymysql + tests +omit = pymysql/tests/* + pymysql/tests/thirdparty/test_MySQLdb/* + +[report] +exclude_lines = + pragma: no cover + except ImportError: + if DEBUG: + def __repr__ + def __str__ + raise NotImplementedError + def __getattr__ + raise ValueError diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..253a13aca --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: ["methane"] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: "pypi/PyMySQL" # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..f2bd4d300 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Complete steps to reproduce the behavior: + +Schema: + +``` +CREATE DATABASE ... +CREATE TABLE ... +``` + +Code: + +```py +import pymysql +con = pymysql.connect(...) +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment** + - OS: [e.g. Windows, Linux] + - Server and version: [e.g. MySQL 8.0.19, MariaDB] + - PyMySQL version: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/django.yaml b/.github/workflows/django.yaml new file mode 100644 index 000000000..5c4609543 --- /dev/null +++ b/.github/workflows/django.yaml @@ -0,0 +1,66 @@ +name: Django test + +on: + push: + # branches: ["main"] + # pull_request: + +jobs: + django-test: + name: "Run Django LTS test suite" + runs-on: ubuntu-latest + # There are some known difference between MySQLdb and PyMySQL. + continue-on-error: true + env: + PIP_NO_PYTHON_VERSION_WARNING: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + # DJANGO_VERSION: "3.2.19" + strategy: + fail-fast: false + matrix: + include: + # Django 3.2.9+ supports Python 3.10 + # https://docs.djangoproject.com/ja/3.2/releases/3.2/ + - django: "3.2.19" + python: "3.10" + + - django: "4.2.1" + python: "3.11" + + steps: + - name: Start MySQL + run: | + sudo systemctl start mysql.service + mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot -proot mysql + mysql -uroot -proot -e "set global innodb_flush_log_at_trx_commit=0;" + mysql -uroot -proot -e "CREATE USER 'scott'@'%' IDENTIFIED BY 'tiger'; GRANT ALL ON *.* TO scott;" + mysql -uroot -proot -e "CREATE DATABASE django_default; CREATE DATABASE django_other;" + + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install mysqlclient + run: | + #pip install mysqlclient # Use stable version + pip install .[rsa] + + - name: Setup Django + run: | + sudo apt-get install libmemcached-dev + wget https://github.com/django/django/archive/${{ matrix.django }}.tar.gz + tar xf ${{ matrix.django }}.tar.gz + cp ci/test_mysql.py django-${{ matrix.django }}/tests/ + cd django-${{ matrix.django }} + pip install . -r tests/requirements/py3.txt + + - name: Run Django test + run: | + cd django-${{ matrix.django }}/tests/ + # test_runner does not using our test_mysql.py + # We can't run whole django test suite for now. + # Run olly backends test + DJANGO_SETTINGS_MODULE=test_mysql python runtests.py backends diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..07ea66031 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,24 @@ +name: Lint + +on: + push: + branches: ["main"] + paths: + - '**.py' + pull_request: + paths: + - '**.py' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/ruff-action@v3 + + - name: format + run: ruff format --diff + + - name: lint + run: ruff check --diff diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000..6abc96b70 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,110 @@ +name: Test + +on: + push: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - db: "mariadb:10.4" + py: "3.13" + + - db: "mariadb:10.5" + py: "3.11" + + - db: "mariadb:10.6" + py: "3.10" + + - db: "mariadb:10.6" + py: "3.9" + + - db: "mariadb:lts" + py: "3.8" + + - db: "mysql:5.7" + py: "pypy-3.10" + + - db: "mysql:8.0" + py: "3.13" + mysql_auth: true + + - db: "mysql:8.4" + py: "3.8" + mysql_auth: true + + services: + mysql: + image: "${{ matrix.db }}" + ports: + - 3306:3306 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes + options: "--name=mysqld" + volumes: + - /run/mysqld:/run/mysqld + + steps: + - uses: actions/checkout@v4 + + - name: Workaround MySQL container permissions + if: startsWith(matrix.db, 'mysql') + run: | + sudo chown 999:999 /run/mysqld + /usr/bin/docker ps --all --filter status=exited --no-trunc --format "{{.ID}}" | xargs -r /usr/bin/docker start + + - name: Set up Python ${{ matrix.py }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + allow-prereleases: true + cache: 'pip' + cache-dependency-path: 'requirements-dev.txt' + + - name: Install dependency + run: | + pip install --upgrade -r requirements-dev.txt + + - name: Set up MySQL + run: | + while : + do + sleep 1 + mysql -h127.0.0.1 -uroot -e 'select version()' && break + done + mysql -h127.0.0.1 -uroot -e "SET GLOBAL local_infile=on" + mysql -h127.0.0.1 -uroot --comments < ci/docker-entrypoint-initdb.d/init.sql + mysql -h127.0.0.1 -uroot --comments < ci/docker-entrypoint-initdb.d/mysql.sql + mysql -h127.0.0.1 -uroot --comments < ci/docker-entrypoint-initdb.d/mariadb.sql + cp ci/docker.json pymysql/tests/databases.json + + - name: Run test + run: | + pytest -v --cov --cov-config .coveragerc pymysql + pytest -v --cov-append --cov-config .coveragerc --doctest-modules pymysql/converters.py + + - name: Run MySQL8 auth test + if: ${{ matrix.mysql_auth }} + run: | + docker cp mysqld:/var/lib/mysql/public_key.pem "${HOME}" + docker cp mysqld:/var/lib/mysql/ca.pem "${HOME}" + docker cp mysqld:/var/lib/mysql/server-cert.pem "${HOME}" + docker cp mysqld:/var/lib/mysql/client-key.pem "${HOME}" + docker cp mysqld:/var/lib/mysql/client-cert.pem "${HOME}" + pytest -v --cov-append --cov-config .coveragerc tests/test_auth.py; + + - name: Upload coverage reports to Codecov + if: github.repository == 'PyMySQL/PyMySQL' + uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index 45dca2995..09a5654fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,16 @@ *.pyc +*.pyo +/.cache +/.coverage +/.idea +/.tox +/.venv +/.vscode +/PyMySQL.egg-info +/build +/dist +/docs/build +/pymysql/tests/databases.json __pycache__ -py3k -dist -PyMySQL.egg-info +Pipfile.lock +pdm.lock diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..59fdb65df --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +python: + install: + - requirements: docs/requirements.txt + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 5c423c3a8..000000000 --- a/CHANGELOG +++ /dev/null @@ -1,37 +0,0 @@ -Changes --------- -0.4 -Miscellaneous bug fixes - -Implementation of SSL support - -Implementation of kill() - -Cleaned up charset functionality - -Fixed BIT type handling - -Connections raise exceptions after they are close()'d - -Full Py3k and unicode support - -0.3 -Implemented most of the extended DBAPI 2.0 spec including callproc() - -Fixed error handling to include the message from the server and support - multiple protocol versions. - -Implemented ping() - -Implemented unicode support (probably needs better testing) - -Removed DeprecationWarnings - -Ran against the MySQLdb unit tests to check for bugs - -Added support for client_flag, charset, sql_mode, read_default_file, - use_unicode, cursorclass, init_command, and connect_timeout. - -Refactoring for some more compatibility with MySQLdb including a fake - pymysql.version_info attribute. - -Now runs with no warnings with the -3 command-line switch - -Added test cases for all outstanding tickets and closed most of them. - -Basic Jython support added. - -Fixed empty result sets bug. - -Integrated new unit tests and refactored the example into one. - -Fixed bug with decimal conversion. - -Fixed string encoding bug. Now unicode and binary data work! - -Added very basic docstrings. - -0.2 -Changed connection parameter name 'password' to 'passwd' - to make it more plugin replaceable for the other mysql clients. - -Changed pack()/unpack() calls so it runs on 64 bit OSes too. - -Added support for unix_socket. - -Added support for no password. - -Renamed decorders to decoders. - -Better handling of non-existing decoder. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a633f6c51 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,428 @@ +# Changes + +## Backward incompatible changes planned in the future. + +* Error classes in Cursor class will be removed after 2024-06 +* `Connection.set_charset(charset)` will be removed after 2024-06 +* `db` and `passwd` will emit DeprecationWarning in v1.2. See #933. +* `Connection.ping(reconnect)` change the default to not reconnect. + +## v1.1.1 + +Release date: 2024-05-21 + +> [!WARNING] +> This release fixes a vulnerability (CVE-2024-36039). +> All users are recommended to update to this version. +> +> If you can not update soon, check the input value from +> untrusted source has an expected type. Only dict input +> from untrusted source can be an attack vector. + +* Prohibit dict parameter for `Cursor.execute()`. It didn't produce valid SQL + and might cause SQL injection. (CVE-2024-36039) +* Added ssl_key_password param. #1145 + +## v1.1.0 + +Release date: 2023-06-26 + +* Fixed SSCursor raising OperationalError for query timeouts on wrong statement (#1032) +* Exposed `Cursor.warning_count` to check for warnings without additional query (#1056) +* Make Cursor iterator (#995) +* Support '_' in key name in my.cnf (#1114) +* `Cursor.fetchall()` returns empty list instead of tuple (#1115). Note that `Cursor.fetchmany()` still return empty tuple after reading all rows for compatibility with Django. +* Deprecate Error classes in Cursor class (#1117) +* Add `Connection.set_character_set(charset, collation=None)`. This method is compatible with mysqlclient. (#1119) +* Deprecate `Connection.set_charset(charset)` (#1119) +* New connection always send "SET NAMES charset [COLLATE collation]" query. (#1119) + Since collation table is vary on MySQL server versions, collation in handshake is fragile. +* Support `charset="utf8mb3"` option (#1127) + + +## v1.0.3 + +Release date: 2023-03-28 + +* Dropped support of end of life MySQL version 5.6 +* Dropped support of end of life MariaDB versions below 10.3 +* Dropped support of end of life Python version 3.6 +* Removed `_last_executed` because of duplication with `_executed` by @rajat315315 in https://github.com/PyMySQL/PyMySQL/pull/948 +* Fix generating authentication response with long strings by @netch80 in https://github.com/PyMySQL/PyMySQL/pull/988 +* update pymysql.constants.CR by @Nothing4You in https://github.com/PyMySQL/PyMySQL/pull/1029 +* Document that the ssl connection parameter can be an SSLContext by @cakemanny in https://github.com/PyMySQL/PyMySQL/pull/1045 +* Raise ProgrammingError on -np.inf in addition to np.inf by @cdcadman in https://github.com/PyMySQL/PyMySQL/pull/1067 +* Use Python 3.11 release instead of -dev in tests by @Nothing4You in https://github.com/PyMySQL/PyMySQL/pull/1076 + + +## v1.0.2 + +Release date: 2021-01-09 + +* Fix `user`, `password`, `host`, `database` are still positional arguments. + All arguments of `connect()` are now keyword-only. (#941) + + +## v1.0.1 + +Release date: 2021-01-08 + +* Stop emitting DeprecationWarning for use of ``db`` and ``passwd``. + Note that they are still deprecated. (#939) +* Add ``python_requires=">=3.6"`` to setup.py. (#936) + + +## v1.0.0 + +Release date: 2021-01-07 + +Backward incompatible changes: + +* Python 2.7 and 3.5 are not supported. +* ``connect()`` uses keyword-only arguments. User must use keyword argument. +* ``connect()`` kwargs ``db`` and ``passwd`` are now deprecated; Use ``database`` and ``password`` instead. +* old_password authentication method (used by MySQL older than 4.1) is not supported. +* MySQL 5.5 and MariaDB 5.5 are not officially supported, although it may still works. +* Removed ``escape_dict``, ``escape_sequence``, and ``escape_string`` from ``pymysql`` + module. They are still in ``pymysql.converters``. + +Other changes: + +* Connection supports context manager API. ``__exit__`` closes the connection. (#886) +* Add MySQL Connector/Python compatible TLS options (#903) +* Major code cleanup; PyMySQL now uses black and flake8. + + +## v0.10.1 + +Release date: 2020-09-10 + +* Fix missing import of ProgrammingError. (#878) +* Fix auth switch request handling. (#890) + + +## v0.10.0 + +Release date: 2020-07-18 + +This version is the last version supporting Python 2.7. + +* MariaDB ed25519 auth is supported. +* Python 3.4 support is dropped. +* Context manager interface is removed from `Connection`. It will be added + with different meaning. +* MySQL warnings are not shown by default because many user report issue to + PyMySQL issue tracker when they see warning. You need to call "SHOW WARNINGS" + explicitly when you want to see warnings. +* Formatting of float object is changed from "3.14" to "3.14e0". +* Use cp1252 codec for latin1 charset. +* Fix decimal literal. +* TRUNCATED_WRONG_VALUE_FOR_FIELD, and ILLEGAL_VALUE_FOR_TYPE are now + DataError instead of InternalError. + + +## 0.9.3 + +Release date: 2018-12-18 + +* cryptography dependency is optional now. +* Fix old_password (used before MySQL 4.1) support. +* Deprecate old_password. +* Stop sending ``sys.argv[0]`` for connection attribute "program_name". +* Close connection when unknown error is happened. +* Deprecate context manager API of Connection object. + +## 0.9.2 + +Release date: 2018-07-04 + +* Disabled unintentinally enabled debug log +* Removed unintentionally installed tests + + +## 0.9.1 + +Release date: 2018-07-03 + +* Fixed caching_sha2_password and sha256_password raise TypeError on PY2 + (#700, #702) + + +## 0.9.0 + +Release date: 2018-06-27 + +* Change default charset from latin1 to utf8mb4. (because MySQL 8 changed) (#692) +* Support sha256_password and caching_sha2_password auth method (#682) +* Add cryptography dependency, because it's needed for new auth methods. +* Remove deprecated `no_delay` option (#694) +* Support connection attributes (#679) +* Map LOCK_DEADLOCK to OperationalError (#693) + +## 0.8.1 + +Release date: 2018-05-07 + +* Reduce `cursor.callproc()` roundtrip time. (#636) + +* Fixed `cursor.query()` is hunged after multi statement failed. (#647) + +* WRONG_DB_NAME and WRONG_COLUMN_NAME is ProgrammingError for now. (#629) + +* Many test suite improvements, especially adding MySQL 8.0 and using Docker. + Thanks to Daniel Black. + +* Dropped support for old Python and MySQL which is not tested long time. + + +## 0.8 + +Release date: 2017-12-20 + +* **BACKWARD INCOMPATIBLE** ``binary_prefix`` option is added and off + by default because of compatibility with mysqlclient. + When you need PyMySQL 0.7 behavior, you have to pass ``binary_prefix=True``. + (#549) + +* **BACKWARD INCOMPATIBLE** ``MULTI_STATEMENTS`` client flag is no longer + set by default, while it was on PyMySQL 0.7. You need to pass + ``client_flag=CLIENT.MULTI_STATEMENTS`` when you connect to explicitly + enable multi-statement mode. (#590) + +* Fixed AuthSwitch packet handling. + +* Raise OperationalError for MariaDB's constraint error. (#607) + +* executemany() accepts query without space between ``VALUES`` and ``(``. (#597) + +* Support config file containing option without value. (#588) + +* Fixed Connection.ping() returned unintended value. + + +## 0.7.11 + +Release date: 2017-04-06 + +* Fixed Connection.close() failed when failed to send COM_CLOSE packet. +* Cursor.executemany() accepts query ends with semicolon. +* ssl parameters can be read from my.cnf. + + +## 0.7.10 + +Release date: 2017-02-14 + +* **SECURITY FIX**: Raise RuntimeError when received LOAD_LOCAL packet while + ``loacal_infile=False``. (Thanks to Bryan Helmig) + +* Raise SERVER_LOST error for MariaDB's shutdown packet (#540) + +* Change default connect_timeout to 10. + +* Add bind_address option (#529) + + +## 0.7.9 + +Release date: 2016-09-03 + +* Fix PyMySQL stop reading rows when first column is empty string (#513) + Reverts DEPRECATE_EOF introduced in 0.7.7. + +## 0.7.8 + +Release date: 2016-09-01 + +* Revert error message change in 0.7.7. + (SQLAlchemy parses error message, #507) + +## 0.7.7 + +Release date: 2016-08-30 + +* Add new unicode collation (#498) +* Fix conv option is not used for encoding objects. +* Experimental support for DEPRECATE_EOF protocol. + +## 0.7.6 + +Release date: 2016-07-29 + +* Fix SELECT JSON type cause UnicodeError +* Avoid float conversion while parsing microseconds +* Warning has number +* SSCursor supports warnings + +## 0.7.5 + +Release date: 2016-06-28 + +* Fix exception raised while importing when getpwuid() fails (#472) +* SSCursor supports LOAD DATA LOCAL INFILE (#473) +* Fix encoding error happen for JSON type (#477) +* Fix test fail on Python 2.7 and MySQL 5.7 (#478) + +## 0.7.4 + +Release date: 2016-05-26 + +* Fix AttributeError may happen while Connection.__del__ (#463) +* Fix SyntaxError in test_cursor. (#464) +* frozenset support for query value. (#461) +* Start using readthedocs.io + +## 0.7.3 + +Release date: 2016-05-19 + +* Add read_timeout and write_timeout option. +* Support serialization customization by `conv` option. +* Unknown type is converted by `str()`, for MySQLdb compatibility. +* Support '%%' in `Cursor.executemany()` +* Support REPLACE statement in `Cursor.executemany()` +* Fix handling incomplete row caused by 'SHOW SLAVE HOSTS'. +* Fix decode error when use_unicode=False on PY3 +* Fix port option in my.cnf file is ignored. + + +## 0.7.2 + +Release date: 2016-02-24 + +* Fix misuse of `max_allowed_packet` parameter. (#426, #407 and #397) +* Add %(name)s plceholder support to `Cursor.executemany()`. (#427, thanks to + @WorldException) + +## 0.7.1 + +Release date: 2016-01-14 + +* Fix auth fail with MySQL 5.1 +* Fix escaping unicode fails on Python 2 + +## 0.7 + +Release date: 2016-01-10 + +* Faster binary escaping +* Add `"_binary" prefix` to string literal for binary types. + binary types are: `bytearray` on Python 2, `bytes` and `bytearray` on Python 3. + This is because recent MySQL show warnings when string literal is invalid for + connection encoding. +* `pymysql.Binary()` returns `bytearray` on Python 2. This is required to distinguish + binary and string. +* Auth plugin support. +* no_delay option is ignored. It will be removed in PyMySQL 0.8. + + +## 0.6.7 + +Release date: 2015-09-30 + +* Allow self signed certificate +* Add max_allowed_packet option +* Fix error when bytes in executemany +* Support geometry type +* Add coveralls badge to README +* Fix some bugs relating to warnings +* Add Cursor.mogrify() method +* no_delay option is deprecated and True by default +* Fix options from my.cnf overrides options from arguments +* Allow socket like object. (It's not feature for end users) +* Strip quotes while reading options from my.cnf file +* Fix encoding issue in executemany() + +## 0.6.6 + +* Add context manager to cursor +* Fix can't encode blob that is not utf-8 on PY3. (regression of 0.6.4, + Thanks to @wiggzz) + +## 0.6.5 +Skipped + +## 0.6.4 +* Support "LOAD LOCAL INFILE". Thanks @wraziens +* Show MySQL warnings after execute query. +* Fix MySQLError may be wrapped with OperationalError while connectiong. (#274) +* SSCursor no longer attempts to expire un-collected rows within __del__, + delaying termination of an interrupted program; cleanup of uncollected + rows is left to the Connection on next execute, which emits a + warning at that time. (#287) +* Support datetime and time with microsecond. (#303) +* Use surrogateescape to format bytes on Python 3. +* OperationalError raised from connect() have information about original + exception. (#304) +* `init_command` now support multi statement. +* `Connection.escape()` method now accepts second argument compatible to + MySQL-Python. + +## 0.6.3 +* Fixed multiple result sets with SSCursor. +* Fixed connection timeout. +* Fixed literal set syntax to work on Py2.6. +* Allow for mysql negative values with 0 hour timedelta. +* Added Connection.begin(). + +## 0.6.2 +* Fixed old password on Python 3. +* Added support for bulk insert in Cursor.executemany(). +* Added support for microseconds in datetimes and dates before 1900. +* Several other bug fixes. + +## 0.6.1 +* Added cursor._last_executed for MySQLdb compatibility +* Cursor.fetchall() and .fetchmany now return list, not tuple +* Allow "length of auth-plugin-data" = 0 +* Cursor.connection references connection object without weakref + +## 0.6 +* Improved Py3k support +* Improved PyPy support +* Added IPv6 support +* Added Thing2Literal for Django/MySQLdb compatibility +* Removed errorhandler +* Fixed GC errors +* Improved test suite +* Many bug fixes +* Many performance improvements + +## 0.4 +* Miscellaneous bug fixes +* Implementation of SSL support +* Implementation of kill() +* Cleaned up charset functionality +* Fixed BIT type handling +* Connections raise exceptions after they are close()'d +* Full Py3k and unicode support + +## 0.3 +* Implemented most of the extended DBAPI 2.0 spec including callproc() +* Fixed error handling to include the message from the server and support + multiple protocol versions. +* Implemented ping() +* Implemented unicode support (probably needs better testing) +* Removed DeprecationWarnings +* Ran against the MySQLdb unit tests to check for bugs +* Added support for client_flag, charset, sql_mode, read_default_file, + use_unicode, cursorclass, init_command, and connect_timeout. +* Refactoring for some more compatibility with MySQLdb including a fake + pymysql.version_info attribute. +* Now runs with no warnings with the -3 command-line switch +* Added test cases for all outstanding tickets and closed most of them. +* Basic Jython support added. +* Fixed empty result sets bug. +* Integrated new unit tests and refactored the example into one. +* Fixed bug with decimal conversion. +* Fixed string encoding bug. Now unicode and binary data work! +* Added very basic docstrings. + +## 0.2 +* Changed connection parameter name 'password' to 'passwd' + to make it more plugin replaceable for the other mysql clients. +* Changed pack()/unpack() calls so it runs on 64 bit OSes too. +* Added support for unix_socket. +* Added support for no password. +* Renamed decorders to decoders. +* Better handling of non-existing decoder. diff --git a/LICENSE b/LICENSE index 102e72e70..86b18e10b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 PyMySQL contributors +Copyright (c) 2010, 2013 PyMySQL contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..e2e577a9d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md LICENSE CHANGELOG.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..7d8bfc860 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +[![Documentation Status](https://readthedocs.org/projects/pymysql/badge/?version=latest)](https://pymysql.readthedocs.io/) +[![codecov](https://codecov.io/gh/PyMySQL/PyMySQL/branch/main/graph/badge.svg?token=ppEuaNXBW4)](https://codecov.io/gh/PyMySQL/PyMySQL) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/PyMySQL/PyMySQL) + +# PyMySQL + +This package contains a pure-Python MySQL and MariaDB client library, based on +[PEP 249](https://www.python.org/dev/peps/pep-0249/). + +## Requirements + +- Python -- one of the following: + - [CPython](https://www.python.org/) : 3.7 and newer + - [PyPy](https://pypy.org/) : Latest 3.x version +- MySQL Server -- one of the following: + - [MySQL](https://www.mysql.com/) \>= 5.7 + - [MariaDB](https://mariadb.org/) \>= 10.4 + +## Installation + +Package is uploaded on [PyPI](https://pypi.org/project/PyMySQL). + +You can install it with pip: + + $ python3 -m pip install PyMySQL + +To use "sha256_password" or "caching_sha2_password" for authenticate, +you need to install additional dependency: + + $ python3 -m pip install PyMySQL[rsa] + +To use MariaDB's "ed25519" authentication method, you need to install +additional dependency: + + $ python3 -m pip install PyMySQL[ed25519] + +## Documentation + +Documentation is available online: + +For support, please refer to the +[StackOverflow](https://stackoverflow.com/questions/tagged/pymysql). + +## Example + +The following examples make use of a simple table + +``` sql +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email` varchar(255) COLLATE utf8_bin NOT NULL, + `password` varchar(255) COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin +AUTO_INCREMENT=1 ; +``` + +``` python +import pymysql.cursors + +# Connect to the database +connection = pymysql.connect(host='localhost', + user='user', + password='passwd', + database='db', + cursorclass=pymysql.cursors.DictCursor) + +with connection: + with connection.cursor() as cursor: + # Create a new record + sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)" + cursor.execute(sql, ('webmaster@python.org', 'very-secret')) + + # connection is not autocommit by default. So you must commit to save + # your changes. + connection.commit() + + with connection.cursor() as cursor: + # Read a single record + sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s" + cursor.execute(sql, ('webmaster@python.org',)) + result = cursor.fetchone() + print(result) +``` + +This example will print: + +``` python +{'password': 'very-secret', 'id': 1} +``` + +## Resources + +- DB-API 2.0: +- MySQL Reference Manuals: +- Getting Help With MariaDB +- MySQL client/server protocol: + +- "Connector" channel in MySQL Community Slack: + +- PyMySQL mailing list: + + +## License + +PyMySQL is released under the MIT License. See LICENSE for more +information. diff --git a/README.rst b/README.rst deleted file mode 100644 index 7a40b986f..000000000 --- a/README.rst +++ /dev/null @@ -1,46 +0,0 @@ -# PyMySQL is looking for a new maintainer! - -Unfortunately I don't have the time to maintain PyMySQL anymore. If you're interested in taking over or forking, please [reach out](https://groups.google.com/forum/#!topic/pymysql-users/hrIYyeGMQwY). - -==================== -PyMySQL Installation -==================== - -.. contents:: -.. - -This package contains a pure-Python MySQL client library. -Documentation on the MySQL client/server protocol can be found here: -http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol -If you would like to run the test suite, edit the config parameters in -pymysql/tests/base.py. The goal of pymysql is to be a drop-in -replacement for MySQLdb and work on CPython 2.3+, Jython, IronPython, PyPy -and Python 3. We test for compatibility by simply changing the import -statements in the Django MySQL backend and running its unit tests as well -as running it against the MySQLdb and myconnpy unit tests. - -Requirements -------------- - -+ Python 2.4 or higher - - * http://www.python.org/ - - * 2.6 is the primary test environment. - -* MySQL 4.1 or higher - - * protocol41 support, experimental 4.0 support - -Installation ------------- - -# easy_install pymysql -# ... or ... -# python setup.py install - -Python 3.0 Support ------------------- - -Simply run the build-py3k.sh script from the local directory. It will -build a working package in the ./py3k directory. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..da9c516dd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/build-py3k.sh b/build-py3k.sh deleted file mode 100755 index 862188c98..000000000 --- a/build-py3k.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -echo "Building Python 3.0 version in ./py3k..." -rm -fr ./py3k -mkdir py3k -cp -rf pymysql py3k/ -cp setup.py py3k/ -cp setup.py.py3k.patch py3k/ -cp CHANGELOG py3k/ -cp README.rst py3k/ -cp LICENSE py3k/ -cd py3k -2to3 .|patch -p0 -patch setup.py setup.py.py3k.patch -cd .. diff --git a/ci/database.json b/ci/database.json new file mode 100644 index 000000000..aad0bfb29 --- /dev/null +++ b/ci/database.json @@ -0,0 +1,4 @@ +[ + {"host": "localhost", "unix_socket": "/var/run/mysqld/mysqld.sock", "user": "root", "password": "", "database": "test1", "use_unicode": true, "local_infile": true}, + {"host": "127.0.0.1", "port": 3306, "user": "test2", "password": "some password", "database": "test2" } +] diff --git a/ci/docker-entrypoint-initdb.d/README b/ci/docker-entrypoint-initdb.d/README new file mode 100644 index 000000000..6a54b93da --- /dev/null +++ b/ci/docker-entrypoint-initdb.d/README @@ -0,0 +1,12 @@ +To test with a MariaDB or MySQL container image: + +docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=1 \ + --name=mysqld -v ./ci/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:z \ + mysql:8.0.26 --local-infile=1 + +cp ci/docker.json pymysql/tests/databases.json + +pytest + + +Note: Some authentication tests that don't match the image version will fail. diff --git a/ci/docker-entrypoint-initdb.d/init.sql b/ci/docker-entrypoint-initdb.d/init.sql new file mode 100644 index 000000000..b741d41c5 --- /dev/null +++ b/ci/docker-entrypoint-initdb.d/init.sql @@ -0,0 +1,7 @@ +create database test1 DEFAULT CHARACTER SET utf8mb4; +create database test2 DEFAULT CHARACTER SET utf8mb4; +create user test2 identified by 'some password'; +grant all on test2.* to test2; +create user test2@localhost identified by 'some password'; +grant all on test2.* to test2@localhost; + diff --git a/ci/docker-entrypoint-initdb.d/mariadb.sql b/ci/docker-entrypoint-initdb.d/mariadb.sql new file mode 100644 index 000000000..912d365a9 --- /dev/null +++ b/ci/docker-entrypoint-initdb.d/mariadb.sql @@ -0,0 +1,2 @@ +/*M!100122 INSTALL SONAME "auth_ed25519" */; +/*M!100122 CREATE FUNCTION ed25519_password RETURNS STRING SONAME "auth_ed25519.so" */; diff --git a/ci/docker-entrypoint-initdb.d/mysql.sql b/ci/docker-entrypoint-initdb.d/mysql.sql new file mode 100644 index 000000000..a4ba0927d --- /dev/null +++ b/ci/docker-entrypoint-initdb.d/mysql.sql @@ -0,0 +1,8 @@ +/*!80001 CREATE USER + user_sha256 IDENTIFIED WITH "sha256_password" BY "pass_sha256_01234567890123456789", + nopass_sha256 IDENTIFIED WITH "sha256_password", + user_caching_sha2 IDENTIFIED WITH "caching_sha2_password" BY "pass_caching_sha2_01234567890123456789", + nopass_caching_sha2 IDENTIFIED WITH "caching_sha2_password" + PASSWORD EXPIRE NEVER */; + +/*!80001 GRANT RELOAD ON *.* TO user_caching_sha2 */; diff --git a/ci/docker.json b/ci/docker.json new file mode 100644 index 000000000..63d19a687 --- /dev/null +++ b/ci/docker.json @@ -0,0 +1,5 @@ +[ + {"host": "127.0.0.1", "port": 3306, "user": "root", "password": "", "database": "test1", "use_unicode": true, "local_infile": true}, + {"host": "127.0.0.1", "port": 3306, "user": "test2", "password": "some password", "database": "test2" }, + {"host": "localhost", "port": 3306, "user": "test2", "password": "some password", "database": "test2", "unix_socket": "/run/mysqld/mysqld.sock"} +] diff --git a/ci/test_mysql.py b/ci/test_mysql.py new file mode 100644 index 000000000..b97978a27 --- /dev/null +++ b/ci/test_mysql.py @@ -0,0 +1,47 @@ +# This is an example test settings file for use with the Django test suite. +# +# The 'sqlite3' backend requires only the ENGINE setting (an in- +# memory database will be used). All other backends will require a +# NAME and potentially authentication information. See the +# following section in the docs for more information: +# +# https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/ +# +# The different databases that Django supports behave differently in certain +# situations, so it is recommended to run the test suite against as many +# database backends as possible. You may want to create a separate settings +# file for each of the backends you test against. + +import pymysql + +pymysql.install_as_MySQLdb() + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_default", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, + "other": { + "ENGINE": "django.db.backends.mysql", + "NAME": "django_other", + "HOST": "127.0.0.1", + "USER": "scott", + "PASSWORD": "tiger", + "TEST": {"CHARSET": "utf8mb3", "COLLATION": "utf8mb3_general_ci"}, + }, +} + +SECRET_KEY = "django_tests_secret_key" + +# Use a fast hasher to speed up tests. +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.MD5PasswordHasher", +] + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +USE_TZ = False diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..919adf200 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +# https://docs.codecov.com/docs/common-recipe-list +coverage: + status: + project: + default: + target: auto + threshold: 3% diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..c1240d2ba --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..48319f033 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx~=8.0 +sphinx-rtd-theme~=3.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..158d0d12f --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,276 @@ +# PyMySQL documentation build configuration file, created by +# sphinx-quickstart on Tue May 17 12:01:11 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath("../../")) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "PyMySQL" +copyright = "2023, Inada Naoki and GitHub contributors" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "0.7" +# The full version, including alpha/beta/rc tags. +release = "0.7.2" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = "PyMySQLdoc" + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + "index", + "PyMySQL.tex", + "PyMySQL Documentation", + "Yutaka Matsubara and GitHub contributors", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + "index", + "pymysql", + "PyMySQL Documentation", + ["Yutaka Matsubara and GitHub contributors"], + 1, + ) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + "index", + "PyMySQL", + "PyMySQL Documentation", + "Yutaka Matsubara and GitHub contributors", + "PyMySQL", + "One line description of project.", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {"http://docs.python.org/": None} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 000000000..e64b64238 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,17 @@ +PyMySQL documentation +===================== + +.. toctree:: + :maxdepth: 2 + + user/index + modules/index + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/source/modules/connections.rst b/docs/source/modules/connections.rst new file mode 100644 index 000000000..7cce62fd3 --- /dev/null +++ b/docs/source/modules/connections.rst @@ -0,0 +1,11 @@ +Connection Object +================= + +.. module:: pymysql.connections + +.. autoclass:: Connection + :members: + :exclude-members: DataError, DatabaseError, Error, InterfaceError, + IntegrityError, InternalError, NotSupportedError, + OperationalError, ProgrammingError, Warning, + escape, literal, write_packet diff --git a/docs/source/modules/cursors.rst b/docs/source/modules/cursors.rst new file mode 100644 index 000000000..19706f3c3 --- /dev/null +++ b/docs/source/modules/cursors.rst @@ -0,0 +1,19 @@ +Cursor Objects +============== + +.. module:: pymysql.cursors + +.. autoclass:: Cursor + :members: + :exclude-members: DataError, DatabaseError, Error, InterfaceError, + IntegrityError, InternalError, NotSupportedError, + OperationalError, ProgrammingError, Warning + +.. autoclass:: SSCursor + :members: + +.. autoclass:: DictCursor + :members: + +.. autoclass:: SSDictCursor + :members: diff --git a/docs/source/modules/index.rst b/docs/source/modules/index.rst new file mode 100644 index 000000000..ef190a117 --- /dev/null +++ b/docs/source/modules/index.rst @@ -0,0 +1,14 @@ +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +For more information, please read the `Python Database API specification +`_. + +.. toctree:: + :maxdepth: 2 + + connections + cursors diff --git a/docs/source/user/development.rst b/docs/source/user/development.rst new file mode 100644 index 000000000..2d80a6248 --- /dev/null +++ b/docs/source/user/development.rst @@ -0,0 +1,34 @@ +.. _development: + +=========== +Development +=========== + +You can help developing PyMySQL by `contributing on GitHub`_. + +.. _contributing on GitHub: https://github.com/PyMySQL/PyMySQL + +Building the documentation +-------------------------- + +Go to the ``docs`` directory and run ``make html``. + + +Test Suite +----------- + +If you would like to run the test suite, create a database for testing like this:: + + mysql -e 'create database test_pymysql DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;' + mysql -e 'create database test_pymysql2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;' + +Then, copy the file ``ci/database.json`` to ``pymysql/tests/databases.json`` +and edit the new file to match your MySQL configuration:: + + $ cp ci/database.json pymysql/tests/databases.json + $ $EDITOR pymysql/tests/databases.json + +To run all the tests, you can use pytest:: + + $ pip install -r requirements-dev.txt + $ pytest -v pymysql diff --git a/docs/source/user/examples.rst b/docs/source/user/examples.rst new file mode 100644 index 000000000..3946db9b9 --- /dev/null +++ b/docs/source/user/examples.rst @@ -0,0 +1,59 @@ +.. _examples: + +======== +Examples +======== + +.. _CRUD: + +CRUD +---- + +The following examples make use of a simple table + +.. code:: sql + + CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email` varchar(255) COLLATE utf8_bin NOT NULL, + `password` varchar(255) COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin + AUTO_INCREMENT=1 ; + + +.. code:: python + + import pymysql.cursors + + # Connect to the database + connection = pymysql.connect(host='localhost', + user='user', + password='passwd', + database='db', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor) + + with connection: + with connection.cursor() as cursor: + # Create a new record + sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)" + cursor.execute(sql, ('webmaster@python.org', 'very-secret')) + + # connection is not autocommit by default. So you must commit to save + # your changes. + connection.commit() + + with connection.cursor() as cursor: + # Read a single record + sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s" + cursor.execute(sql, ('webmaster@python.org',)) + result = cursor.fetchone() + print(result) + + +This example will print: + +.. code:: python + + {'id': 1, 'password': 'very-secret'} diff --git a/docs/source/user/index.rst b/docs/source/user/index.rst new file mode 100644 index 000000000..17b9b07fb --- /dev/null +++ b/docs/source/user/index.rst @@ -0,0 +1,14 @@ +User Guide +------------ + +The PyMySQL user guide explains how to install PyMySQL and how to contribute to +the library as a developer. + + +.. toctree:: + :maxdepth: 1 + + installation + examples + resources + development diff --git a/docs/source/user/installation.rst b/docs/source/user/installation.rst new file mode 100644 index 000000000..9313f14d3 --- /dev/null +++ b/docs/source/user/installation.rst @@ -0,0 +1,32 @@ +.. _installation: + +============ +Installation +============ + +The last stable release is available on PyPI and can be installed with ``pip``:: + + $ python3 -m pip install PyMySQL + +To use "sha256_password" or "caching_sha2_password" for authenticate, +you need to install additional dependency:: + + $ python3 -m pip install PyMySQL[rsa] + +Requirements +------------- + +* Python -- one of the following: + + - CPython_ >= 3.7 + - Latest PyPy_ 3 + +* MySQL Server -- one of the following: + + - MySQL_ >= 5.7 + - MariaDB_ >= 10.3 + +.. _CPython: http://www.python.org/ +.. _PyPy: http://pypy.org/ +.. _MySQL: http://www.mysql.com/ +.. _MariaDB: https://mariadb.org/ diff --git a/docs/source/user/resources.rst b/docs/source/user/resources.rst new file mode 100644 index 000000000..19b3db931 --- /dev/null +++ b/docs/source/user/resources.rst @@ -0,0 +1,14 @@ +.. _resources: + +============ +Resources +============ + +DB-API 2.0: http://www.python.org/dev/peps/pep-0249 + +MySQL Reference Manuals: http://dev.mysql.com/doc/ + +MySQL client/server protocol: +http://dev.mysql.com/doc/internals/en/client-server-protocol.html + +PyMySQL mailing list: https://groups.google.com/forum/#!forum/pymysql-users diff --git a/example.py b/example.py index df24d7c1e..c12f103b5 100644 --- a/example.py +++ b/example.py @@ -1,24 +1,17 @@ #!/usr/bin/env python - import pymysql -#conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock', user='root', passwd=None, db='mysql') - -conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='', db='mysql') - +conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="", db="mysql") cur = conn.cursor() cur.execute("SELECT Host,User FROM user") -# print cur.description +print(cur.description) +print() -# r = cur.fetchall() -# print r -# ...or... -for r in cur.fetchall(): - print r +for row in cur: + print(row) cur.close() conn.close() - diff --git a/pymysql/__init__.py b/pymysql/__init__.py index 0b3686d4c..bbf9023ef 100644 --- a/pymysql/__init__.py +++ b/pymysql/__init__.py @@ -1,7 +1,7 @@ -''' -PyMySQL: A pure-Python drop-in replacement for MySQLdb. +""" +PyMySQL: A pure-Python MySQL client library. -Copyright (c) 2010 PyMySQL contributors +Copyright (c) 2010-2016 PyMySQL contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,40 +20,69 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" -''' +import sys -VERSION = (0, 5, None) +from .constants import FIELD_TYPE +from .err import ( + Warning, + Error, + InterfaceError, + DataError, + DatabaseError, + OperationalError, + IntegrityError, + InternalError, + NotSupportedError, + ProgrammingError, + MySQLError, +) +from .times import ( + Date, + Time, + Timestamp, + DateFromTicks, + TimeFromTicks, + TimestampFromTicks, +) + +# PyMySQL version. +# Used by setuptools and connection_attrs +VERSION = (1, 1, 1, "final", 1) +VERSION_STRING = "1.1.1" + +### for mysqlclient compatibility +### Django checks mysqlclient version. +version_info = (1, 4, 6, "final", 1) +__version__ = "1.4.6" -from constants import FIELD_TYPE -from converters import escape_dict, escape_sequence, escape_string -from err import Warning, Error, InterfaceError, DataError, \ - DatabaseError, OperationalError, IntegrityError, InternalError, \ - NotSupportedError, ProgrammingError, MySQLError -from times import Date, Time, Timestamp, \ - DateFromTicks, TimeFromTicks, TimestampFromTicks -import sys +def get_client_info(): # for MySQLdb compatibility + return __version__ + + +def install_as_MySQLdb(): + """ + After this function is called, any application that imports MySQLdb + will unwittingly actually use pymysql. + """ + sys.modules["MySQLdb"] = sys.modules["pymysql"] + -try: - frozenset -except NameError: - from sets import ImmutableSet as frozenset - try: - from sets import BaseSet as set - except ImportError: - from sets import Set as set +# end of mysqlclient compatibility code threadsafety = 1 apilevel = "2.0" -paramstyle = "format" +paramstyle = "pyformat" -class DBAPISet(frozenset): +from . import connections # noqa: E402 +class DBAPISet(frozenset): def __ne__(self, other): if isinstance(other, set): - return super(DBAPISet, self).__ne__(self, other) + return frozenset.__ne__(self, other) else: return other not in self @@ -67,70 +96,88 @@ def __hash__(self): return frozenset.__hash__(self) -STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, - FIELD_TYPE.VAR_STRING]) -BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB, - FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB]) -NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT, - FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG, - FIELD_TYPE.TINY, FIELD_TYPE.YEAR]) -DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE]) -TIME = DBAPISet([FIELD_TYPE.TIME]) +STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING]) +BINARY = DBAPISet( + [ + FIELD_TYPE.BLOB, + FIELD_TYPE.LONG_BLOB, + FIELD_TYPE.MEDIUM_BLOB, + FIELD_TYPE.TINY_BLOB, + ] +) +NUMBER = DBAPISet( + [ + FIELD_TYPE.DECIMAL, + FIELD_TYPE.DOUBLE, + FIELD_TYPE.FLOAT, + FIELD_TYPE.INT24, + FIELD_TYPE.LONG, + FIELD_TYPE.LONGLONG, + FIELD_TYPE.TINY, + FIELD_TYPE.YEAR, + ] +) +DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE]) +TIME = DBAPISet([FIELD_TYPE.TIME]) TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME]) -DATETIME = TIMESTAMP -ROWID = DBAPISet() +DATETIME = TIMESTAMP +ROWID = DBAPISet() + def Binary(x): """Return x as a binary type.""" - return str(x) + return bytes(x) -def Connect(*args, **kwargs): - """ - Connect to the database; see connections.Connection.__init__() for - more information. - """ - from connections import Connection - return Connection(*args, **kwargs) - -from pymysql import connections as _orig_conn -Connect.__doc__ = _orig_conn.Connection.__init__.__doc__ + """\nSee connections.Connection.__init__() for - information about defaults.""" -del _orig_conn - -def get_client_info(): # for MySQLdb compatibility - return '%s.%s.%s' % VERSION -connect = Connection = Connect +def thread_safe(): + return True # match MySQLdb.thread_safe() -# we include a doctored version_info here for MySQLdb compatibility -version_info = (1,2,2,"final",0) +Connect = connect = Connection = connections.Connection NULL = "NULL" -__version__ = get_client_info() - -def thread_safe(): - return True # match MySQLdb.thread_safe() - -def install_as_MySQLdb(): - """ - After this function is called, any application that imports MySQLdb or - _mysql will unwittingly actually use - """ - sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"] __all__ = [ - 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', 'Date', - 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', 'TimestampFromTicks', - 'DataError', 'DatabaseError', 'Error', 'FIELD_TYPE', 'IntegrityError', - 'InterfaceError', 'InternalError', 'MySQLError', 'NULL', 'NUMBER', - 'NotSupportedError', 'DBAPISet', 'OperationalError', 'ProgrammingError', - 'ROWID', 'STRING', 'TIME', 'TIMESTAMP', 'Warning', 'apilevel', 'connect', - 'connections', 'constants', 'converters', 'cursors', - 'escape_dict', 'escape_sequence', 'escape_string', 'get_client_info', - 'paramstyle', 'threadsafety', 'version_info', - + "BINARY", + "Binary", + "Connect", + "Connection", + "DATE", + "Date", + "Time", + "Timestamp", + "DateFromTicks", + "TimeFromTicks", + "TimestampFromTicks", + "DataError", + "DatabaseError", + "Error", + "FIELD_TYPE", + "IntegrityError", + "InterfaceError", + "InternalError", + "MySQLError", + "NULL", + "NUMBER", + "NotSupportedError", + "DBAPISet", + "OperationalError", + "ProgrammingError", + "ROWID", + "STRING", + "TIME", + "TIMESTAMP", + "Warning", + "apilevel", + "connect", + "connections", + "constants", + "converters", + "cursors", + "get_client_info", + "paramstyle", + "threadsafety", + "version_info", "install_as_MySQLdb", - - "NULL","__version__", - ] + "__version__", +] diff --git a/pymysql/_auth.py b/pymysql/_auth.py new file mode 100644 index 000000000..4790449b8 --- /dev/null +++ b/pymysql/_auth.py @@ -0,0 +1,272 @@ +""" +Implements auth methods +""" + +from .err import OperationalError + + +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization, hashes + from cryptography.hazmat.primitives.asymmetric import padding + + _have_cryptography = True +except ImportError: + _have_cryptography = False + +from functools import partial +import hashlib + + +DEBUG = False +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 = False + + +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 + + +def _roundtrip(conn, send_data): + conn.write_packet(send_data) + pkt = 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()), + algorithm=hashes.SHA1(), + label=None, + ), + ) + + +def sha256_password_auth(conn, pkt): + if conn._secure: + if DEBUG: + print("sha256: Sending plain password") + data = conn.password + b"\0" + return _roundtrip(conn, data) + + if pkt.is_auth_switch_request(): + conn.salt = pkt.read_all() + if conn.salt.endswith(b"\0"): + conn.salt = conn.salt[:-1] + if not conn.server_public_key and conn.password: + # Request server public key + if DEBUG: + print("sha256: Requesting server public key") + pkt = _roundtrip(conn, b"\1") + + if pkt.is_extra_auth_data(): + conn.server_public_key = pkt._data[1:] + if DEBUG: + print("Received public key:\n", conn.server_public_key.decode("ascii")) + + 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 _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) + + +def caching_sha2_password_auth(conn, pkt): + # No password fast path + if not conn.password: + return _roundtrip(conn, b"") + + if pkt.is_auth_switch_request(): + # Try from fast auth + conn.salt = pkt.read_all() + if conn.salt.endswith(b"\0"): # str.removesuffix is available in 3.9 + conn.salt = conn.salt[:-1] + if DEBUG: + print(f"caching sha2: Trying fast path. salt={conn.salt.hex()!r}") + scrambled = scramble_caching_sha2(conn.password, conn.salt) + pkt = _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._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: + if DEBUG: + print("caching sha2: succeeded by fast path.") + pkt = conn._read_packet() + pkt.check_error() # pkt must be OK packet + return pkt + + if n != 4: + raise OperationalError("caching sha2: Unknown result for fast auth: %s" % n) + + if DEBUG: + print("caching sha2: Trying full auth...") + + if conn._secure: + if DEBUG: + print("caching sha2: Sending plain password via secure connection") + return _roundtrip(conn, conn.password + b"\0") + + if not conn.server_public_key: + pkt = _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._data[:1] + ) + + conn.server_public_key = pkt._data[1:] + if DEBUG: + print(conn.server_public_key.decode("ascii")) + + data = sha2_rsa_encrypt(conn.password, conn.salt, conn.server_public_key) + pkt = _roundtrip(conn, data) diff --git a/pymysql/charset.py b/pymysql/charset.py index ce994ebc2..ec8e14e21 100644 --- a/pymysql/charset.py +++ b/pymysql/charset.py @@ -1,244 +1,217 @@ -MBLENGTH = { - 8:1, - 33:3, - 88:2, - 91:2 - } +# Internal use only. Do not use directly. + +MBLENGTH = {8: 1, 33: 3, 88: 2, 91: 2} + class Charset: - def __init__(self, id, name, collation, is_default): + def __init__(self, id, name, collation, is_default=False): self.id, self.name, self.collation = id, name, collation - self.is_default = is_default == 'Yes' + self.is_default = is_default + + def __repr__(self): + return ( + f"Charset(id={self.id}, name={self.name!r}, collation={self.collation!r})" + ) + + @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 + class Charsets: def __init__(self): self._by_id = {} + self._by_name = {} def add(self, c): self._by_id[c.id] = c + if c.is_default: + self._by_name[c.name] = c def by_id(self, id): return self._by_id[id] def by_name(self, name): - for c in self._by_id.values(): - if c.name == name and c.is_default: - return c + name = name.lower() + if name == "utf8": + name = "utf8mb4" + return self._by_name.get(name) + _charsets = Charsets() +charset_by_name = _charsets.by_name +charset_by_id = _charsets.by_id + """ +TODO: update this script. + 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()) -" - + id, name, collation, is_default = l.split(chr(9)) + if is_default.strip() == "Yes": + print('_charsets.add(Charset(%s, \'%s\', \'%s\', True))' \ + % (id, name, collation)) + else: + print('_charsets.add(Charset(%s, \'%s\', \'%s\'))' \ + % (id, name, collation, bool(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(35, 'ucs2', 'ucs2_general_ci', 'Yes')) -_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(54, 'utf16', 'utf16_general_ci', 'Yes')) -_charsets.add(Charset(55, 'utf16', 'utf16_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(60, 'utf32', 'utf32_general_ci', 'Yes')) -_charsets.add(Charset(61, 'utf32', 'utf32_bin', '')) -_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(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(90, 'ucs2', 'ucs2_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(101, 'utf16', 'utf16_unicode_ci', '')) -_charsets.add(Charset(102, 'utf16', 'utf16_icelandic_ci', '')) -_charsets.add(Charset(103, 'utf16', 'utf16_latvian_ci', '')) -_charsets.add(Charset(104, 'utf16', 'utf16_romanian_ci', '')) -_charsets.add(Charset(105, 'utf16', 'utf16_slovenian_ci', '')) -_charsets.add(Charset(106, 'utf16', 'utf16_polish_ci', '')) -_charsets.add(Charset(107, 'utf16', 'utf16_estonian_ci', '')) -_charsets.add(Charset(108, 'utf16', 'utf16_spanish_ci', '')) -_charsets.add(Charset(109, 'utf16', 'utf16_swedish_ci', '')) -_charsets.add(Charset(110, 'utf16', 'utf16_turkish_ci', '')) -_charsets.add(Charset(111, 'utf16', 'utf16_czech_ci', '')) -_charsets.add(Charset(112, 'utf16', 'utf16_danish_ci', '')) -_charsets.add(Charset(113, 'utf16', 'utf16_lithuanian_ci', '')) -_charsets.add(Charset(114, 'utf16', 'utf16_slovak_ci', '')) -_charsets.add(Charset(115, 'utf16', 'utf16_spanish2_ci', '')) -_charsets.add(Charset(116, 'utf16', 'utf16_roman_ci', '')) -_charsets.add(Charset(117, 'utf16', 'utf16_persian_ci', '')) -_charsets.add(Charset(118, 'utf16', 'utf16_esperanto_ci', '')) -_charsets.add(Charset(119, 'utf16', 'utf16_hungarian_ci', '')) -_charsets.add(Charset(120, 'utf16', 'utf16_sinhala_ci', '')) -_charsets.add(Charset(128, 'ucs2', 'ucs2_unicode_ci', '')) -_charsets.add(Charset(129, 'ucs2', 'ucs2_icelandic_ci', '')) -_charsets.add(Charset(130, 'ucs2', 'ucs2_latvian_ci', '')) -_charsets.add(Charset(131, 'ucs2', 'ucs2_romanian_ci', '')) -_charsets.add(Charset(132, 'ucs2', 'ucs2_slovenian_ci', '')) -_charsets.add(Charset(133, 'ucs2', 'ucs2_polish_ci', '')) -_charsets.add(Charset(134, 'ucs2', 'ucs2_estonian_ci', '')) -_charsets.add(Charset(135, 'ucs2', 'ucs2_spanish_ci', '')) -_charsets.add(Charset(136, 'ucs2', 'ucs2_swedish_ci', '')) -_charsets.add(Charset(137, 'ucs2', 'ucs2_turkish_ci', '')) -_charsets.add(Charset(138, 'ucs2', 'ucs2_czech_ci', '')) -_charsets.add(Charset(139, 'ucs2', 'ucs2_danish_ci', '')) -_charsets.add(Charset(140, 'ucs2', 'ucs2_lithuanian_ci', '')) -_charsets.add(Charset(141, 'ucs2', 'ucs2_slovak_ci', '')) -_charsets.add(Charset(142, 'ucs2', 'ucs2_spanish2_ci', '')) -_charsets.add(Charset(143, 'ucs2', 'ucs2_roman_ci', '')) -_charsets.add(Charset(144, 'ucs2', 'ucs2_persian_ci', '')) -_charsets.add(Charset(145, 'ucs2', 'ucs2_esperanto_ci', '')) -_charsets.add(Charset(146, 'ucs2', 'ucs2_hungarian_ci', '')) -_charsets.add(Charset(147, 'ucs2', 'ucs2_sinhala_ci', '')) -_charsets.add(Charset(159, 'ucs2', 'ucs2_general_mysql500_ci', '')) -_charsets.add(Charset(160, 'utf32', 'utf32_unicode_ci', '')) -_charsets.add(Charset(161, 'utf32', 'utf32_icelandic_ci', '')) -_charsets.add(Charset(162, 'utf32', 'utf32_latvian_ci', '')) -_charsets.add(Charset(163, 'utf32', 'utf32_romanian_ci', '')) -_charsets.add(Charset(164, 'utf32', 'utf32_slovenian_ci', '')) -_charsets.add(Charset(165, 'utf32', 'utf32_polish_ci', '')) -_charsets.add(Charset(166, 'utf32', 'utf32_estonian_ci', '')) -_charsets.add(Charset(167, 'utf32', 'utf32_spanish_ci', '')) -_charsets.add(Charset(168, 'utf32', 'utf32_swedish_ci', '')) -_charsets.add(Charset(169, 'utf32', 'utf32_turkish_ci', '')) -_charsets.add(Charset(170, 'utf32', 'utf32_czech_ci', '')) -_charsets.add(Charset(171, 'utf32', 'utf32_danish_ci', '')) -_charsets.add(Charset(172, 'utf32', 'utf32_lithuanian_ci', '')) -_charsets.add(Charset(173, 'utf32', 'utf32_slovak_ci', '')) -_charsets.add(Charset(174, 'utf32', 'utf32_spanish2_ci', '')) -_charsets.add(Charset(175, 'utf32', 'utf32_roman_ci', '')) -_charsets.add(Charset(176, 'utf32', 'utf32_persian_ci', '')) -_charsets.add(Charset(177, 'utf32', 'utf32_esperanto_ci', '')) -_charsets.add(Charset(178, 'utf32', 'utf32_hungarian_ci', '')) -_charsets.add(Charset(179, 'utf32', 'utf32_sinhala_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(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', '')) - -def charset_by_name(name): - return _charsets.by_name(name) - -def charset_by_id(id): - return _charsets.by_id(id) +_charsets.add(Charset(1, "big5", "big5_chinese_ci", True)) +_charsets.add(Charset(2, "latin2", "latin2_czech_cs")) +_charsets.add(Charset(3, "dec8", "dec8_swedish_ci", True)) +_charsets.add(Charset(4, "cp850", "cp850_general_ci", True)) +_charsets.add(Charset(5, "latin1", "latin1_german1_ci")) +_charsets.add(Charset(6, "hp8", "hp8_english_ci", True)) +_charsets.add(Charset(7, "koi8r", "koi8r_general_ci", True)) +_charsets.add(Charset(8, "latin1", "latin1_swedish_ci", True)) +_charsets.add(Charset(9, "latin2", "latin2_general_ci", True)) +_charsets.add(Charset(10, "swe7", "swe7_swedish_ci", True)) +_charsets.add(Charset(11, "ascii", "ascii_general_ci", True)) +_charsets.add(Charset(12, "ujis", "ujis_japanese_ci", True)) +_charsets.add(Charset(13, "sjis", "sjis_japanese_ci", True)) +_charsets.add(Charset(14, "cp1251", "cp1251_bulgarian_ci")) +_charsets.add(Charset(15, "latin1", "latin1_danish_ci")) +_charsets.add(Charset(16, "hebrew", "hebrew_general_ci", True)) +_charsets.add(Charset(18, "tis620", "tis620_thai_ci", True)) +_charsets.add(Charset(19, "euckr", "euckr_korean_ci", True)) +_charsets.add(Charset(20, "latin7", "latin7_estonian_cs")) +_charsets.add(Charset(21, "latin2", "latin2_hungarian_ci")) +_charsets.add(Charset(22, "koi8u", "koi8u_general_ci", True)) +_charsets.add(Charset(23, "cp1251", "cp1251_ukrainian_ci")) +_charsets.add(Charset(24, "gb2312", "gb2312_chinese_ci", True)) +_charsets.add(Charset(25, "greek", "greek_general_ci", True)) +_charsets.add(Charset(26, "cp1250", "cp1250_general_ci", True)) +_charsets.add(Charset(27, "latin2", "latin2_croatian_ci")) +_charsets.add(Charset(28, "gbk", "gbk_chinese_ci", True)) +_charsets.add(Charset(29, "cp1257", "cp1257_lithuanian_ci")) +_charsets.add(Charset(30, "latin5", "latin5_turkish_ci", True)) +_charsets.add(Charset(31, "latin1", "latin1_german2_ci")) +_charsets.add(Charset(32, "armscii8", "armscii8_general_ci", True)) +_charsets.add(Charset(33, "utf8mb3", "utf8mb3_general_ci", True)) +_charsets.add(Charset(34, "cp1250", "cp1250_czech_cs")) +_charsets.add(Charset(36, "cp866", "cp866_general_ci", True)) +_charsets.add(Charset(37, "keybcs2", "keybcs2_general_ci", True)) +_charsets.add(Charset(38, "macce", "macce_general_ci", True)) +_charsets.add(Charset(39, "macroman", "macroman_general_ci", True)) +_charsets.add(Charset(40, "cp852", "cp852_general_ci", True)) +_charsets.add(Charset(41, "latin7", "latin7_general_ci", True)) +_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", True)) +_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", True)) +_charsets.add(Charset(52, "cp1251", "cp1251_general_cs")) +_charsets.add(Charset(53, "macroman", "macroman_bin")) +_charsets.add(Charset(57, "cp1256", "cp1256_general_ci", True)) +_charsets.add(Charset(58, "cp1257", "cp1257_bin")) +_charsets.add(Charset(59, "cp1257", "cp1257_general_ci", True)) +_charsets.add(Charset(63, "binary", "binary", True)) +_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, "utf8mb3", "utf8mb3_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, "utf8mb3", "utf8mb3_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", True)) +_charsets.add(Charset(93, "geostd8", "geostd8_bin")) +_charsets.add(Charset(94, "latin1", "latin1_spanish_ci")) +_charsets.add(Charset(95, "cp932", "cp932_japanese_ci", True)) +_charsets.add(Charset(96, "cp932", "cp932_bin")) +_charsets.add(Charset(97, "eucjpms", "eucjpms_japanese_ci", True)) +_charsets.add(Charset(98, "eucjpms", "eucjpms_bin")) +_charsets.add(Charset(99, "cp1250", "cp1250_polish_ci")) +_charsets.add(Charset(192, "utf8mb3", "utf8mb3_unicode_ci")) +_charsets.add(Charset(193, "utf8mb3", "utf8mb3_icelandic_ci")) +_charsets.add(Charset(194, "utf8mb3", "utf8mb3_latvian_ci")) +_charsets.add(Charset(195, "utf8mb3", "utf8mb3_romanian_ci")) +_charsets.add(Charset(196, "utf8mb3", "utf8mb3_slovenian_ci")) +_charsets.add(Charset(197, "utf8mb3", "utf8mb3_polish_ci")) +_charsets.add(Charset(198, "utf8mb3", "utf8mb3_estonian_ci")) +_charsets.add(Charset(199, "utf8mb3", "utf8mb3_spanish_ci")) +_charsets.add(Charset(200, "utf8mb3", "utf8mb3_swedish_ci")) +_charsets.add(Charset(201, "utf8mb3", "utf8mb3_turkish_ci")) +_charsets.add(Charset(202, "utf8mb3", "utf8mb3_czech_ci")) +_charsets.add(Charset(203, "utf8mb3", "utf8mb3_danish_ci")) +_charsets.add(Charset(204, "utf8mb3", "utf8mb3_lithuanian_ci")) +_charsets.add(Charset(205, "utf8mb3", "utf8mb3_slovak_ci")) +_charsets.add(Charset(206, "utf8mb3", "utf8mb3_spanish2_ci")) +_charsets.add(Charset(207, "utf8mb3", "utf8mb3_roman_ci")) +_charsets.add(Charset(208, "utf8mb3", "utf8mb3_persian_ci")) +_charsets.add(Charset(209, "utf8mb3", "utf8mb3_esperanto_ci")) +_charsets.add(Charset(210, "utf8mb3", "utf8mb3_hungarian_ci")) +_charsets.add(Charset(211, "utf8mb3", "utf8mb3_sinhala_ci")) +_charsets.add(Charset(212, "utf8mb3", "utf8mb3_german2_ci")) +_charsets.add(Charset(213, "utf8mb3", "utf8mb3_croatian_ci")) +_charsets.add(Charset(214, "utf8mb3", "utf8mb3_unicode_520_ci")) +_charsets.add(Charset(215, "utf8mb3", "utf8mb3_vietnamese_ci")) +_charsets.add(Charset(223, "utf8mb3", "utf8mb3_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", True)) +_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")) diff --git a/pymysql/connections.py b/pymysql/connections.py index 99a256cc4..99fcfcd0b 100644 --- a/pymysql/connections.py +++ b/pymysql/connections.py @@ -1,525 +1,239 @@ # Python implementation of the MySQL client-server protocol -# http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol - -try: - import hashlib - sha_new = lambda *args, **kwargs: hashlib.new("sha1", *args, **kwargs) -except ImportError: - import sha - sha_new = sha.new - +# 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 errno +import os import socket -try: - import ssl - SSL_ENABLED = True -except ImportError: - SSL_ENABLED = False - import struct import sys -import os -import ConfigParser +import traceback +import warnings + +from . import _auth + +from .charset import charset_by_name, charset_by_id +from .constants import CLIENT, COMMAND, CR, ER, FIELD_TYPE, SERVER_STATUS +from . import converters +from .cursors import Cursor +from .optionfile import Parser +from .protocol import ( + dump_packet, + MysqlPacket, + FieldDescriptorPacket, + OKPacketWrapper, + EOFPacketWrapper, + LoadLocalPacketWrapper, +) +from . import err, VERSION_STRING try: - import cStringIO as StringIO + import ssl + + SSL_ENABLED = True except ImportError: - import StringIO + ssl = None + SSL_ENABLED = False try: import getpass + DEFAULT_USER = getpass.getuser() -except ImportError: + del getpass +except (ImportError, KeyError, OSError): + # When there's no entry in OS database for a current user: + # KeyError is raised in Python 3.12 and below. + # OSError is raised in Python 3.13+ DEFAULT_USER = None -from charset import MBLENGTH, charset_by_name, charset_by_id -from cursors import Cursor -from constants import FIELD_TYPE, FLAG -from constants import SERVER_STATUS -from constants.CLIENT import * -from constants.COMMAND import * -from util import join_bytes, byte2int, int2byte -from converters import escape_item, encoders, decoders -from err import raise_mysql_exception, Warning, Error, \ - InterfaceError, DataError, DatabaseError, OperationalError, \ - IntegrityError, InternalError, NotSupportedError, ProgrammingError - DEBUG = False - -NULL_COLUMN = 251 -UNSIGNED_CHAR_COLUMN = 251 -UNSIGNED_SHORT_COLUMN = 252 -UNSIGNED_INT24_COLUMN = 253 -UNSIGNED_INT64_COLUMN = 254 -UNSIGNED_CHAR_LENGTH = 1 -UNSIGNED_SHORT_LENGTH = 2 -UNSIGNED_INT24_LENGTH = 3 -UNSIGNED_INT64_LENGTH = 8 - -DEFAULT_CHARSET = 'latin1' - - -def dump_packet(data): - - def is_ascii(data): - if byte2int(data) >= 65 and byte2int(data) <= 122: #data.isalnum(): - return data - return '.' - - try: - print "packet length %d" % len(data) - print "method call[1]: %s" % sys._getframe(1).f_code.co_name - print "method call[2]: %s" % sys._getframe(2).f_code.co_name - print "method call[3]: %s" % sys._getframe(3).f_code.co_name - print "method call[4]: %s" % sys._getframe(4).f_code.co_name - print "method call[5]: %s" % sys._getframe(5).f_code.co_name - print "-" * 88 - except ValueError: pass - dump_data = [data[i:i+16] for i in xrange(len(data)) if i%16 == 0] - for d in dump_data: - print ' '.join(map(lambda x:"%02X" % byte2int(x), d)) + \ - ' ' * (16 - len(d)) + ' ' * 2 + \ - ' '.join(map(lambda x:"%s" % is_ascii(x), d)) - print "-" * 88 - print "" - -def _scramble(password, message): - if password == None or len(password) == 0: - return int2byte(0) - if DEBUG: print 'password=' + password - stage1 = sha_new(password).digest() - stage2 = sha_new(stage1).digest() - s = sha_new() - s.update(message) - s.update(stage2) - result = s.digest() - return _my_crypt(result, stage1) - -def _my_crypt(message1, message2): - length = len(message1) - result = struct.pack('B', length) - for i in xrange(length): - x = (struct.unpack('B', message1[i:i+1])[0] ^ \ - struct.unpack('B', message2[i:i+1])[0]) - result += struct.pack('B', x) - return result - -# old_passwords support ported from libmysql/password.c -SCRAMBLE_LENGTH_323 = 8 - -class RandStruct_323(object): - def __init__(self, seed1, seed2): - self.max_value = 0x3FFFFFFFL - self.seed1 = seed1 % self.max_value - self.seed2 = seed2 % self.max_value - - def my_rnd(self): - self.seed1 = (self.seed1 * 3L + self.seed2) % self.max_value - self.seed2 = (self.seed1 + self.seed2 + 33L) % self.max_value - return float(self.seed1) / float(self.max_value) - -def _scramble_323(password, message): - hash_pass = _hash_password_323(password) - hash_message = _hash_password_323(message[:SCRAMBLE_LENGTH_323]) - hash_pass_n = struct.unpack(">LL", hash_pass) - hash_message_n = struct.unpack(">LL", hash_message) - - rand_st = RandStruct_323(hash_pass_n[0] ^ hash_message_n[0], - hash_pass_n[1] ^ hash_message_n[1]) - outbuf = StringIO.StringIO() - for _ in xrange(min(SCRAMBLE_LENGTH_323, len(message))): - outbuf.write(int2byte(int(rand_st.my_rnd() * 31) + 64)) - extra = int2byte(int(rand_st.my_rnd() * 31)) - out = outbuf.getvalue() - outbuf = StringIO.StringIO() - for c in out: - outbuf.write(int2byte(byte2int(c) ^ byte2int(extra))) - return outbuf.getvalue() - -def _hash_password_323(password): - nr = 1345345333L - add = 7L - nr2 = 0x12345671L - - for c in [byte2int(x) for x in password if x not in (' ', '\t')]: - nr^= (((nr & 63)+add)*c)+ (nr << 8) & 0xFFFFFFFF - nr2= (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF - add= (add + c) & 0xFFFFFFFF - - r1 = nr & ((1L << 31) - 1L) # kill sign bits - r2 = nr2 & ((1L << 31) - 1L) - - # pack - return struct.pack(">LL", r1, r2) - -def pack_int24(n): - return struct.pack('BBB', n&0xFF, (n>>8)&0xFF, (n>>16)&0xFF) - -def unpack_uint16(n): - return struct.unpack(' len(self.__data): - raise Exception('Invalid advance amount (%s) for cursor. ' - 'Position=%s' % (length, new_position)) - self.__position = new_position - - def rewind(self, 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 - - def peek(self, size): - """Look at the first 'size' bytes in packet without moving cursor.""" - 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))) - if DEBUG: - print error - self.dump() - raise AssertionError(error) - return result - - def get_bytes(self, position, 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)] - - def read_length_coded_binary(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. - """ - c = byte2int(self.read(1)) - if c == NULL_COLUMN: - return None - if c < UNSIGNED_CHAR_COLUMN: - return c - elif c == UNSIGNED_SHORT_COLUMN: - return unpack_uint16(self.read(UNSIGNED_SHORT_LENGTH)) - elif c == UNSIGNED_INT24_COLUMN: - return unpack_int24(self.read(UNSIGNED_INT24_LENGTH)) - elif c == UNSIGNED_INT64_COLUMN: - # TODO: what was 'longlong'? confirm it wasn't used? - return 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".) - """ - length = self.read_length_coded_binary() - if length is None: - return None - return self.read(length) - - def is_ok_packet(self): - return byte2int(self.get_bytes(0)) == 0 - - def is_eof_packet(self): - return byte2int(self.get_bytes(0)) == 254 # 'fe' - - def is_resultset_packet(self): - field_count = byte2int(self.get_bytes(0)) - return field_count >= 1 and field_count <= 250 - - def is_error_packet(self): - return byte2int(self.get_bytes(0)) == 255 - - def check_error(self): - if self.is_error_packet(): - self.rewind() - self.advance(1) # field_count == error (we already know that) - errno = unpack_uint16(self.read(2)) - if DEBUG: print "errno = %d" % errno - raise_mysql_exception(self.__data) - - def dump(self): - dump_packet(self.__data) - - -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. - """ - - def __init__(self, *args): - MysqlPacket.__init__(self, *args) - self.__parse_field_descriptor() - - def __parse_field_descriptor(self): - """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() - self.org_table = self.read_length_coded_string() - self.name = self.read_length_coded_string().decode(self.connection.charset) - self.org_name = self.read_length_coded_string() - self.advance(1) # non-null filler - self.charsetnr = struct.unpack('`_ in the + specification. + """ - if use_unicode is None and sys.version_info[0] > 2: - use_unicode = True + _sock = None + _rfile = None + _auth_plugin_name = "" + _closed = False + _secure = False + + 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="", + collation=None, + sql_mode=None, + read_default_file=None, + conv=None, + use_unicode=True, + client_flag=0, + cursorclass=Cursor, + init_command=None, + connect_timeout=10, + read_default_group=None, + autocommit=False, + local_infile=False, + max_allowed_packet=16 * 1024 * 1024, + defer_connect=False, + auth_plugin_map=None, + read_timeout=None, + write_timeout=None, + bind_address=None, + binary_prefix=False, + program_name=None, + server_public_key=None, + ssl=None, + ssl_ca=None, + ssl_cert=None, + ssl_disabled=None, + ssl_key=None, + ssl_key_password=None, + ssl_verify_cert=None, + ssl_verify_identity=None, + compress=None, # not supported + named_pipe=None, # not supported + passwd=None, # deprecated + db=None, # deprecated + ): + if db is not None and database is None: + # We will raise warning in 2022 or later. + # See https://github.com/PyMySQL/PyMySQL/issues/939 + # warnings.warn("'db' is deprecated, use 'database'", DeprecationWarning, 3) + database = db + if passwd is not None and not password: + # We will raise warning in 2022 or later. + # See https://github.com/PyMySQL/PyMySQL/issues/939 + # warnings.warn( + # "'passwd' is deprecated, use 'password'", DeprecationWarning, 3 + # ) + password = passwd if compress or named_pipe: - raise NotImplementedError, "compress and named_pipe arguments are not supported" - - if ssl and (ssl.has_key('capath') or ssl.has_key('cipher')): - raise NotImplementedError, 'ssl options capath and cipher are not supported' + raise NotImplementedError( + "compress and named_pipe arguments are not supported" + ) - self.ssl = False - if ssl: - if not SSL_ENABLED: - raise NotImplementedError, "ssl module not found" - self.ssl = True - client_flag |= SSL - for k in ('key', 'cert', 'ca'): - v = None - if ssl.has_key(k): - v = ssl[k] - setattr(self, k, v) + self._local_infile = bool(local_infile) + if self._local_infile: + client_flag |= CLIENT.LOCAL_FILES if read_default_group and not read_default_file: if sys.platform.startswith("win"): @@ -531,247 +245,588 @@ def __init__(self, host="localhost", user=None, passwd="", if not read_default_group: read_default_group = "client" - cfg = ConfigParser.RawConfigParser() + cfg = Parser() cfg.read(os.path.expanduser(read_default_file)) - def _config(key, default): + def _config(key, arg): + if arg: + return arg try: - return cfg.get(read_default_group,key) - except: - return default + return cfg.get(read_default_group, key) + except Exception: + return arg - user = _config("user",user) - passwd = _config("password",passwd) + user = _config("user", user) + password = _config("password", password) host = _config("host", host) - db = _config("db",db) - unix_socket = _config("socket",unix_socket) + database = _config("database", database) + unix_socket = _config("socket", unix_socket) port = int(_config("port", port)) + bind_address = _config("bind-address", bind_address) charset = _config("default-character-set", charset) + if not ssl: + ssl = {} + if isinstance(ssl, dict): + for key in ["ca", "capath", "cert", "key", "password", "cipher"]: + value = _config("ssl-" + key, ssl.get(key)) + if value: + ssl[key] = value - self.host = host - self.port = port + self.ssl = False + if not ssl_disabled: + if ssl_ca or ssl_cert or ssl_key or ssl_verify_cert or ssl_verify_identity: + ssl = { + "ca": ssl_ca, + "check_hostname": bool(ssl_verify_identity), + "verify_mode": ssl_verify_cert + if ssl_verify_cert is not None + else False, + } + if ssl_cert is not None: + ssl["cert"] = ssl_cert + if ssl_key is not None: + ssl["key"] = ssl_key + if ssl_key_password is not None: + ssl["password"] = ssl_key_password + if ssl: + if not SSL_ENABLED: + raise NotImplementedError("ssl module not found") + self.ssl = True + client_flag |= CLIENT.SSL + self.ctx = self._create_ssl_ctx(ssl) + + 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 = passwd - self.db = db - self.no_delay = no_delay + 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 charset: - self.charset = charset - self.use_unicode = True - else: - self.charset = DEFAULT_CHARSET - self.use_unicode = False - - if use_unicode is not None: - self.use_unicode = use_unicode - - client_flag |= CAPABILITIES - client_flag |= MULTI_STATEMENTS + self.bind_address = bind_address + 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 + if write_timeout is not None and write_timeout <= 0: + raise ValueError("write_timeout should be > 0") + self._write_timeout = write_timeout + + self.charset = charset or DEFAULT_CHARSET + self.collation = collation + self.use_unicode = use_unicode + + self.encoding = charset_by_name(self.charset).encoding + + client_flag |= CLIENT.CAPABILITIES if self.db: - client_flag |= CONNECT_WITH_DB + client_flag |= CLIENT.CONNECT_WITH_DB + self.client_flag = client_flag self.cursorclass = cursorclass - self.connect_timeout = connect_timeout self._result = None self._affected_rows = 0 self.host_info = "Not connected" - self.messages = [] + # 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": "pymysql", + "_client_version": VERSION_STRING, + "_pid": str(os.getpid()), + } + + if program_name: + self._connect_attrs["program_name"] = program_name + + if defer_connect: + self._sock = None + else: + self.connect() - self.autocommit_mode = False - self._connect() + def __enter__(self): + return self + + def __exit__(self, *exc_info): + del exc_info + self.close() + + def _create_ssl_ctx(self, sslp): + if isinstance(sslp, ssl.SSLContext): + return sslp + 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) + + # Python 3.13 enables VERIFY_X509_STRICT by default. + # But self signed certificates that are generated by MySQL automatically + # doesn't pass the verification. + ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT + + 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"), password=sslp.get("password") + ) + if "cipher" in sslp: + ctx.set_ciphers(sslp["cipher"]) + ctx.options |= ssl.OP_NO_SSLv2 + ctx.options |= ssl.OP_NO_SSLv3 + return ctx - self.set_charset(charset) - self.encoders = encoders - self.decoders = conv + def close(self): + """ + Send the quit message and close the socket. - if sql_mode is not None: - c = self.cursor() - c.execute("SET sql_mode=%s", (sql_mode,)) + See `Connection.close() `_ + in the specification. - self.commit() + :raise Error: If the connection is already closed. + """ + if self._closed: + raise err.Error("Already closed") + self._closed = True + if self._sock is None: + return + send_data = struct.pack("`_ + in the specification. + """ + self._execute_command(COMMAND.COM_QUERY, "COMMIT") + self._read_ok_packet() def rollback(self): - ''' Roll back the current transaction ''' - try: - self._execute_command(COM_QUERY, "ROLLBACK") - self.read_packet() - except: - exc,value,tb = sys.exc_info() - self.errorhandler(None, exc, value) + """ + Roll back the current transaction. - def escape(self, obj): - ''' Escape whatever value you pass to it ''' - return escape_item(obj, self.charset) + See `Connection.rollback() `_ + in the specification. + """ + self._execute_command(COMMAND.COM_QUERY, "ROLLBACK") + self._read_ok_packet() + + def show_warnings(self): + """Send the "SHOW WARNINGS" SQL command.""" + self._execute_command(COMMAND.COM_QUERY, "SHOW WARNINGS") + result = MySQLResult(self) + result.read() + return result.rows + + def select_db(self, db): + """ + Set current db. + + :param db: The name of the db. + """ + self._execute_command(COMMAND.COM_INIT_DB, db) + self._read_ok_packet() + + 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)): + ret = self._quote_bytes(obj) + if self._binary_prefix: + ret = "_binary" + ret + return ret + return converters.escape_item(obj, self.charset, mapping=mapping) def literal(self, obj): - ''' Alias for escape() ''' - return escape_item(obj, self.charset) + """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.SERVER_STATUS_NO_BACKSLASH_ESCAPES: + return s.replace("'", "''") + return converters.escape_string(s) + + def _quote_bytes(self, s): + if self.server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES: + return "'{}'".format( + s.replace(b"'", b"''").decode("ascii", "surrogateescape") + ) + return converters.escape_bytes(s) def cursor(self, cursor=None): - ''' Create a new cursor to execute queries with ''' + """ + 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`. + """ if cursor: return cursor(self) return self.cursorclass(self) - def __enter__(self): - ''' Context manager that returns a Cursor ''' - return self.cursor() - - def __exit__(self, exc, value, traceback): - ''' On successful exit, commit. On exception, rollback. ''' - if exc: - self.rollback() - else: - self.commit() - # The following methods are INTERNAL USE ONLY (called from Cursor) def query(self, sql, unbuffered=False): - if DEBUG: - print "sending query: %s" % sql - self._execute_command(COM_QUERY, sql) + # if DEBUG: + # print("DEBUG: sending query:", sql) + if isinstance(sql, str): + sql = sql.encode(self.encoding, "surrogateescape") + self._execute_command(COMMAND.COM_QUERY, sql) self._affected_rows = self._read_query_result(unbuffered=unbuffered) return self._affected_rows - def next_result(self): - self._affected_rows = self._read_query_result() + def next_result(self, unbuffered=False): + self._affected_rows = self._read_query_result(unbuffered=unbuffered) return self._affected_rows def affected_rows(self): return self._affected_rows def kill(self, thread_id): - arg = struct.pack('= 5: + self.client_flag |= CLIENT.MULTI_RESULTS if self.user is None: - raise ValueError, "Did not specify a username" + raise ValueError("Did not specify a username") charset_id = charset_by_name(self.charset).id - self.user = self.user.encode(self.charset) - - data_init = struct.pack('=5.0) + data += authresp + b"\0" + + if self.db and self.server_capabilities & CLIENT.CONNECT_WITH_DB: + if isinstance(self.db, str): + self.db = self.db.encode(self.encoding) + data += self.db + b"\0" + + if self.server_capabilities & CLIENT.PLUGIN_AUTH: + data += (plugin_name or b"") + b"\0" + + if self.server_capabilities & CLIENT.CONNECT_ATTRS: + connect_attrs = b"" + for k, v in self._connect_attrs.items(): + k = k.encode("utf-8") + connect_attrs += _lenenc_int(len(k)) + k + v = v.encode("utf-8") + connect_attrs += _lenenc_int(len(v)) + v + data += _lenenc_int(len(connect_attrs)) + connect_attrs + + self.write_packet(data) + auth_packet = self._read_packet() + + # if authentication method isn't accepted the first byte + # will have the octet 254 + if auth_packet.is_auth_switch_request(): + if DEBUG: + print("received auth switch") + # 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 & CLIENT.PLUGIN_AUTH + and plugin_name is not None + ): + auth_packet = self._process_auth(plugin_name, auth_packet) + else: + raise err.OperationalError("received unknown auth switch request") + elif auth_packet.is_extra_auth_data(): + if DEBUG: + print("received extra data") + # https://dev.mysql.com/doc/internals/en/successful-authentication.html + if self._auth_plugin_name == "caching_sha2_password": + auth_packet = _auth.caching_sha2_password_auth(self, auth_packet) + elif self._auth_plugin_name == "sha256_password": + auth_packet = _auth.sha256_password_auth(self, auth_packet) + else: + raise err.OperationalError( + "Received extra packet for auth method %r", self._auth_plugin_name + ) - self.wfile.write(data) - self.wfile.flush() - auth_packet = MysqlPacket(self) - auth_packet.check_error() - if DEBUG: auth_packet.dump() + if DEBUG: + print("Succeed to auth") + 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 err.OperationalError( + CR.CR_AUTH_PLUGIN_CANNOT_LOAD, + f"Authentication plugin '{plugin_name}'" + f" not loaded: - {type(handler)!r} missing authenticate method", + ) + if plugin_name == b"caching_sha2_password": + return _auth.caching_sha2_password_auth(self, auth_packet) + elif plugin_name == b"sha256_password": + return _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 err.OperationalError( + CR.CR_AUTH_PLUGIN_CANNOT_LOAD, + f"Authentication plugin '{plugin_name}'" + f" not loaded: - {handler!r} missing prompt method", + ) + except TypeError: + raise err.OperationalError( + CR.CR_AUTH_PLUGIN_ERR, + f"Authentication plugin '{plugin_name}'" + f" {handler!r} didn't respond with string. Returned '{resp!r}' to prompt {prompt!r}", + ) + else: + raise err.OperationalError( + CR.CR_AUTH_PLUGIN_CANNOT_LOAD, + f"Authentication plugin '{plugin_name}' not configured", + ) + pkt = self._read_packet() + pkt.check_error() + if pkt.is_ok_packet() or last: + break + return pkt + else: + raise err.OperationalError( + CR.CR_AUTH_PLUGIN_CANNOT_LOAD, + "Authentication plugin '%s' not configured" % plugin_name, + ) + + self.write_packet(data) + pkt = 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 err.OperationalError( + CR.CR_AUTH_PLUGIN_CANNOT_LOAD, + f"Authentication plugin '{plugin_name}'" + f" not loaded: - {plugin_class!r} cannot be constructed with connection object", + ) + else: + handler = None + return handler # _mysql support def thread_id(self): @@ -887,64 +1096,96 @@ def get_proto_info(self): def _get_server_information(self): i = 0 - packet = MysqlPacket(self) + packet = self._read_packet() data = packet.get_all_data() - if DEBUG: dump_packet(data) - #packet_len = byte2int(data[i:i+1]) - #i += 4 - self.protocol_version = byte2int(data[i:i+1]) - + self.protocol_version = data[i] i += 1 - server_end = data.find(int2byte(0), i) - # TODO: is this the correct charset? should it be default_charset? - self.server_version = data[i:server_end].decode(self.charset) + server_end = data.find(b"\0", i) + self.server_version = data[i:server_end].decode("latin1") i = server_end + 1 - self.server_thread_id = struct.unpack('= i + 1: - i += 1 + self.salt = data[i : i + 8] + i += 9 # 8 + 1(filler) + + self.server_capabilities = struct.unpack("= i + 6: + lang, stat, cap_h, salt_len = struct.unpack("= 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 - self.server_language = byte2int(data[i:i+1]) - self.server_charset = charset_by_id(self.server_language).name + # AUTH PLUGIN NAME may appear here. + if self.server_capabilities & CLIENT.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") - i += 16 - if len(data) >= i+12-1: - rest_salt = data[i:i+12] - self.salt += rest_salt + if _DEFAULT_AUTH_PLUGIN is not None: # for tests + self._auth_plugin_name = _DEFAULT_AUTH_PLUGIN def get_server_info(self): return self.server_version - Warning = Warning - Error = Error - InterfaceError = InterfaceError - DatabaseError = DatabaseError - DataError = DataError - OperationalError = OperationalError - IntegrityError = IntegrityError - InternalError = InternalError - ProgrammingError = ProgrammingError - NotSupportedError = NotSupportedError - -# TODO: move OK and EOF packet parsing/logic into a proper subclass -# of MysqlPacket like has been done with FieldDescriptorPacket. -class MySQLResult(object): + Warning = err.Warning + Error = err.Error + InterfaceError = err.InterfaceError + DatabaseError = err.DatabaseError + DataError = err.DataError + OperationalError = err.OperationalError + IntegrityError = err.IntegrityError + InternalError = err.InternalError + ProgrammingError = err.ProgrammingError + NotSupportedError = err.NotSupportedError + +class MySQLResult: def __init__(self, connection): - from weakref import proxy - self.connection = proxy(connection) + """ + :type connection: Connection + """ + self.connection = connection self.affected_rows = None self.insert_id = None - self.server_status = 0 + self.server_status = None self.warning_count = 0 self.message = None self.field_count = 0 @@ -958,29 +1199,42 @@ def __del__(self): self._finish_unbuffered_query() def read(self): - first_packet = self.connection.read_packet() + try: + first_packet = self.connection._read_packet() - # TODO: use classes for different packet types? - if first_packet.is_ok_packet(): - self._read_ok_packet(first_packet) - else: - self._read_result_packet(first_packet) + if first_packet.is_ok_packet(): + self._read_ok_packet(first_packet) + elif first_packet.is_load_local_packet(): + self._read_load_local_packet(first_packet) + else: + self._read_result_packet(first_packet) + finally: + self.connection = None def init_unbuffered_query(self): - self.unbuffered_active = True - first_packet = self.connection.read_packet() + """ + :raise OperationalError: If the connection to the MySQL server is lost. + :raise InternalError: + """ + first_packet = self.connection._read_packet() if first_packet.is_ok_packet(): + self.connection = None self._read_ok_packet(first_packet) - self.unbuffered_active = False + elif first_packet.is_load_local_packet(): + try: + self._read_load_local_packet(first_packet) + finally: + self.connection = None else: - self.field_count = byte2int(first_packet.read(1)) + self.field_count = first_packet.read_length_encoded_integer() 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 + self.unbuffered_active = True def _read_ok_packet(self, first_packet): ok_packet = OKPacketWrapper(first_packet) @@ -989,91 +1243,193 @@ def _read_ok_packet(self, first_packet): 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 + + 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: + sender.send_data() + except: + self.connection._read_packet() # skip ok packet + raise + + ok_packet = self.connection._read_packet() + if ( + not ok_packet.is_ok_packet() + ): # pragma: no cover - upstream induced protocol error + raise err.OperationalError( + CR.CR_COMMANDS_OUT_OF_SYNC, + "Commands Out of Sync", + ) + self._read_ok_packet(ok_packet) def _check_packet_is_eof(self, packet): - if packet.is_eof_packet(): - eof_packet = EOFPacketWrapper(packet) - self.warning_count = eof_packet.warning_count - self.has_next = eof_packet.has_next - return True - return False + if not packet.is_eof_packet(): + return False + # TODO: Support CLIENT.DEPRECATE_EOF + # 1) Add DEPRECATE_EOF to CAPABILITIES + # 2) Mask CAPABILITIES with server_capabilities + # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: + # use OKPacketWrapper instead of EOFPacketWrapper + wp = EOFPacketWrapper(packet) + self.warning_count = wp.warning_count + self.has_next = wp.has_next + return True def _read_result_packet(self, first_packet): - self.field_count = byte2int(first_packet.read(1)) + self.field_count = first_packet.read_length_encoded_integer() self._get_descriptions() self._read_rowdata_packet() def _read_rowdata_packet_unbuffered(self): # Check if in an active query - if self.unbuffered_active == False: return - + if not self.unbuffered_active: + return + # EOF - packet = self.connection.read_packet() + packet = self.connection._read_packet() if self._check_packet_is_eof(packet): self.unbuffered_active = False + self.connection = None self.rows = None return - row = [] - for field in self.fields: - data = packet.read_length_coded_string() - converted = None - if field.type_code in self.connection.decoders: - converter = self.connection.decoders[field.type_code] - if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter) - if data != None: - converted = converter(self.connection, field, data) - row.append(converted) - + row = self._read_row_from_packet(packet) self.affected_rows = 1 - self.rows = tuple((row)) - if DEBUG: self.rows + self.rows = (row,) # rows should tuple of row for MySQL-python compatibility. + return row 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 = self.connection.read_packet() + try: + packet = self.connection._read_packet() + except err.OperationalError as e: + if e.args[0] in ( + ER.QUERY_TIMEOUT, + ER.STATEMENT_TIMEOUT, + ): + # if the query timed out we can simply ignore this error + self.unbuffered_active = False + self.connection = None + return + + raise + if self._check_packet_is_eof(packet): self.unbuffered_active = False + self.connection = None # release reference to kill cyclic reference. - # TODO: implement this as an iteratable so that it is more - # memory efficient and lower-latency to client... def _read_rowdata_packet(self): - """Read a rowdata packet for each data row in the result set.""" - rows = [] - while True: - packet = self.connection.read_packet() - if self._check_packet_is_eof(packet): - break + """Read a rowdata packet for each data row in the result set.""" + rows = [] + while True: + packet = 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) + def _read_row_from_packet(self, packet): row = [] - for field in self.fields: - data = packet.read_length_coded_string() - converted = None - if field.type_code in self.connection.decoders: - converter = self.connection.decoders[field.type_code] - if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter) - if data != None: - converted = converter(self.connection, field, data) - row.append(converted) - - rows.append(tuple(row)) - - self.affected_rows = len(rows) - self.rows = tuple(rows) - if DEBUG: self.rows + 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 DEBUG: + print("DEBUG: DATA = ", data) + if converter is not None: + data = converter(data) + row.append(data) + return tuple(row) 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 xrange(self.field_count): - field = self.connection.read_packet(FieldDescriptorPacket) + + for i in range(self.field_count): + field = self.connection._read_packet(FieldDescriptorPacket) self.fields.append(field) description.append(field.description()) - - eof_packet = self.connection.read_packet() - assert eof_packet.is_eof_packet(), 'Protocol error, expecting EOF' + field_type = field.type_code + if use_unicode: + if field_type == FIELD_TYPE.JSON: + # When SELECT from JSON column: charset = binary + # When SELECT CAST(... AS JSON): charset = connection encoding + # This behavior is different from TEXT / BLOB. + # We should decode result by connection encoding regardless charsetnr. + # See https://github.com/PyMySQL/PyMySQL/issues/488 + encoding = conn_encoding # SELECT CAST(... AS JSON) + elif field_type in TEXT_TYPES: + if field.charsetnr == 63: # binary + # TEXTs with charset=binary means BINARY types. + encoding = None + else: + encoding = conn_encoding + else: + # Integers, Dates and Times, and other basic data is encoded in ascii + encoding = "ascii" + else: + encoding = None + converter = self.connection.decoders.get(field_type) + if converter is converters.through: + converter = None + if DEBUG: + print(f"DEBUG: field={field}, converter={converter}") + self.converters.append((encoding, converter)) + + eof_packet = self.connection._read_packet() + assert eof_packet.is_eof_packet(), "Protocol error, expecting EOF" self.description = tuple(description) + + +class LoadLocalFile: + def __init__(self, filename, connection): + self.filename = filename + self.connection = connection + + def send_data(self): + """Send data packets from the local file to the server""" + if not self.connection._sock: + raise err.InterfaceError(0, "") + conn: Connection = self.connection + + try: + with open(self.filename, "rb") as open_file: + packet_size = min( + conn.max_allowed_packet, 16 * 1024 + ) # 16KB is efficient enough + while True: + chunk = open_file.read(packet_size) + if not chunk: + break + conn.write_packet(chunk) + except OSError: + raise err.OperationalError( + ER.FILE_NOT_FOUND, + f"Can't find file '{self.filename}'", + ) + finally: + if not conn._closed: + # send the empty packet to signify we are done sending data + conn.write_packet(b"") diff --git a/pymysql/constants/CLIENT.py b/pymysql/constants/CLIENT.py index 9d11ea100..34fe57a5d 100644 --- a/pymysql/constants/CLIENT.py +++ b/pymysql/constants/CLIENT.py @@ -1,4 +1,4 @@ - +# https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags LONG_PASSWORD = 1 FOUND_ROWS = 1 << 1 LONG_FLAG = 1 << 2 @@ -12,9 +12,27 @@ INTERACTIVE = 1 << 10 SSL = 1 << 11 IGNORE_SIGPIPE = 1 << 12 -TRANSACTIONS = 1 << 13 +TRANSACTIONS = 1 << 13 SECURE_CONNECTION = 1 << 15 MULTI_STATEMENTS = 1 << 16 MULTI_RESULTS = 1 << 17 -CAPABILITIES = LONG_PASSWORD|LONG_FLAG|TRANSACTIONS| \ - PROTOCOL_41|SECURE_CONNECTION +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 diff --git a/pymysql/constants/COMMAND.py b/pymysql/constants/COMMAND.py index 4a757da25..2d98850b8 100644 --- a/pymysql/constants/COMMAND.py +++ b/pymysql/constants/COMMAND.py @@ -1,4 +1,3 @@ - COM_SLEEP = 0x00 COM_QUIT = 0x01 COM_INIT_DB = 0x02 @@ -9,15 +8,25 @@ 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_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 diff --git a/pymysql/constants/CR.py b/pymysql/constants/CR.py new file mode 100644 index 000000000..deae977e5 --- /dev/null +++ b/pymysql/constants/CR.py @@ -0,0 +1,79 @@ +# flake8: noqa +# 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_INSECURE_API_ERR = 2062 +CR_FILE_NAME_TOO_LONG = 2063 +CR_SSL_FIPS_MODE_ERR = 2064 +CR_DEPRECATED_COMPRESSION_NOT_SUPPORTED = 2065 +CR_COMPRESSION_WRONGLY_CONFIGURED = 2066 +CR_KERBEROS_USER_NOT_FOUND = 2067 +CR_LOAD_DATA_LOCAL_INFILE_REJECTED = 2068 +CR_LOAD_DATA_LOCAL_INFILE_REALPATH_FAIL = 2069 +CR_DNS_SRV_LOOKUP_FAILED = 2070 +CR_MANDATORY_TRACKER_NOT_FOUND = 2071 +CR_INVALID_FACTOR_NO = 2072 +CR_ERROR_LAST = 2072 diff --git a/pymysql/constants/ER.py b/pymysql/constants/ER.py index 553983fb7..98729d12d 100644 --- a/pymysql/constants/ER.py +++ b/pymysql/constants/ER.py @@ -1,4 +1,3 @@ - ERROR_FIRST = 1000 HASHCHK = 1000 NISAMCHK = 1001 @@ -470,3 +469,9 @@ HOSTNAME = 1467 WRONG_STRING_LENGTH = 1468 ERROR_LAST = 1468 + +# MariaDB only +STATEMENT_TIMEOUT = 1969 +QUERY_TIMEOUT = 3024 +# https://github.com/PyMySQL/PyMySQL/issues/607 +CONSTRAINT_FAILED = 4025 diff --git a/pymysql/constants/FIELD_TYPE.py b/pymysql/constants/FIELD_TYPE.py index 3df7ff487..b8b448660 100644 --- a/pymysql/constants/FIELD_TYPE.py +++ b/pymysql/constants/FIELD_TYPE.py @@ -1,5 +1,3 @@ - - DECIMAL = 0 TINY = 1 SHORT = 2 @@ -17,6 +15,7 @@ NEWDATE = 14 VARCHAR = 15 BIT = 16 +JSON = 245 NEWDECIMAL = 246 ENUM = 247 SET = 248 diff --git a/pymysql/constants/SERVER_STATUS.py b/pymysql/constants/SERVER_STATUS.py index 010f542d7..8f8d77688 100644 --- a/pymysql/constants/SERVER_STATUS.py +++ b/pymysql/constants/SERVER_STATUS.py @@ -1,4 +1,3 @@ - SERVER_STATUS_IN_TRANS = 1 SERVER_STATUS_AUTOCOMMIT = 2 SERVER_MORE_RESULTS_EXISTS = 8 @@ -9,4 +8,3 @@ SERVER_STATUS_DB_DROPPED = 256 SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512 SERVER_STATUS_METADATA_CHANGED = 1024 - diff --git a/pymysql/converters.py b/pymysql/converters.py index f08fdf013..dbf97ca75 100644 --- a/pymysql/converters.py +++ b/pymysql/converters.py @@ -1,183 +1,248 @@ -import re import datetime +from decimal import Decimal +import re import time -import sys -from constants import FIELD_TYPE, FLAG -from charset import charset_by_id +from .err import ProgrammingError +from .constants import FIELD_TYPE -PYTHON3 = sys.version_info[0] > 2 -try: - set -except NameError: - try: - from sets import BaseSet as set - except ImportError: - from sets import Set as set - -ESCAPE_REGEX = re.compile(r"[\0\n\r\032\'\"\\]") -ESCAPE_MAP = {'\0': '\\0', '\n': '\\n', '\r': '\\r', '\032': '\\Z', - '\'': '\\\'', '"': '\\"', '\\': '\\\\'} - -def escape_item(val, charset): - if type(val) in [tuple, list, set]: - return escape_sequence(val, charset) - if type(val) is dict: - return escape_dict(val, charset) - if PYTHON3 and hasattr(val, "decode") and not isinstance(val, unicode): - # deal with py3k bytes - val = val.decode(charset) - encoder = encoders[type(val)] - val = encoder(val) - if type(val) in [str, int, unicode]: - return val - val = val.encode(charset) +def escape_item(val, charset, mapping=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 -def escape_dict(val, charset): - n = {} - for k, v in val.items(): - quoted = escape_item(v, charset) - n[k] = quoted - return n -def escape_sequence(val, charset): +def escape_dict(val, charset, mapping=None): + raise TypeError("dict can not be used as parameter") + + +def escape_sequence(val, charset, mapping=None): n = [] for item in val: - quoted = escape_item(item, charset) + quoted = escape_item(item, charset, mapping) n.append(quoted) return "(" + ",".join(n) + ")" -def escape_set(val, charset): - val = map(lambda x: escape_item(x, charset), val) - return ','.join(val) -def escape_bool(value): +def escape_set(val, charset, mapping=None): + return ",".join([escape_item(x, charset, mapping) for x in val]) + + +def escape_bool(value, mapping=None): return str(int(value)) -def escape_object(value): + +def escape_int(value, mapping=None): return str(value) -def escape_int(value): - return value -escape_long = escape_object +def escape_float(value, mapping=None): + s = repr(value) + if s in ("inf", "-inf", "nan"): + raise ProgrammingError("%s can not be used with MySQL" % s) + if "e" not in s: + s += "e0" + return s + -def escape_float(value): - return ('%.15g' % value) +_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("'")] = "\\'" -def escape_string(value): - return ("'%s'" % ESCAPE_REGEX.sub( - lambda match: ESCAPE_MAP.get(match.group(0)), value)) -def escape_unicode(value): - return escape_string(value) +def escape_string(value, mapping=None): + """escapes *value* without adding quote. + + Value should be unicode + """ + return value.translate(_escape_table) + -def escape_None(value): - return 'NULL' +def escape_bytes_prefixed(value, mapping=None): + return "_binary'%s'" % value.decode("ascii", "surrogateescape").translate( + _escape_table + ) -def escape_timedelta(obj): + +def escape_bytes(value, mapping=None): + return "'%s'" % value.decode("ascii", "surrogateescape").translate(_escape_table) + + +def escape_str(value, mapping=None): + return "'%s'" % escape_string(str(value), mapping) + + +def escape_None(value, mapping=None): + return "NULL" + + +def escape_timedelta(obj, mapping=None): seconds = int(obj.seconds) % 60 minutes = int(obj.seconds // 60) % 60 hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24 - return escape_string('%02d:%02d:%02d' % (hours, minutes, seconds)) + 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) + -def escape_time(obj): - s = "%02d:%02d:%02d" % (int(obj.hour), int(obj.minute), - int(obj.second)) +def escape_time(obj, mapping=None): if obj.microsecond: - s += ".%f" % 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) + + +def escape_datetime(obj, mapping=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) - return escape_string(s) -def escape_datetime(obj): - return escape_string(obj.strftime("%Y-%m-%d %H:%M:%S")) +def escape_date(obj, mapping=None): + fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'" + return fmt.format(obj) -def escape_date(obj): - return escape_string(obj.strftime("%Y-%m-%d")) -def escape_struct_time(obj): +def escape_struct_time(obj, mapping=None): return escape_datetime(datetime.datetime(*obj[:6])) -def convert_datetime(connection, field, obj): + +def Decimal2Literal(o, d): + return format(o, "f") + + +def _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}))?" +) + + +def convert_datetime(obj): """Returns a DATETIME or TIMESTAMP column value as a datetime object: - >>> datetime_or_None('2007-02-25 23:06:20') + >>> convert_datetime('2007-02-25 23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) - >>> datetime_or_None('2007-02-25T23:06:20') + >>> convert_datetime('2007-02-25T23:06:20') datetime.datetime(2007, 2, 25, 23, 6, 20) - Illegal values are returned as None: - - >>> datetime_or_None('2007-02-31T23:06:20') is None - True - >>> datetime_or_None('0000-00-00 00:00:00') is None - True + Illegal values are returned as str: + >>> convert_datetime('2007-02-31T23:06:20') + '2007-02-31T23:06:20' + >>> convert_datetime('0000-00-00 00:00:00') + '0000-00-00 00:00:00' """ - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) - if ' ' in obj: - sep = ' ' - elif 'T' in obj: - sep = 'T' - else: - return convert_date(connection, field, obj) + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = DATETIME_RE.match(obj) + if not m: + return convert_date(obj) try: - ymd, hms = obj.split(sep, 1) - return datetime.datetime(*[ int(x) for x in ymd.split('-')+hms.split(':') ]) + 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(connection, field, obj) + return convert_date(obj) + + +TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") -def convert_timedelta(connection, field, obj): + +def convert_timedelta(obj): """Returns a TIME column as a timedelta object: - >>> timedelta_or_None('25:06:17') - datetime.timedelta(1, 3977) - >>> timedelta_or_None('-25:06:17') - datetime.timedelta(-2, 83177) + >>> convert_timedelta('25:06:17') + datetime.timedelta(days=1, seconds=3977) + >>> convert_timedelta('-25:06:17') + datetime.timedelta(days=-2, seconds=82423) - Illegal values are returned as None: + Illegal values are returned as string: - >>> timedelta_or_None('random crap') is None - True + >>> convert_timedelta('random crap') + 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = TIMEDELTA_RE.match(obj) + if not m: + return obj + try: - microseconds = 0 - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) - if "." in obj: - (obj, tail) = obj.split('.') - microseconds = int(tail) - hours, minutes, seconds = obj.split(':') - tdelta = datetime.timedelta( - hours = int(hours), - minutes = int(minutes), - seconds = int(seconds), - microseconds = microseconds + 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 None + return obj -def convert_time(connection, field, obj): + +TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_time(obj): """Returns a TIME column as a time object: - >>> time_or_None('15:06:17') + >>> convert_time('15:06:17') datetime.time(15, 6, 17) - Illegal values are returned as None: + Illegal values are returned as str: - >>> time_or_None('-25:06:17') is None - True - >>> time_or_None('random crap') is None - True + >>> convert_time('-25:06:17') + '-25:06:17' + >>> convert_time('random crap') + 'random crap' Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but can accept values as (+|-)DD HH:MM:SS. The latter format will not @@ -188,169 +253,111 @@ def convert_time(connection, field, obj): to be treated as time-of-day and not a time offset, then you can use set this function as the converter for FIELD_TYPE.TIME. """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = TIME_RE.match(obj) + if not m: + return obj + try: - microseconds = 0 - if "." in obj: - (obj, tail) = obj.split('.') - microseconds = int(tail) - hours, minutes, seconds = obj.split(':') - return datetime.time(hour=int(hours), minute=int(minutes), - second=int(seconds), microsecond=microseconds) + 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 None + return obj + -def convert_date(connection, field, obj): +def convert_date(obj): """Returns a DATE column as a date object: - >>> date_or_None('2007-02-26') + >>> convert_date('2007-02-26') datetime.date(2007, 2, 26) - Illegal values are returned as None: - - >>> date_or_None('2007-02-31') is None - True - >>> date_or_None('0000-00-00') is None - True + Illegal values are returned as str: + >>> convert_date('2007-02-31') + '2007-02-31' + >>> convert_date('0000-00-00') + '0000-00-00' """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") try: - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) - return datetime.date(*[ int(x) for x in obj.split('-', 2) ]) + return datetime.date(*[int(x) for x in obj.split("-", 2)]) except ValueError: - return None + return obj -def convert_mysql_timestamp(connection, field, timestamp): - """Convert a MySQL TIMESTAMP to a Timestamp object. - MySQL >= 4.1 returns TIMESTAMP in the same format as DATETIME: +def through(x): + return x - >>> mysql_timestamp_converter('2007-02-25 22:32:17') - datetime.datetime(2007, 2, 25, 22, 32, 17) - MySQL < 4.1 uses a big string of numbers: +# 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 - >>> mysql_timestamp_converter('20070225223217') - datetime.datetime(2007, 2, 25, 22, 32, 17) - - Illegal values are returned as None: - - >>> mysql_timestamp_converter('2007-02-31 22:32:17') is None - True - >>> mysql_timestamp_converter('00000000000000') is None - True - - """ - if not isinstance(timestamp, unicode): - timestamp = timestamp.decode(connection.charset) - - if timestamp[4] == '-': - return convert_datetime(connection, field, timestamp) - timestamp += "0"*(14-len(timestamp)) # padding - year, month, day, hour, minute, second = \ - int(timestamp[:4]), int(timestamp[4:6]), int(timestamp[6:8]), \ - int(timestamp[8:10]), int(timestamp[10:12]), int(timestamp[12:14]) - try: - return datetime.datetime(year, month, day, hour, minute, second) - except ValueError: - return None - -def convert_set(s): - return set(s.split(",")) - -def convert_bit(connection, field, 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 - return b - -def convert_characters(connection, field, data): - field_charset = charset_by_id(field.charsetnr).name - if field.flags & FLAG.SET: - return convert_set(data.decode(field_charset)) - if field.flags & FLAG.BINARY: - return data - - if connection.use_unicode: - data = data.decode(field_charset) - elif connection.charset != field_charset: - data = data.decode(field_charset) - data = data.encode(connection.charset) - return data - -def convert_int(connection, field, data): - return int(data) - -def convert_long(connection, field, data): - return long(data) - -def convert_float(connection, field, data): - return float(data) encoders = { - bool: escape_bool, - int: escape_int, - long: escape_long, - float: escape_float, - str: escape_string, - unicode: escape_unicode, - tuple: escape_sequence, - list:escape_sequence, - set: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, - } + 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, +} + decoders = { - FIELD_TYPE.BIT: convert_bit, - FIELD_TYPE.TINY: convert_int, - FIELD_TYPE.SHORT: convert_int, - FIELD_TYPE.LONG: convert_long, - FIELD_TYPE.FLOAT: convert_float, - FIELD_TYPE.DOUBLE: convert_float, - FIELD_TYPE.DECIMAL: convert_float, - FIELD_TYPE.NEWDECIMAL: convert_float, - FIELD_TYPE.LONGLONG: convert_long, - FIELD_TYPE.INT24: convert_int, - FIELD_TYPE.YEAR: convert_int, - FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp, - FIELD_TYPE.DATETIME: convert_datetime, - FIELD_TYPE.TIME: convert_timedelta, - FIELD_TYPE.DATE: convert_date, - FIELD_TYPE.SET: convert_set, - FIELD_TYPE.BLOB: convert_characters, - FIELD_TYPE.TINY_BLOB: convert_characters, - FIELD_TYPE.MEDIUM_BLOB: convert_characters, - FIELD_TYPE.LONG_BLOB: convert_characters, - FIELD_TYPE.STRING: convert_characters, - FIELD_TYPE.VAR_STRING: convert_characters, - FIELD_TYPE.VARCHAR: convert_characters, - #FIELD_TYPE.BLOB: str, - #FIELD_TYPE.STRING: str, - #FIELD_TYPE.VAR_STRING: str, - #FIELD_TYPE.VARCHAR: str - } -conversions = decoders # for MySQLdb compatibility - -try: - # python version > 2.3 - from decimal import Decimal - def convert_decimal(connection, field, data): - data = data.decode(connection.charset) - return Decimal(data) - decoders[FIELD_TYPE.DECIMAL] = convert_decimal - decoders[FIELD_TYPE.NEWDECIMAL] = convert_decimal - - def escape_decimal(obj): - return unicode(obj) - encoders[Decimal] = escape_decimal - -except ImportError: - pass + FIELD_TYPE.BIT: convert_bit, + FIELD_TYPE.TINY: int, + FIELD_TYPE.SHORT: int, + FIELD_TYPE.LONG: int, + FIELD_TYPE.FLOAT: float, + FIELD_TYPE.DOUBLE: float, + FIELD_TYPE.LONGLONG: int, + FIELD_TYPE.INT24: int, + FIELD_TYPE.YEAR: int, + FIELD_TYPE.TIMESTAMP: convert_datetime, + FIELD_TYPE.DATETIME: convert_datetime, + FIELD_TYPE.TIME: convert_timedelta, + FIELD_TYPE.DATE: convert_date, + FIELD_TYPE.BLOB: through, + FIELD_TYPE.TINY_BLOB: through, + FIELD_TYPE.MEDIUM_BLOB: through, + FIELD_TYPE.LONG_BLOB: through, + FIELD_TYPE.STRING: through, + FIELD_TYPE.VAR_STRING: through, + FIELD_TYPE.VARCHAR: through, + FIELD_TYPE.DECIMAL: Decimal, + FIELD_TYPE.NEWDECIMAL: Decimal, +} + + +# for MySQLdb compatibility +conversions = encoders.copy() +conversions.update(decoders) +Thing2Literal = escape_str + +# Run doctests with `pytest --doctest-modules pymysql/converters.py` diff --git a/pymysql/cursors.py b/pymysql/cursors.py index f9775f569..8be05ca23 100644 --- a/pymysql/cursors.py +++ b/pymysql/cursors.py @@ -1,67 +1,78 @@ -# -*- coding: utf-8 -*- -import struct import re +import warnings +from . import err -try: - import cStringIO as StringIO -except ImportError: - import StringIO -from err import Warning, Error, InterfaceError, DataError, \ - DatabaseError, OperationalError, IntegrityError, InternalError, \ - NotSupportedError, ProgrammingError +#: 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, +) -insert_values = re.compile(r'\svalues\s*(\(.+\))', re.IGNORECASE) -class Cursor(object): - ''' - This is the object you use to interact with the database. - ''' +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. + max_stmt_length = 1024000 + def __init__(self, connection): - ''' - Do not create an instance of a Cursor yourself. Call - connections.Connection.cursor(). - ''' - from weakref import proxy - self.connection = proxy(connection) + self.connection = connection + self.warning_count = 0 self.description = None self.rownumber = 0 self.rowcount = -1 self.arraysize = 1 self._executed = None - self.messages = [] - self.errorhandler = connection.errorhandler - self._has_next = None - self._rows = () - - def __del__(self): - ''' - When this gets GC'd close it. - ''' - self.close() + self._result = None + self._rows = None def close(self): - ''' + """ Closing a cursor just exhausts all remaining data. - ''' - if not self.connection: + """ + conn = self.connection + if conn is None: return try: while self.nextset(): pass - except: - pass + finally: + self.connection = None + + def __enter__(self): + return self - self.connection = None + def __exit__(self, *exc_info): + del exc_info + self.close() def _get_db(self): if not self.connection: - self.errorhandler(self, ProgrammingError, "cursor closed") + raise err.ProgrammingError("Cursor closed") return self.connection def _check_executed(self): if not self._executed: - self.errorhandler(self, ProgrammingError, "execute() first") + raise err.ProgrammingError("execute() first") + + def _conv_row(self, row): + return row def setinputsizes(self, *args): """Does nothing, required by DB API.""" @@ -69,76 +80,155 @@ def setinputsizes(self, *args): def setoutputsizes(self, *args): """Does nothing, required by DB API.""" - def nextset(self): - ''' Get the next query set ''' - if self._executed: - self.fetchall() - del self.messages[:] - - if not self._has_next: + 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 - connection = self._get_db() - connection.next_result() + if not current_result.has_next: + return None + self._result = None + self._clear_result() + conn.next_result(unbuffered=unbuffered) self._do_get_result() return True - def execute(self, query, args=None): - ''' Execute a query ''' - from sys import exc_info + def nextset(self): + return self._nextset(False) - conn = self._get_db() - charset = conn.charset - del self.messages[:] + 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) - # TODO: make sure that conn.escape is correct + def mogrify(self, query, args=None): + """ + Returns the exact string that would be sent to the database by calling the + execute() method. - if isinstance(query, unicode): - query = query.encode(charset) + :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: - if isinstance(args, tuple) or isinstance(args, list): - escaped_args = tuple(conn.escape(arg) for arg in args) - elif isinstance(args, dict): - escaped_args = dict((key, conn.escape(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 - escaped_args = conn.escape(args) + query = query % self._escape_args(args, conn) - query = query % escaped_args + return query - result = 0 - try: - result = self._query(query) - except: - exc, value, tb = exc_info() - del tb - self.messages.append((exc,value)) - self.errorhandler(self, exc, value) + 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 self.nextset(): + pass + + query = self.mogrify(query, args) + result = self._query(query) self._executed = query return result def executemany(self, query, args): - ''' Run several data against one query ''' - del self.messages[:] - #conn = self._get_db() + """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 - #charset = conn.charset - #if isinstance(query, unicode): - # query = query.encode(charset) - self.rowcount = sum([ self.execute(query, arg) for arg in args ]) + m = RE_INSERT_VALUES.match(query) + if m: + q_prefix = m.group(1) % () + q_values = m.group(2).rstrip() + q_postfix = m.group(3) or "" + assert q_values[0] == "(" and q_values[-1] == ")" + return self._do_execute_many( + q_prefix, + q_values, + q_postfix, + args, + self.max_stmt_length, + self._get_db().encoding, + ) + + self.rowcount = sum(self.execute(query, arg) for arg in args) return self.rowcount + 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 += self.execute(sql + postfix) + sql = bytearray(prefix) + else: + sql += b"," + sql += v + rows += self.execute(sql + postfix) + self.rowcount = rows + return rows def callproc(self, procname, args=()): - """Execute stored procedure procname with args + """Execute stored procedure procname with args. - procname -- string, name of procedure to execute on server + :param procname: Name of procedure to execute on server. + :type procname: str - args -- Sequence of parameters to use with procedure + :param args: Sequence of parameters to use with procedure. + :type args: tuple or list Returns the original args. @@ -162,25 +252,26 @@ def callproc(self, procname, args=()): disconnected. """ conn = self._get_db() - for index, arg in enumerate(args): - q = "SET @_%s_%d=%s" % (procname, index, conn.escape(arg)) - if isinstance(q, unicode): - q = q.encode(conn.charset) - self._query(q) + if args: + fmt = f"@_{procname}_%d=%s" + self._query( + "SET %s" + % ",".join( + fmt % (index, conn.escape(arg)) for index, arg in enumerate(args) + ) + ) self.nextset() - q = "CALL %s(%s)" % (procname, - ','.join(['@_%s_%d' % (procname, i) - for i in range(len(args))])) - if isinstance(q, unicode): - q = q.encode(conn.charset) + q = "CALL {}({})".format( + procname, + ",".join(["@_%s_%d" % (procname, i) for i in range(len(args))]), + ) self._query(q) self._executed = q - return args def fetchone(self): - ''' Fetch the next row ''' + """Fetch the next row.""" self._check_executed() if self._rows is None or self.rownumber >= len(self._rows): return None @@ -189,175 +280,198 @@ def fetchone(self): return result def fetchmany(self, size=None): - ''' Fetch several rows ''' + """Fetch several rows.""" self._check_executed() - end = self.rownumber + (size or self.arraysize) - result = self._rows[self.rownumber:end] if self._rows is None: - return None + # Django expects () for EOF. + # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 + return () + end = self.rownumber + (size or self.arraysize) + result = self._rows[self.rownumber : end] self.rownumber = min(end, len(self._rows)) return result def fetchall(self): - ''' Fetch all the rows ''' + """Fetch all the rows.""" self._check_executed() if self._rows is None: - return None + return [] if self.rownumber: - result = self._rows[self.rownumber:] + result = self._rows[self.rownumber :] else: result = self._rows self.rownumber = len(self._rows) return result - def scroll(self, value, mode='relative'): + def scroll(self, value, mode="relative"): self._check_executed() - if mode == 'relative': + if mode == "relative": r = self.rownumber + value - elif mode == 'absolute': + elif mode == "absolute": r = value else: - self.errorhandler(self, ProgrammingError, - "unknown scroll mode %s" % mode) + raise err.ProgrammingError("unknown scroll mode %s" % mode) - if r < 0 or r >= len(self._rows): - self.errorhandler(self, IndexError, "out of range") + if not (0 <= r < len(self._rows)): + raise IndexError("out of range") self.rownumber = r def _query(self, q): conn = self._get_db() - self._last_executed = q + self._clear_result() conn.query(q) self._do_get_result() return self.rowcount + def _clear_result(self): + self.rownumber = 0 + self._result = None + + self.rowcount = 0 + self.warning_count = 0 + self.description = None + self.lastrowid = None + self._rows = None + def _do_get_result(self): conn = self._get_db() - self.rowcount = conn._result.affected_rows - self.rownumber = 0 - self.description = conn._result.description - self.lastrowid = conn._result.insert_id - self._rows = conn._result.rows - self._has_next = conn._result.has_next + self._result = result = conn._result + + self.rowcount = result.affected_rows + self.warning_count = result.warning_count + self.description = result.description + self.lastrowid = result.insert_id + self._rows = result.rows def __iter__(self): - return iter(self.fetchone, None) + return self - Warning = Warning - Error = Error - InterfaceError = InterfaceError - DatabaseError = DatabaseError - DataError = DataError - OperationalError = OperationalError - IntegrityError = IntegrityError - InternalError = InternalError - ProgrammingError = ProgrammingError - NotSupportedError = NotSupportedError - -class DictCursor(Cursor): - """A cursor which returns results as a dictionary""" + def __next__(self): + row = self.fetchone() + if row is None: + raise StopIteration + return row - def execute(self, query, args=None): - result = super(DictCursor, self).execute(query, args) + def __getattr__(self, name): + # DB-API 2.0 optional extension says these errors can be accessed + # via Connection object. But MySQLdb had defined them on Cursor object. + if name in ( + "Warning", + "Error", + "InterfaceError", + "DatabaseError", + "DataError", + "OperationalError", + "IntegrityError", + "InternalError", + "ProgrammingError", + "NotSupportedError", + ): + # Deprecated since v1.1 + warnings.warn( + "PyMySQL errors hould be accessed from `pymysql` package", + DeprecationWarning, + stacklevel=2, + ) + return getattr(err, name) + raise AttributeError(name) + + +class DictCursorMixin: + # You can override this to use OrderedDict or other dict-like types. + dict_type = dict + + def _do_get_result(self): + super()._do_get_result() + fields = [] if self.description: - self._fields = [ field[0] for field in self.description ] - return result + for f in self._result.fields: + name = f.name + if name in fields: + name = f.table_name + "." + name + fields.append(name) + self._fields = fields - def fetchone(self): - ''' Fetch the next row ''' - self._check_executed() - if self._rows is None or self.rownumber >= len(self._rows): - return None - result = dict(zip(self._fields, self._rows[self.rownumber])) - self.rownumber += 1 - return result + if fields and self._rows: + self._rows = [self._conv_row(r) for r in self._rows] - def fetchmany(self, size=None): - ''' Fetch several rows ''' - self._check_executed() - if self._rows is None: + def _conv_row(self, row): + if row is None: return None - end = self.rownumber + (size or self.arraysize) - result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:end] ] - self.rownumber = min(end, len(self._rows)) - return tuple(result) + return self.dict_type(zip(self._fields, row)) + + +class DictCursor(DictCursorMixin, Cursor): + """A cursor which returns results as a dictionary""" - def fetchall(self): - ''' Fetch all the rows ''' - self._check_executed() - if self._rows is None: - return None - if self.rownumber: - result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:] ] - else: - result = [ dict(zip(self._fields, r)) for r in self._rows ] - self.rownumber = len(self._rows) - return tuple(result) 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, + 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. """ - + + def _conv_row(self, row): + return row + def close(self): - conn = self._get_db() - conn._result._finish_unbuffered_query() - + conn = self.connection + if conn is None: + return + + if self._result is not None and self._result is conn._result: + self._result._finish_unbuffered_query() + try: - if self._has_next: - while self.nextset(): pass - except: pass + while self.nextset(): + pass + finally: + self.connection = None + + __del__ = close def _query(self, q): conn = self._get_db() - self._last_executed = q + self._clear_result() conn.query(q, unbuffered=True) self._do_get_result() return self.rowcount - + + def nextset(self): + return self._nextset(unbuffered=True) + def read_next(self): - """ Read next row """ - - conn = self._get_db() - conn._result._read_rowdata_packet_unbuffered() - return conn._result.rows - + """Read next row.""" + return self._conv_row(self._result._read_rowdata_packet_unbuffered()) + def fetchone(self): - """ Fetch next row """ - + """Fetch next row.""" self._check_executed() row = self.read_next() if row is None: + self.warning_count = self._result.warning_count return None self.rownumber += 1 return row - + 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 = self.fetchone() - if row is None: - break - rows.append(row) - return tuple(rows) + return list(self.fetchall_unbuffered()) def fetchall_unbuffered(self): """ @@ -365,46 +479,53 @@ def fetchall_unbuffered(self): however, it doesn't make sense to return everything in a list, as that would use ridiculous memory for large result sets. """ - - row = self.fetchone() - while row is not None: - yield row - row = self.fetchone() - + return iter(self.fetchone, None) + def fetchmany(self, size=None): - """ Fetch many """ - + """Fetch many.""" self._check_executed() if size is None: size = self.arraysize - + rows = [] - for i in range(0, size): + for i in range(size): row = self.read_next() if row is None: + self.warning_count = self._result.warning_count break rows.append(row) self.rownumber += 1 - return tuple(rows) - - def scroll(self, value, mode='relative'): + if not rows: + # Django expects () for EOF. + # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 + return () + return rows + + def scroll(self, value, mode="relative"): self._check_executed() - if not mode == 'relative' and not mode == 'absolute': - self.errorhandler(self, ProgrammingError, - "unknown scroll mode %s" % mode) - - if mode == 'relative': + + if mode == "relative": if value < 0: - self.errorhandler(self, NotSupportedError, - "Backwards scrolling not supported by this cursor") - - for i in range(0, value): self.read_next() + raise err.NotSupportedError( + "Backwards scrolling not supported by this cursor" + ) + + for _ in range(value): + self.read_next() self.rownumber += value - else: + elif mode == "absolute": if value < self.rownumber: - self.errorhandler(self, NotSupportedError, - "Backwards scrolling not supported by this cursor") - + raise err.NotSupportedError( + "Backwards scrolling not supported by this cursor" + ) + end = value - self.rownumber - for i in range(0, end): self.read_next() + for _ in range(end): + self.read_next() self.rownumber = value + else: + raise err.ProgrammingError("unknown scroll mode %s" % mode) + + +class SSDictCursor(DictCursorMixin, SSCursor): + """An unbuffered cursor, which returns results as a dictionary""" diff --git a/pymysql/err.py b/pymysql/err.py index b4322c633..dac65d3be 100644 --- a/pymysql/err.py +++ b/pymysql/err.py @@ -1,57 +1,39 @@ import struct +from .constants import ER -try: - StandardError, Warning -except ImportError: - try: - from exceptions import StandardError, Warning - except ImportError: - import sys - e = sys.modules['exceptions'] - StandardError = e.StandardError - Warning = e.Warning - -from constants import ER -import sys - -class MySQLError(StandardError): - + +class MySQLError(Exception): """Exception related to operation with MySQL.""" class Warning(Warning, MySQLError): - """Exception raised for important warnings like data truncations while inserting, etc.""" -class Error(MySQLError): +class Error(MySQLError): """Exception that is the base class of all other error exceptions (not Warning).""" class InterfaceError(Error): - """Exception raised for errors that are related to the database interface rather than the database itself.""" class DatabaseError(Error): - """Exception raised for errors that are related to the database.""" class DataError(DatabaseError): - """Exception raised for errors that are due to problems with the processed data like division by zero, numeric value out of range, etc.""" class OperationalError(DatabaseError): - """Exception raised for errors that are related to the database's operation and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs, the data source name is not @@ -60,28 +42,24 @@ class OperationalError(DatabaseError): class IntegrityError(DatabaseError): - """Exception raised when the relational integrity of the database is affected, e.g. a foreign key check fails, duplicate key, etc.""" class InternalError(DatabaseError): - """Exception raised when the database encounters an internal error, e.g. the cursor is not valid anymore, the transaction is out of sync, etc.""" class ProgrammingError(DatabaseError): - """Exception raised for programming errors, e.g. table not found or already exists, syntax error in the SQL statement, wrong number of parameters specified, etc.""" class NotSupportedError(DatabaseError): - """Exception raised in case a method or database API was used which is not supported by the database, e.g. requesting a .rollback() on a connection that does not support transaction or @@ -90,58 +68,83 @@ class NotSupportedError(DatabaseError): error_map = {} + def _map_error(exc, *errors): for error in errors: error_map[error] = exc -_map_error(ProgrammingError, ER.DB_CREATE_EXISTS, ER.SYNTAX_ERROR, - ER.PARSE_ERROR, ER.NO_SUCH_TABLE, ER.WRONG_DB_NAME, - ER.WRONG_TABLE_NAME, ER.FIELD_SPECIFIED_TWICE, - ER.INVALID_GROUP_FUNC_USE, ER.UNSUPPORTED_EXTENSION, - ER.TABLE_MUST_HAVE_COLUMNS, ER.CANT_DO_THIS_DURING_AN_TRANSACTION) -_map_error(DataError, ER.WARN_DATA_TRUNCATED, ER.WARN_NULL_TO_NOTNULL, - ER.WARN_DATA_OUT_OF_RANGE, ER.NO_DEFAULT, ER.PRIMARY_CANT_HAVE_NULL, - ER.DATA_TOO_LONG, ER.DATETIME_FUNCTION_OVERFLOW) -_map_error(IntegrityError, ER.DUP_ENTRY, ER.NO_REFERENCED_ROW, - ER.NO_REFERENCED_ROW_2, ER.ROW_IS_REFERENCED, ER.ROW_IS_REFERENCED_2, - ER.CANNOT_ADD_FOREIGN) -_map_error(NotSupportedError, ER.WARNING_NOT_COMPLETE_ROLLBACK, - ER.NOT_SUPPORTED_YET, ER.FEATURE_DISABLED, ER.UNKNOWN_STORAGE_ENGINE) -_map_error(OperationalError, ER.DBACCESS_DENIED_ERROR, ER.ACCESS_DENIED_ERROR, - ER.TABLEACCESS_DENIED_ERROR, ER.COLUMNACCESS_DENIED_ERROR) -del _map_error, ER +_map_error( + ProgrammingError, + ER.DB_CREATE_EXISTS, + ER.SYNTAX_ERROR, + ER.PARSE_ERROR, + ER.NO_SUCH_TABLE, + ER.WRONG_DB_NAME, + ER.WRONG_TABLE_NAME, + ER.FIELD_SPECIFIED_TWICE, + ER.INVALID_GROUP_FUNC_USE, + ER.UNSUPPORTED_EXTENSION, + ER.TABLE_MUST_HAVE_COLUMNS, + ER.CANT_DO_THIS_DURING_AN_TRANSACTION, + ER.WRONG_DB_NAME, + ER.WRONG_COLUMN_NAME, +) +_map_error( + DataError, + ER.WARN_DATA_TRUNCATED, + ER.WARN_NULL_TO_NOTNULL, + ER.WARN_DATA_OUT_OF_RANGE, + ER.NO_DEFAULT, + ER.PRIMARY_CANT_HAVE_NULL, + ER.DATA_TOO_LONG, + ER.DATETIME_FUNCTION_OVERFLOW, + ER.TRUNCATED_WRONG_VALUE_FOR_FIELD, + ER.ILLEGAL_VALUE_FOR_TYPE, +) +_map_error( + IntegrityError, + ER.DUP_ENTRY, + ER.NO_REFERENCED_ROW, + ER.NO_REFERENCED_ROW_2, + ER.ROW_IS_REFERENCED, + ER.ROW_IS_REFERENCED_2, + ER.CANNOT_ADD_FOREIGN, + ER.BAD_NULL_ERROR, +) +_map_error( + NotSupportedError, + ER.WARNING_NOT_COMPLETE_ROLLBACK, + ER.NOT_SUPPORTED_YET, + ER.FEATURE_DISABLED, + ER.UNKNOWN_STORAGE_ENGINE, +) +_map_error( + OperationalError, + ER.DBACCESS_DENIED_ERROR, + ER.ACCESS_DENIED_ERROR, + ER.CON_COUNT_ERROR, + ER.TABLEACCESS_DENIED_ERROR, + ER.COLUMNACCESS_DENIED_ERROR, + ER.CONSTRAINT_FAILED, + ER.LOCK_DEADLOCK, +) - -def _get_error_info(data): - errno = struct.unpack('= 2 and value[0] == value[-1] == quote: + return value[1:-1] + return value + + def optionxform(self, key): + return key.lower().replace("_", "-") + + def get(self, section, option): + value = configparser.RawConfigParser.get(self, section, option) + return self.__remove_quotes(value) diff --git a/pymysql/protocol.py b/pymysql/protocol.py new file mode 100644 index 000000000..98fde6d0c --- /dev/null +++ b/pymysql/protocol.py @@ -0,0 +1,356 @@ +# Python implementation of low level MySQL client-server protocol +# http://dev.mysql.com/doc/internals/en/client-server-protocol.html + +from .charset import MBLENGTH +from .constants import FIELD_TYPE, SERVER_STATUS +from . import err + +import struct +import sys + + +DEBUG = False + +NULL_COLUMN = 251 +UNSIGNED_CHAR_COLUMN = 251 +UNSIGNED_SHORT_COLUMN = 252 +UNSIGNED_INT24_COLUMN = 253 +UNSIGNED_INT64_COLUMN = 254 + + +def dump_packet(data): # pragma: no cover + def printable(data): + if 32 <= data < 127: + return chr(data) + return "." + + try: + print("packet length:", len(data)) + for i in range(1, 7): + f = sys._getframe(i) + print("call[%d]: %s (line %d)" % (i, f.f_code.co_name, f.f_lineno)) + print("-" * 66) + except ValueError: + pass + dump_data = [data[i : i + 16] for i in range(0, min(len(data), 256), 16)] + for d in dump_data: + print( + " ".join(f"{x:02X}" for x in d) + + " " * (16 - len(d)) + + " " * 2 + + "".join(printable(x) for x in d) + ) + print("-" * 66) + print() + + +class MysqlPacket: + """Representation of a MySQL response packet. + + Provides an interface for reading/parsing the packet results. + """ + + __slots__ = ("_position", "_data") + + def __init__(self, data, encoding): + self._position = 0 + self._data = data + + def get_all_data(self): + return self._data + + def read(self, size): + """Read the first 'size' bytes in packet and advance cursor past them.""" + result = self._data[self._position : (self._position + size)] + if len(result) != size: + error = ( + "Result length not requested length:\n" + f"Expected={size}. Actual={len(result)}. Position: {self._position}. Data Length: {len(self._data)}" + ) + if DEBUG: + print(error) + self.dump() + raise AssertionError(error) + self._position += size + return result + + def read_all(self): + """Read all remaining data in the packet. + + (Subsequent read() will return errors.) + """ + result = self._data[self._position :] + self._position = None # ensure no subsequent read() + return result + + def advance(self, length): + """Advance the cursor in data buffer 'length' bytes.""" + new_position = self._position + length + if new_position < 0 or new_position > len(self._data): + raise Exception( + f"Invalid advance amount ({length}) for cursor. Position={new_position}" + ) + self._position = new_position + + def rewind(self, 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 + + def get_bytes(self, position, 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)] + + def read_uint8(self): + result = self._data[self._position] + self._position += 1 + return result + + def read_uint16(self): + result = struct.unpack_from("= 7 + + def 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 + + def is_auth_switch_request(self): + # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest + return self._data[0] == 0xFE + + def is_extra_auth_data(self): + # https://dev.mysql.com/doc/internals/en/successful-authentication.html + return self._data[0] == 1 + + def is_resultset_packet(self): + field_count = self._data[0] + return 1 <= field_count <= 250 + + def is_load_local_packet(self): + return self._data[0] == 0xFB + + def is_error_packet(self): + return self._data[0] == 0xFF + + def check_error(self): + if self.is_error_packet(): + self.raise_for_error() + + def raise_for_error(self): + self.rewind() + self.advance(1) # field_count == error (we already know that) + errno = self.read_uint16() + if DEBUG: + print("errno =", errno) + err.raise_mysql_exception(self._data) + + def dump(self): + dump_packet(self._data) + + +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. + """ + + def __init__(self, data, encoding): + MysqlPacket.__init__(self, data, encoding) + self._parse_field_descriptor(encoding) + + def _parse_field_descriptor(self, 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("= version_tuple + + def get_mysql_vendor(self, conn): + server_version = conn.get_server_info() + + if "MariaDB" in server_version: + return "mariadb" + + return "mysql" + + _connections = None + + @property + def connections(self): + if self._connections is None: + self._connections = [] + for params in self.databases: + self._connections.append(pymysql.connect(**params)) + self.addCleanup(self._teardown_connections) + return self._connections + + def connect(self, **params): + p = self.databases[0].copy() + p.update(params) + conn = pymysql.connect(**p) + + @self.addCleanup + def teardown(): + if conn.open: + conn.close() + + return conn + + def _teardown_connections(self): + if self._connections: + for connection in self._connections: + if connection.open: + connection.close() + self._connections = None + + def safe_create_table(self, connection, tablename, ddl, cleanup=True): + """create a table. - def setUp(self): - self.connections = [] + Ensures any existing version of that table is first dropped. - for params in self.databases: - self.connections.append(pymysql.connect(**params)) + Also adds a cleanup rule to drop the table after the test + completes. + """ + cursor = connection.cursor() - def tearDown(self): - for connection in self.connections: - connection.close() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cursor.execute(f"drop table if exists `{tablename}`") + cursor.execute(ddl) + cursor.close() + if cleanup: + self.addCleanup(self.drop_table, connection, tablename) + def drop_table(self, connection, tablename): + cursor = connection.cursor() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cursor.execute(f"drop table if exists `{tablename}`") + cursor.close() diff --git a/pymysql/tests/data/load_local_data.txt b/pymysql/tests/data/load_local_data.txt new file mode 100644 index 000000000..f9f99c8fa --- /dev/null +++ b/pymysql/tests/data/load_local_data.txt @@ -0,0 +1,22749 @@ +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, diff --git a/pymysql/tests/data/load_local_warn_data.txt b/pymysql/tests/data/load_local_warn_data.txt new file mode 100644 index 000000000..2bda5a96f --- /dev/null +++ b/pymysql/tests/data/load_local_warn_data.txt @@ -0,0 +1,50 @@ +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, diff --git a/pymysql/tests/test_DictCursor.py b/pymysql/tests/test_DictCursor.py index 0166214a3..4e545792a 100644 --- a/pymysql/tests/test_DictCursor.py +++ b/pymysql/tests/test_DictCursor.py @@ -2,55 +2,126 @@ import pymysql.cursors import datetime +import warnings + class TestDictCursor(base.PyMySQLTestCase): + bob = {"name": "bob", "age": 21, "DOB": datetime.datetime(1990, 2, 6, 23, 4, 56)} + jim = {"name": "jim", "age": 56, "DOB": datetime.datetime(1955, 5, 9, 13, 12, 45)} + fred = {"name": "fred", "age": 100, "DOB": datetime.datetime(1911, 9, 12, 1, 1, 1)} + + cursor_type = pymysql.cursors.DictCursor + + def setUp(self): + super().setUp() + self.conn = conn = self.connect() + c = conn.cursor(self.cursor_type) + + # create a table and some data to query + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists dictcursor") + # include in filterwarnings since for unbuffered dict cursor warning for lack of table + # will only be propagated at start of next execute() call + c.execute( + """CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""" + ) + data = [ + ("bob", 21, "1990-02-06 23:04:56"), + ("jim", 56, "1955-05-09 13:12:45"), + ("fred", 100, "1911-09-12 01:01:01"), + ] + c.executemany("insert into dictcursor values (%s,%s,%s)", data) + + def tearDown(self): + c = self.conn.cursor() + c.execute("drop table dictcursor") + super().tearDown() + + def _ensure_cursor_expired(self, cursor): + pass def test_DictCursor(self): - #all assert test compare to the structure as would come out from MySQLdb - conn = self.connections[0] - c = conn.cursor(pymysql.cursors.DictCursor) - # create a table ane some data to query - c.execute("""CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""") - data = (("bob",21,"1990-02-06 23:04:56"), - ("jim",56,"1955-05-09 13:12:45"), - ("fred",100,"1911-09-12 01:01:01")) - bob = {'name':'bob','age':21,'DOB':datetime.datetime(1990, 02, 6, 23, 04, 56)} - jim = {'name':'jim','age':56,'DOB':datetime.datetime(1955, 05, 9, 13, 12, 45)} - fred = {'name':'fred','age':100,'DOB':datetime.datetime(1911, 9, 12, 1, 1, 1)} - try: - c.executemany("insert into dictcursor values (%s,%s,%s)", data) - # try an update which should return no rows - c.execute("update dictcursor set age=20 where name='bob'") - bob['age'] = 20 - # pull back the single row dict for bob and check - c.execute("SELECT * from dictcursor where name='bob'") - r = c.fetchone() - self.assertEqual(bob,r,"fetchone via DictCursor failed") - # same again, but via fetchall => tuple) - c.execute("SELECT * from dictcursor where name='bob'") - r = c.fetchall() - self.assertEqual((bob,),r,"fetch a 1 row result via fetchall failed via DictCursor") - # same test again but iterate over the - c.execute("SELECT * from dictcursor where name='bob'") - for r in c: - self.assertEqual(bob, r,"fetch a 1 row result via iteration failed via DictCursor") - # get all 3 row via fetchall - c.execute("SELECT * from dictcursor") - r = c.fetchall() - self.assertEqual((bob,jim,fred), r, "fetchall failed via DictCursor") - #same test again but do a list comprehension - c.execute("SELECT * from dictcursor") - r = [x for x in c] - self.assertEqual([bob,jim,fred], r, "list comprehension failed via DictCursor") - # get all 2 row via fetchmany - c.execute("SELECT * from dictcursor") - r = c.fetchmany(2) - self.assertEqual((bob,jim), r, "fetchmany failed via DictCursor") - finally: - c.execute("drop table dictcursor") - -__all__ = ["TestDictCursor"] + bob, jim, fred = self.bob.copy(), self.jim.copy(), self.fred.copy() + # all assert test compare to the structure as would come out from MySQLdb + conn = self.conn + c = conn.cursor(self.cursor_type) + + # try an update which should return no rows + c.execute("update dictcursor set age=20 where name='bob'") + bob["age"] = 20 + # pull back the single row dict for bob and check + c.execute("SELECT * from dictcursor where name='bob'") + r = c.fetchone() + self.assertEqual(bob, r, "fetchone via DictCursor failed") + self._ensure_cursor_expired(c) + + # same again, but via fetchall => tuple) + c.execute("SELECT * from dictcursor where name='bob'") + r = c.fetchall() + self.assertEqual( + [bob], r, "fetch a 1 row result via fetchall failed via DictCursor" + ) + # same test again but iterate over the + c.execute("SELECT * from dictcursor where name='bob'") + for r in c: + self.assertEqual( + bob, r, "fetch a 1 row result via iteration failed via DictCursor" + ) + # get all 3 row via fetchall + c.execute("SELECT * from dictcursor") + r = c.fetchall() + self.assertEqual([bob, jim, fred], r, "fetchall failed via DictCursor") + # same test again but do a list comprehension + c.execute("SELECT * from dictcursor") + r = list(c) + self.assertEqual([bob, jim, fred], r, "DictCursor should be iterable") + # get all 2 row via fetchmany + c.execute("SELECT * from dictcursor") + r = c.fetchmany(2) + self.assertEqual([bob, jim], r, "fetchmany failed via DictCursor") + self._ensure_cursor_expired(c) + + def test_custom_dict(self): + class MyDict(dict): + pass + + class MyDictCursor(self.cursor_type): + dict_type = MyDict + + keys = ["name", "age", "DOB"] + bob = MyDict([(k, self.bob[k]) for k in keys]) + jim = MyDict([(k, self.jim[k]) for k in keys]) + fred = MyDict([(k, self.fred[k]) for k in keys]) + + cur = self.conn.cursor(MyDictCursor) + cur.execute("SELECT * FROM dictcursor WHERE name='bob'") + r = cur.fetchone() + self.assertEqual(bob, r, "fetchone() returns MyDictCursor") + self._ensure_cursor_expired(cur) + + cur.execute("SELECT * FROM dictcursor") + r = cur.fetchall() + self.assertEqual([bob, jim, fred], r, "fetchall failed via MyDictCursor") + + cur.execute("SELECT * FROM dictcursor") + r = list(cur) + self.assertEqual([bob, jim, fred], r, "list failed via MyDictCursor") + + cur.execute("SELECT * FROM dictcursor") + r = cur.fetchmany(2) + self.assertEqual([bob, jim], r, "list failed via MyDictCursor") + self._ensure_cursor_expired(cur) + + +class TestSSDictCursor(TestDictCursor): + cursor_type = pymysql.cursors.SSDictCursor + + def _ensure_cursor_expired(self, cursor): + list(cursor.fetchall_unbuffered()) + if __name__ == "__main__": import unittest + unittest.main() diff --git a/pymysql/tests/test_SSCursor.py b/pymysql/tests/test_SSCursor.py index a0e3caf34..d5e6e2bce 100644 --- a/pymysql/tests/test_SSCursor.py +++ b/pymysql/tests/test_SSCursor.py @@ -1,100 +1,236 @@ -import sys +import pytest + +from pymysql.tests import base +import pymysql.cursors +from pymysql.constants import CLIENT, ER -try: - from pymysql.tests import base - import pymysql.cursors -except: - # For local testing from top-level directory, without installing - sys.path.append('../pymysql') - from pymysql.tests import base - import pymysql.cursors class TestSSCursor(base.PyMySQLTestCase): def test_SSCursor(self): affected_rows = 18446744073709551615 - - conn = self.connections[0] + + conn = self.connect(client_flag=CLIENT.MULTI_STATEMENTS) data = [ - ('America', '', 'America/Jamaica'), - ('America', '', 'America/Los_Angeles'), - ('America', '', 'America/Lima'), - ('America', '', 'America/New_York'), - ('America', '', 'America/Menominee'), - ('America', '', 'America/Havana'), - ('America', '', 'America/El_Salvador'), - ('America', '', 'America/Costa_Rica'), - ('America', '', 'America/Denver'), - ('America', '', 'America/Detroit'),] - - try: - cursor = conn.cursor(pymysql.cursors.SSCursor) - - # Create table - cursor.execute(('CREATE TABLE tz_data (' - 'region VARCHAR(64),' - 'zone VARCHAR(64),' - 'name VARCHAR(64))')) - - # Test INSERT - for i in data: - cursor.execute('INSERT INTO tz_data VALUES (%s, %s, %s)', i) - self.assertEqual(conn.affected_rows(), 1, 'affected_rows does not match') - conn.commit() - - # Test fetchone() - iter = 0 - cursor.execute('SELECT * FROM tz_data') - while True: - row = cursor.fetchone() - if row is None: - break - iter += 1 - - # Test cursor.rowcount - self.assertEqual(cursor.rowcount, affected_rows, - 'cursor.rowcount != %s' % (str(affected_rows))) - - # Test cursor.rownumber - self.assertEqual(cursor.rownumber, iter, - 'cursor.rowcount != %s' % (str(iter))) - - # Test row came out the same as it went in - self.assertEqual((row in data), True, - 'Row not found in source data') - - # Test fetchall - cursor.execute('SELECT * FROM tz_data') - self.assertEqual(len(cursor.fetchall()), len(data), - 'fetchall failed. Number of rows does not match') - - # Test fetchmany - cursor.execute('SELECT * FROM tz_data') - self.assertEqual(len(cursor.fetchmany(2)), 2, - 'fetchmany failed. Number of rows does not match') - - # So MySQLdb won't throw "Commands out of sync" - while True: - res = cursor.fetchone() - if res is None: - break - - # Test update, affected_rows() - cursor.execute('UPDATE tz_data SET zone = %s', ['Foo']) + ("America", "", "America/Jamaica"), + ("America", "", "America/Los_Angeles"), + ("America", "", "America/Lima"), + ("America", "", "America/New_York"), + ("America", "", "America/Menominee"), + ("America", "", "America/Havana"), + ("America", "", "America/El_Salvador"), + ("America", "", "America/Costa_Rica"), + ("America", "", "America/Denver"), + ("America", "", "America/Detroit"), + ] + + cursor = conn.cursor(pymysql.cursors.SSCursor) + + # Create table + cursor.execute( + "CREATE TABLE tz_data (region VARCHAR(64), zone VARCHAR(64), name VARCHAR(64))" + ) + + conn.begin() + # Test INSERT + for i in data: + cursor.execute("INSERT INTO tz_data VALUES (%s, %s, %s)", i) + self.assertEqual(conn.affected_rows(), 1, "affected_rows does not match") + conn.commit() + + # Test fetchone() + iter = 0 + cursor.execute("SELECT * FROM tz_data") + while True: + row = cursor.fetchone() + if row is None: + break + iter += 1 + + # Test cursor.rowcount + self.assertEqual( + cursor.rowcount, + affected_rows, + "cursor.rowcount != %s" % (str(affected_rows)), + ) + + # Test cursor.rownumber + self.assertEqual( + cursor.rownumber, iter, "cursor.rowcount != %s" % (str(iter)) + ) + + # Test row came out the same as it went in + self.assertEqual((row in data), True, "Row not found in source data") + + # Test fetchall + cursor.execute("SELECT * FROM tz_data") + self.assertEqual( + len(cursor.fetchall()), + len(data), + "fetchall failed. Number of rows does not match", + ) + + # Test fetchmany + cursor.execute("SELECT * FROM tz_data") + self.assertEqual( + len(cursor.fetchmany(2)), + 2, + "fetchmany failed. Number of rows does not match", + ) + + # So MySQLdb won't throw "Commands out of sync" + while True: + res = cursor.fetchone() + if res is None: + break + + # Test update, affected_rows() + cursor.execute("UPDATE tz_data SET zone = %s", ["Foo"]) + conn.commit() + self.assertEqual( + cursor.rowcount, + len(data), + "Update failed. affected_rows != %s" % (str(len(data))), + ) + + # Test executemany + cursor.executemany("INSERT INTO tz_data VALUES (%s, %s, %s)", data) + self.assertEqual( + cursor.rowcount, + len(data), + "executemany failed. cursor.rowcount != %s" % (str(len(data))), + ) + + # Test multiple datasets + cursor.execute("SELECT 1; SELECT 2; SELECT 3") + self.assertListEqual(list(cursor), [(1,)]) + self.assertTrue(cursor.nextset()) + self.assertListEqual(list(cursor), [(2,)]) + self.assertTrue(cursor.nextset()) + self.assertListEqual(list(cursor), [(3,)]) + self.assertFalse(cursor.nextset()) + + cursor.execute("DROP TABLE IF EXISTS tz_data") + cursor.close() + + def test_execution_time_limit(self): + # this method is similarly implemented in test_cursor + + conn = self.connect() + + # table creation and filling is SSCursor only as it's not provided by self.setUp() + self.safe_create_table( + conn, + "test", + "create table test (data varchar(10))", + ) + with conn.cursor() as cur: + cur.execute( + "insert into test (data) values " + "('row1'), ('row2'), ('row3'), ('row4'), ('row5')" + ) conn.commit() - self.assertEqual(cursor.rowcount, len(data), - 'Update failed. affected_rows != %s' % (str(len(data)))) - - # Test executemany - cursor.executemany('INSERT INTO tz_data VALUES (%s, %s, %s)', data) - self.assertEqual(cursor.rowcount, len(data), - 'executemany failed. cursor.rowcount != %s' % (str(len(data)))) - - finally: - cursor.execute('DROP TABLE tz_data') - cursor.close() + + db_type = self.get_mysql_vendor(conn) + + with conn.cursor(pymysql.cursors.SSCursor) as cur: + # MySQL MAX_EXECUTION_TIME takes ms + # MariaDB max_statement_time takes seconds as int/float, introduced in 10.1 + + # this will sleep 0.01 seconds per row + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + + cur.execute(sql) + # unlike Cursor, SSCursor returns a list of tuples here + self.assertEqual( + cur.fetchall(), + [ + ("row1", 0), + ("row2", 0), + ("row3", 0), + ("row4", 0), + ("row5", 0), + ], + ) + + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + cur.execute(sql) + self.assertEqual(cur.fetchone(), ("row1", 0)) + + # this discards the previous unfinished query and raises an + # incomplete unbuffered query warning + with pytest.warns(UserWarning): + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + # SSCursor will not read the EOF packet until we try to read + # another row. Skipping this will raise an incomplete unbuffered + # query warning in the next cur.execute(). + self.assertEqual(cur.fetchone(), None) + + if db_type == "mysql": + sql = "SELECT /*+ MAX_EXECUTION_TIME(1) */ data, sleep(1) FROM test" + else: + sql = "SET STATEMENT max_statement_time=0.001 FOR SELECT data, sleep(1) FROM test" + with pytest.raises(pymysql.err.OperationalError) as cm: + # in an unbuffered cursor the OperationalError may not show up + # until fetching the entire result + cur.execute(sql) + cur.fetchall() + + if db_type == "mysql": + # this constant was only introduced in MySQL 5.7, not sure + # what was returned before, may have been ER_QUERY_INTERRUPTED + self.assertEqual(cm.value.args[0], ER.QUERY_TIMEOUT) + else: + self.assertEqual(cm.value.args[0], ER.STATEMENT_TIMEOUT) + + # connection should still be fine at this point + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + def test_warnings(self): + con = self.connect() + cur = con.cursor(pymysql.cursors.SSCursor) + cur.execute("DROP TABLE IF EXISTS `no_exists_table`") + self.assertEqual(cur.warning_count, 1) + + cur.execute("SHOW WARNINGS") + w = cur.fetchone() + self.assertEqual(w[1], ER.BAD_TABLE_ERROR) + self.assertIn( + "no_exists_table", + w[2], + ) + + # ensure unbuffered result is finished + self.assertIsNone(cur.fetchone()) + + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + self.assertIsNone(cur.fetchone()) + + self.assertEqual(cur.warning_count, 0) + + cur.execute("SELECT CAST('abc' AS SIGNED)") + # this ensures fully retrieving the unbuffered result + rows = cur.fetchmany(2) + self.assertEqual(len(rows), 1) + self.assertEqual(cur.warning_count, 1) + __all__ = ["TestSSCursor"] if __name__ == "__main__": import unittest + unittest.main() diff --git a/pymysql/tests/test_basic.py b/pymysql/tests/test_basic.py index fb7a30f8e..0fe13b59d 100644 --- a/pymysql/tests/test_basic.py +++ b/pymysql/tests/test_basic.py @@ -1,62 +1,117 @@ +import datetime +import json +import time + +import pytest + +import pymysql.cursors from pymysql.tests import base -from pymysql import util -import time -import datetime + +__all__ = ["TestConversion", "TestCursor", "TestBulkInserts"] + class TestConversion(base.PyMySQLTestCase): def test_datatypes(self): - """ test every data type """ - conn = self.connections[0] + """test every data type""" + conn = self.connect() c = conn.cursor() - c.execute("create table test_datatypes (b bit, i int, l bigint, f real, s varchar(32), u varchar(32), bb blob, d date, dt datetime, ts timestamp, td time, t time, st datetime)") + c.execute( + """ +create table test_datatypes ( + b bit, + i int, + l bigint, + f real, + s varchar(32), + u varchar(32), + bb blob, + d date, + dt datetime, + ts timestamp, + td time, + t time, + st datetime) +""" + ) try: # insert values - v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime.now(), datetime.timedelta(5,6), datetime.time(16,32), time.localtime()) - c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", v) + + v = ( + True, + -3, + 123456789012, + 5.7, + "hello'\" world", + "Espa\xc3\xb1ol", + "binary\x00data".encode(conn.encoding), + datetime.date(1988, 2, 2), + datetime.datetime(2014, 5, 15, 7, 45, 57), + datetime.timedelta(5, 6), + datetime.time(16, 32), + time.localtime(), + ) + c.execute( + "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values" + " (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + v, + ) c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes") r = c.fetchone() - self.assertEqual(util.int2byte(1), r[0]) - self.assertEqual(v[1:8], r[1:8]) - # mysql throws away microseconds so we need to check datetimes - # specially. additionally times are turned into timedeltas. - self.assertEqual(datetime.datetime(*v[8].timetuple()[:6]), r[8]) - self.assertEqual(v[9], r[9]) # just timedeltas - self.assertEqual(datetime.timedelta(0, 60 * (v[10].hour * 60 + v[10].minute)), r[10]) + self.assertEqual(b"\x01", r[0]) + self.assertEqual(v[1:10], r[1:10]) + self.assertEqual( + datetime.timedelta(0, 60 * (v[10].hour * 60 + v[10].minute)), r[10] + ) self.assertEqual(datetime.datetime(*v[-1][:6]), r[-1]) c.execute("delete from test_datatypes") # check nulls - c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", [None] * 12) + c.execute( + "insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st)" + " values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + [None] * 12, + ) c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes") r = c.fetchone() self.assertEqual(tuple([None] * 12), r) c.execute("delete from test_datatypes") - # check sequence type - c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)") - c.execute("select l from test_datatypes where i in %s order by i", ((2,6),)) - r = c.fetchall() - self.assertEqual(((4,),(8,)), r) + # check sequences type + for seq_type in (tuple, list, set, frozenset): + c.execute( + "insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)" + ) + seq = seq_type([2, 6]) + c.execute( + "select l from test_datatypes where i in %s order by i", (seq,) + ) + r = c.fetchall() + self.assertEqual(((4,), (8,)), r) + c.execute("delete from test_datatypes") + finally: c.execute("drop table test_datatypes") def test_dict(self): - """ test dict escaping """ - conn = self.connections[0] + """test dict escaping""" + conn = self.connect() c = conn.cursor() c.execute("create table test_dict (a integer, b integer, c integer)") try: - c.execute("insert into test_dict (a,b,c) values (%(a)s, %(b)s, %(c)s)", {"a":1,"b":2,"c":3}) + c.execute( + "insert into test_dict (a,b,c) values (%(a)s, %(b)s, %(c)s)", + {"a": 1, "b": 2, "c": 3}, + ) c.execute("select a,b,c from test_dict") - self.assertEqual((1,2,3), c.fetchone()) + self.assertEqual((1, 2, 3), c.fetchone()) finally: c.execute("drop table test_dict") def test_string(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() c.execute("create table test_dict (a text)") test_value = "I am a test string" @@ -68,7 +123,7 @@ def test_string(self): c.execute("drop table test_dict") def test_integer(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() c.execute("create table test_dict (a integer)") test_value = 12345 @@ -79,38 +134,73 @@ def test_integer(self): finally: c.execute("drop table test_dict") + def test_binary(self): + """test binary data""" + data = bytes(bytearray(range(255))) + conn = self.connect() + self.safe_create_table( + conn, "test_binary", "create table test_binary (b binary(255))" + ) + + with conn.cursor() as c: + c.execute("insert into test_binary (b) values (_binary %s)", (data,)) + c.execute("select b from test_binary") + self.assertEqual(data, c.fetchone()[0]) + + def test_blob(self): + """test blob data""" + data = bytes(bytearray(range(256)) * 4) + conn = self.connect() + self.safe_create_table(conn, "test_blob", "create table test_blob (b blob)") + + with conn.cursor() as c: + c.execute("insert into test_blob (b) values (_binary %s)", (data,)) + c.execute("select b from test_blob") + self.assertEqual(data, c.fetchone()[0]) - def test_big_blob(self): - """ test tons of data """ - conn = self.connections[0] - c = conn.cursor() - c.execute("create table test_big_blob (b blob)") - try: - data = "pymysql" * 1024 - c.execute("insert into test_big_blob (b) values (%s)", (data,)) - c.execute("select b from test_big_blob") - self.assertEqual(data.encode(conn.charset), c.fetchone()[0]) - finally: - c.execute("drop table test_big_blob") - def test_untyped(self): - """ test conversion of null, empty string """ - conn = self.connections[0] + """test conversion of null, empty string""" + conn = self.connect() c = conn.cursor() c.execute("select null,''") - self.assertEqual((None,u''), c.fetchone()) + self.assertEqual((None, ""), c.fetchone()) c.execute("select '',null") - self.assertEqual((u'',None), c.fetchone()) - - def test_datetime(self): - """ test conversion of null, empty string """ - conn = self.connections[0] + self.assertEqual(("", None), c.fetchone()) + + def test_timedelta(self): + """test timedelta conversion""" + conn = self.connect() + c = conn.cursor() + c.execute( + "select time('12:30'), time('23:12:59'), time('23:12:59.05100')," + + " time('-12:30'), time('-23:12:59'), time('-23:12:59.05100'), time('-00:30')" + ) + self.assertEqual( + ( + datetime.timedelta(0, 45000), + datetime.timedelta(0, 83579), + datetime.timedelta(0, 83579, 51000), + -datetime.timedelta(0, 45000), + -datetime.timedelta(0, 83579), + -datetime.timedelta(0, 83579, 51000), + -datetime.timedelta(0, 1800), + ), + c.fetchone(), + ) + + def test_datetime_microseconds(self): + """test datetime conversion w microseconds""" + + conn = self.connect() c = conn.cursor() - c.execute("select time('12:30'), time('23:12:59'), time('23:12:59.05100')") - self.assertEqual((datetime.timedelta(0, 45000), - datetime.timedelta(0, 83579), - datetime.timedelta(0, 83579, 51000)), - c.fetchone()) + dt = datetime.datetime(2013, 11, 12, 9, 9, 9, 123450) + c.execute("create table test_datetime (id int, ts datetime(6))") + try: + c.execute("insert into test_datetime values (%s, %s)", (1, dt)) + c.execute("select ts from test_datetime") + self.assertEqual((dt,), c.fetchone()) + finally: + c.execute("drop table test_datetime") class TestCursor(base.PyMySQLTestCase): @@ -119,7 +209,7 @@ class TestCursor(base.PyMySQLTestCase): # compatible with the DB-API 2.0 spec and has not broken # any unit tests for anything we've tried. - #def test_description(self): + # def test_description(self): # """ test description attribute """ # # result is from MySQLdb module # r = (('Host', 254, 11, 60, 60, 0, 0), @@ -161,15 +251,15 @@ class TestCursor(base.PyMySQLTestCase): # ('max_updates', 3, 1, 11, 11, 0, 0), # ('max_connections', 3, 1, 11, 11, 0, 0), # ('max_user_connections', 3, 1, 11, 11, 0, 0)) - # conn = self.connections[0] + # conn = self.connect() # c = conn.cursor() # c.execute("select * from mysql.user") # # self.assertEqual(r, c.description) def test_fetch_no_result(self): - """ test a fetchone() with no rows """ - conn = self.connections[0] + """test a fetchone() with no rows""" + conn = self.connect() c = conn.cursor() c.execute("create table test_nr (b varchar(32))") try: @@ -180,34 +270,188 @@ def test_fetch_no_result(self): c.execute("drop table test_nr") def test_aggregates(self): - """ test aggregate functions """ - conn = self.connections[0] + """test aggregate functions""" + conn = self.connect() c = conn.cursor() try: - c.execute('create table test_aggregates (i integer)') - for i in xrange(0, 10): - c.execute('insert into test_aggregates (i) values (%s)', (i,)) - c.execute('select sum(i) from test_aggregates') - r, = c.fetchone() - self.assertEqual(sum(range(0,10)), r) + c.execute("create table test_aggregates (i integer)") + for i in range(0, 10): + c.execute("insert into test_aggregates (i) values (%s)", (i,)) + c.execute("select sum(i) from test_aggregates") + (r,) = c.fetchone() + self.assertEqual(sum(range(0, 10)), r) finally: - c.execute('drop table test_aggregates') + c.execute("drop table test_aggregates") def test_single_tuple(self): - """ test a single tuple """ - conn = self.connections[0] + """test a single tuple""" + conn = self.connect() c = conn.cursor() - try: - c.execute("create table mystuff (id integer primary key)") - c.execute("insert into mystuff (id) values (1)") - c.execute("insert into mystuff (id) values (2)") - c.execute("select id from mystuff where id in %s", ((1,),)) - self.assertEqual([(1,)], list(c.fetchall())) - finally: - c.execute("drop table mystuff") + self.safe_create_table( + conn, "mystuff", "create table mystuff (id integer primary key)" + ) + c.execute("insert into mystuff (id) values (1)") + c.execute("insert into mystuff (id) values (2)") + c.execute("select id from mystuff where id in %s", ((1,),)) + self.assertEqual([(1,)], list(c.fetchall())) + c.close() + + def test_json(self): + args = self.databases[0].copy() + args["charset"] = "utf8mb4" + conn = pymysql.connect(**args) + # MariaDB only has limited JSON support, stores data as longtext + # https://mariadb.com/kb/en/json-data-type/ + if not self.mysql_server_is(conn, (5, 7, 0)): + pytest.skip("JSON type is only supported on MySQL >= 5.7") + + self.safe_create_table( + conn, + "test_json", + """\ +create table test_json ( + id int not null, + json JSON not null, + primary key (id) +);""", + ) + cur = conn.cursor() + + json_str = '{"hello": "こんにちは"}' + cur.execute("INSERT INTO test_json (id, `json`) values (42, %s)", (json_str,)) + cur.execute("SELECT `json` from `test_json` WHERE `id`=42") + res = cur.fetchone()[0] + self.assertEqual(json.loads(res), json.loads(json_str)) + + if self.get_mysql_vendor(conn) == "mysql": + cur.execute("SELECT CAST(%s AS JSON) AS x", (json_str,)) + res = cur.fetchone()[0] + self.assertEqual(json.loads(res), json.loads(json_str)) + + +class TestBulkInserts(base.PyMySQLTestCase): + cursor_type = pymysql.cursors.DictCursor + + def setUp(self): + super().setUp() + self.conn = conn = self.connect() + + # create a table and some data to query + self.safe_create_table( + conn, + "bulkinsert", + """\ +CREATE TABLE bulkinsert +( +id int, +name char(20), +age int, +height int, +PRIMARY KEY (id) +) +""", + ) + + def _verify_records(self, data): + conn = self.connect() + cursor = conn.cursor() + cursor.execute("SELECT id, name, age, height from bulkinsert") + result = cursor.fetchall() + self.assertEqual(sorted(data), sorted(result)) + + def test_bulk_insert(self): + conn = self.connect() + cursor = conn.cursor() + + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany( + "insert into bulkinsert (id, name, age, height) values (%s,%s,%s,%s)", + data, + ) + self.assertEqual( + cursor._executed, + bytearray( + b"insert into bulkinsert (id, name, age, height) values " + b"(0,'bob',21,123),(1,'jim',56,45),(2,'fred',100,180)" + ), + ) + cursor.execute("commit") + self._verify_records(data) + + def test_bulk_insert_multiline_statement(self): + conn = self.connect() + cursor = conn.cursor() + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany( + """insert +into bulkinsert (id, name, +age, height) +values (%s, +%s , %s, +%s ) + """, + data, + ) + self.assertEqual( + cursor._executed.strip(), + bytearray( + b"""insert +into bulkinsert (id, name, +age, height) +values (0, +'bob' , 21, +123 ),(1, +'jim' , 56, +45 ),(2, +'fred' , 100, +180 )""" + ), + ) + cursor.execute("commit") + self._verify_records(data) -__all__ = ["TestConversion","TestCursor"] + def test_bulk_insert_single_record(self): + conn = self.connect() + cursor = conn.cursor() + data = [(0, "bob", 21, 123)] + cursor.executemany( + "insert into bulkinsert (id, name, age, height) values (%s,%s,%s,%s)", + data, + ) + cursor.execute("commit") + self._verify_records(data) -if __name__ == "__main__": - import unittest - unittest.main() + def test_issue_288(self): + """executemany should work with "insert ... on update""" + conn = self.connect() + cursor = conn.cursor() + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany( + """insert +into bulkinsert (id, name, +age, height) +values (%s, +%s , %s, +%s ) on duplicate key update +age = values(age) + """, + data, + ) + self.assertEqual( + cursor._executed.strip(), + bytearray( + b"""insert +into bulkinsert (id, name, +age, height) +values (0, +'bob' , 21, +123 ),(1, +'jim' , 56, +45 ),(2, +'fred' , 100, +180 ) on duplicate key update +age = values(age)""" + ), + ) + cursor.execute("commit") + self._verify_records(data) diff --git a/pymysql/tests/test_charset.py b/pymysql/tests/test_charset.py new file mode 100644 index 000000000..1dbe6fffa --- /dev/null +++ b/pymysql/tests/test_charset.py @@ -0,0 +1,44 @@ +import pymysql.charset + + +def test_utf8(): + utf8mb3 = pymysql.charset.charset_by_name("utf8mb3") + assert utf8mb3.name == "utf8mb3" + assert utf8mb3.collation == "utf8mb3_general_ci" + assert ( + repr(utf8mb3) + == "Charset(id=33, name='utf8mb3', collation='utf8mb3_general_ci')" + ) + + # MySQL 8.0 changed the default collation for utf8mb4. + # But we use old default for compatibility. + utf8mb4 = pymysql.charset.charset_by_name("utf8mb4") + assert utf8mb4.name == "utf8mb4" + assert utf8mb4.collation == "utf8mb4_general_ci" + assert ( + repr(utf8mb4) + == "Charset(id=45, name='utf8mb4', collation='utf8mb4_general_ci')" + ) + + # utf8 is alias of utf8mb4 since MySQL 8.0, and PyMySQL v1.1. + lowercase_utf8 = pymysql.charset.charset_by_name("utf8") + assert lowercase_utf8 == utf8mb4 + + # Regardless of case, UTF8 (which is special cased) should resolve to the same thing + uppercase_utf8 = pymysql.charset.charset_by_name("UTF8") + mixedcase_utf8 = pymysql.charset.charset_by_name("UtF8") + assert uppercase_utf8 == lowercase_utf8 + assert mixedcase_utf8 == lowercase_utf8 + + +def test_case_sensitivity(): + lowercase_latin1 = pymysql.charset.charset_by_name("latin1") + assert lowercase_latin1 is not None + + # lowercase and uppercase should resolve to the same charset + uppercase_latin1 = pymysql.charset.charset_by_name("LATIN1") + assert uppercase_latin1 == lowercase_latin1 + + # lowercase and mixed case should resolve to the same charset + mixedcase_latin1 = pymysql.charset.charset_by_name("LaTiN1") + assert mixedcase_latin1 == lowercase_latin1 diff --git a/pymysql/tests/test_connection.py b/pymysql/tests/test_connection.py new file mode 100644 index 000000000..1a16c982a --- /dev/null +++ b/pymysql/tests/test_connection.py @@ -0,0 +1,934 @@ +import datetime +import ssl +import pytest +import time +from unittest import mock + +import pymysql +from pymysql.tests import base +from pymysql.constants import CLIENT + + +class TempUser: + def __init__(self, c, user, db, auth=None, authdata=None, password=None): + self._c = c + self._user = user + self._db = db + create = "CREATE USER " + user + if password is not None: + create += " IDENTIFIED BY '%s'" % password + elif auth is not None: + create += " IDENTIFIED WITH %s" % auth + if authdata is not None: + create += " AS '%s'" % authdata + try: + c.execute(create) + self._created = True + except pymysql.err.InternalError: + # already exists - TODO need to check the same plugin applies + self._created = False + try: + c.execute(f"GRANT SELECT ON {db}.* TO {user}") + self._grant = True + except pymysql.err.InternalError: + self._grant = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self._grant: + self._c.execute(f"REVOKE SELECT ON {self._db}.* FROM {self._user}") + if self._created: + self._c.execute("DROP USER %s" % self._user) + + +class TestAuthentication(base.PyMySQLTestCase): + socket_auth = False + socket_found = False + two_questions_found = False + three_attempts_found = False + pam_found = False + mysql_old_password_found = False + sha256_password_found = False + ed25519_found = False + + import os + + osuser = os.environ.get("USER") + + # socket auth requires the current user and for the connection to be a socket + # rest do grants @localhost due to incomplete logic - TODO change to @% then + db = base.PyMySQLTestCase.databases[0].copy() + + socket_auth = db.get("unix_socket") is not None and db.get("host") in ( + "localhost", + "127.0.0.1", + ) + + cur = pymysql.connect(**db).cursor() + del db["user"] + cur.execute("SHOW PLUGINS") + for r in cur: + if (r[1], r[2]) != ("ACTIVE", "AUTHENTICATION"): + continue + if r[3] == "auth_socket.so" or r[0] == "unix_socket": + socket_plugin_name = r[0] + socket_found = True + elif r[3] == "dialog_examples.so": + if r[0] == "two_questions": + two_questions_found = True + elif r[0] == "three_attempts": + three_attempts_found = True + elif r[0] == "pam": + pam_found = True + pam_plugin_name = r[3].split(".")[0] + if pam_plugin_name == "auth_pam": + pam_plugin_name = "pam" + # MySQL: authentication_pam + # https://dev.mysql.com/doc/refman/5.5/en/pam-authentication-plugin.html + + # MariaDB: pam + # https://mariadb.com/kb/en/mariadb/pam-authentication-plugin/ + + # Names differ but functionality is close + elif r[0] == "mysql_old_password": + mysql_old_password_found = True + elif r[0] == "sha256_password": + sha256_password_found = True + elif r[0] == "ed25519": + ed25519_found = True + # else: + # print("plugin: %r" % r[0]) + + def test_plugin(self): + conn = self.connect() + cur = conn.cursor() + cur.execute( + "select plugin from mysql.user where concat(user, '@', host)=current_user()" + ) + for r in cur: + self.assertIn(conn._auth_plugin_name, (r[0], "mysql_native_password")) + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(socket_found, reason="socket plugin already installed") + def testSocketAuthInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connect().cursor() + try: + cur.execute("install plugin auth_socket soname 'auth_socket.so'") + TestAuthentication.socket_found = True + self.socket_plugin_name = "auth_socket" + self.realtestSocketAuth() + except pymysql.err.InternalError: + try: + cur.execute("install soname 'auth_socket'") + TestAuthentication.socket_found = True + self.socket_plugin_name = "unix_socket" + self.realtestSocketAuth() + except pymysql.err.InternalError: + TestAuthentication.socket_found = False + pytest.skip("we couldn't install the socket plugin") + finally: + if TestAuthentication.socket_found: + cur.execute("uninstall plugin %s" % self.socket_plugin_name) + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(not socket_found, reason="no socket plugin") + def testSocketAuth(self): + self.realtestSocketAuth() + + def realtestSocketAuth(self): + with TempUser( + self.connect().cursor(), + TestAuthentication.osuser + "@localhost", + self.databases[0]["database"], + self.socket_plugin_name, + ): + pymysql.connect(user=TestAuthentication.osuser, **self.db) + + class Dialog: + fail = False + + def __init__(self, con): + self.fail = TestAuthentication.Dialog.fail + pass + + def prompt(self, echo, prompt): + if self.fail: + self.fail = False + return b"bad guess at a password" + return self.m.get(prompt) + + class DialogHandler: + def __init__(self, con): + self.con = con + + def authenticate(self, pkt): + while True: + flag = pkt.read_uint8() + # echo = (flag & 0x06) == 0x02 + last = (flag & 0x01) == 0x01 + prompt = pkt.read_all() + + if prompt == b"Password, please:": + self.con.write_packet(b"stillnotverysecret\0") + else: + self.con.write_packet(b"no idea what to do with this prompt\0") + pkt = self.con._read_packet() + pkt.check_error() + if pkt.is_ok_packet() or last: + break + return pkt + + class DefectiveHandler: + def __init__(self, con): + self.con = con + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif( + two_questions_found, reason="two_questions plugin already installed" + ) + def testDialogAuthTwoQuestionsInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connect().cursor() + try: + cur.execute("install plugin two_questions soname 'dialog_examples.so'") + TestAuthentication.two_questions_found = True + self.realTestDialogAuthTwoQuestions() + except pymysql.err.InternalError: + pytest.skip("we couldn't install the two_questions plugin") + finally: + if TestAuthentication.two_questions_found: + cur.execute("uninstall plugin two_questions") + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(not two_questions_found, reason="no two questions auth plugin") + def testDialogAuthTwoQuestions(self): + self.realTestDialogAuthTwoQuestions() + + def realTestDialogAuthTwoQuestions(self): + TestAuthentication.Dialog.fail = False + TestAuthentication.Dialog.m = { + b"Password, please:": b"notverysecret", + b"Are you sure ?": b"yes, of course", + } + with TempUser( + self.connect().cursor(), + "pymysql_2q@localhost", + self.databases[0]["database"], + "two_questions", + "notverysecret", + ): + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user="pymysql_2q", **self.db) + pymysql.connect( + user="pymysql_2q", + auth_plugin_map={b"dialog": TestAuthentication.Dialog}, + **self.db, + ) + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif( + three_attempts_found, reason="three_attempts plugin already installed" + ) + def testDialogAuthThreeAttemptsQuestionsInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connect().cursor() + try: + cur.execute("install plugin three_attempts soname 'dialog_examples.so'") + TestAuthentication.three_attempts_found = True + self.realTestDialogAuthThreeAttempts() + except pymysql.err.InternalError: + pytest.skip("we couldn't install the three_attempts plugin") + finally: + if TestAuthentication.three_attempts_found: + cur.execute("uninstall plugin three_attempts") + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(not three_attempts_found, reason="no three attempts plugin") + def testDialogAuthThreeAttempts(self): + self.realTestDialogAuthThreeAttempts() + + def realTestDialogAuthThreeAttempts(self): + TestAuthentication.Dialog.m = {b"Password, please:": b"stillnotverysecret"} + TestAuthentication.Dialog.fail = ( + True # fail just once. We've got three attempts after all + ) + with TempUser( + self.connect().cursor(), + "pymysql_3a@localhost", + self.databases[0]["database"], + "three_attempts", + "stillnotverysecret", + ): + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"dialog": TestAuthentication.Dialog}, + **self.db, + ) + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"dialog": TestAuthentication.DialogHandler}, + **self.db, + ) + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user="pymysql_3a", auth_plugin_map={b"dialog": object}, **self.db + ) + + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"dialog": TestAuthentication.DefectiveHandler}, + **self.db, + ) + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"notdialogplugin": TestAuthentication.Dialog}, + **self.db, + ) + TestAuthentication.Dialog.m = {b"Password, please:": b"I do not know"} + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"dialog": TestAuthentication.Dialog}, + **self.db, + ) + TestAuthentication.Dialog.m = {b"Password, please:": None} + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user="pymysql_3a", + auth_plugin_map={b"dialog": TestAuthentication.Dialog}, + **self.db, + ) + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(pam_found, reason="pam plugin already installed") + @pytest.mark.skipif( + os.environ.get("PASSWORD") is None, reason="PASSWORD env var required" + ) + @pytest.mark.skipif( + os.environ.get("PAMSERVICE") is None, reason="PAMSERVICE env var required" + ) + def testPamAuthInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connect().cursor() + try: + cur.execute("install plugin pam soname 'auth_pam.so'") + TestAuthentication.pam_found = True + self.realTestPamAuth() + except pymysql.err.InternalError: + pytest.skip("we couldn't install the auth_pam plugin") + finally: + if TestAuthentication.pam_found: + cur.execute("uninstall plugin pam") + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif(not pam_found, reason="no pam plugin") + @pytest.mark.skipif( + os.environ.get("PASSWORD") is None, reason="PASSWORD env var required" + ) + @pytest.mark.skipif( + os.environ.get("PAMSERVICE") is None, reason="PAMSERVICE env var required" + ) + def testPamAuth(self): + self.realTestPamAuth() + + def realTestPamAuth(self): + db = self.db.copy() + import os + + db["password"] = os.environ.get("PASSWORD") + cur = self.connect().cursor() + try: + cur.execute("show grants for " + TestAuthentication.osuser + "@localhost") + grants = cur.fetchone()[0] + cur.execute("drop user " + TestAuthentication.osuser + "@localhost") + except pymysql.OperationalError as e: + # assuming the user doesn't exist which is ok too + self.assertEqual(1045, e.args[0]) + grants = None + with TempUser( + cur, + TestAuthentication.osuser + "@localhost", + self.databases[0]["database"], + "pam", + os.environ.get("PAMSERVICE"), + ): + try: + pymysql.connect(user=TestAuthentication.osuser, **db) + db["password"] = "very bad guess at password" + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user=TestAuthentication.osuser, + auth_plugin_map={ + b"mysql_cleartext_password": TestAuthentication.DefectiveHandler + }, + **self.db, + ) + except pymysql.OperationalError as e: + self.assertEqual(1045, e.args[0]) + # we had 'bad guess at password' work with pam. Well at least we get + # a permission denied here + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect( + user=TestAuthentication.osuser, + auth_plugin_map={ + b"mysql_cleartext_password": TestAuthentication.DefectiveHandler + }, + **self.db, + ) + if grants: + # recreate the user + cur.execute(grants) + + @pytest.mark.skipif(not socket_auth, reason="connection to unix_socket required") + @pytest.mark.skipif( + not sha256_password_found, + reason="no sha256 password authentication plugin found", + ) + def testAuthSHA256(self): + conn = self.connect() + c = conn.cursor() + with TempUser( + c, + "pymysql_sha256@localhost", + self.databases[0]["database"], + "sha256_password", + ): + c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' ='Sh@256Pa33'") + c.execute("FLUSH PRIVILEGES") + db = self.db.copy() + db["password"] = "Sh@256Pa33" + # Although SHA256 is supported, need the configuration of public key of + # the mysql server. Currently will get error by this test. + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user="pymysql_sha256", **db) + + @pytest.mark.skipif(not ed25519_found, reason="no ed25519 authention plugin") + def testAuthEd25519(self): + db = self.db.copy() + del db["password"] + conn = self.connect() + c = conn.cursor() + c.execute("select ed25519_password(''), ed25519_password('ed25519_password')") + for r in c: + empty_pass = r[0].decode("ascii") + non_empty_pass = r[1].decode("ascii") + + with TempUser( + c, + "pymysql_ed25519", + self.databases[0]["database"], + "ed25519", + empty_pass, + ): + pymysql.connect(user="pymysql_ed25519", password="", **db) + + with TempUser( + c, + "pymysql_ed25519", + self.databases[0]["database"], + "ed25519", + non_empty_pass, + ): + pymysql.connect(user="pymysql_ed25519", password="ed25519_password", **db) + + +class TestConnection(base.PyMySQLTestCase): + def test_utf8mb4(self): + """This test requires MySQL >= 5.5""" + arg = self.databases[0].copy() + arg["charset"] = "utf8mb4" + pymysql.connect(**arg) + + def test_set_character_set(self): + con = self.connect() + cur = con.cursor() + + con.set_character_set("latin1") + cur.execute("SELECT @@character_set_connection") + self.assertEqual(cur.fetchone(), ("latin1",)) + self.assertEqual(con.encoding, "cp1252") + + con.set_character_set("utf8mb4", "utf8mb4_general_ci") + cur.execute("SELECT @@character_set_connection, @@collation_connection") + self.assertEqual(cur.fetchone(), ("utf8mb4", "utf8mb4_general_ci")) + self.assertEqual(con.encoding, "utf8") + + def test_largedata(self): + """Large query and response (>=16MB)""" + cur = self.connect().cursor() + cur.execute("SELECT @@max_allowed_packet") + if cur.fetchone()[0] < 16 * 1024 * 1024 + 10: + print("Set max_allowed_packet to bigger than 17MB") + return + t = "a" * (16 * 1024 * 1024) + cur.execute("SELECT '" + t + "'") + assert cur.fetchone()[0] == t + + def test_autocommit(self): + con = self.connect() + self.assertFalse(con.get_autocommit()) + + cur = con.cursor() + cur.execute("SET AUTOCOMMIT=1") + self.assertTrue(con.get_autocommit()) + + con.autocommit(False) + self.assertFalse(con.get_autocommit()) + cur.execute("SELECT @@AUTOCOMMIT") + self.assertEqual(cur.fetchone()[0], 0) + + def test_select_db(self): + con = self.connect() + current_db = self.databases[0]["database"] + other_db = self.databases[1]["database"] + + cur = con.cursor() + cur.execute("SELECT database()") + self.assertEqual(cur.fetchone()[0], current_db) + + con.select_db(other_db) + cur.execute("SELECT database()") + self.assertEqual(cur.fetchone()[0], other_db) + + def test_connection_gone_away(self): + """ + http://dev.mysql.com/doc/refman/5.0/en/gone-away.html + http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html#error_cr_server_gone_error + """ + con = self.connect() + cur = con.cursor() + cur.execute("SET wait_timeout=1") + time.sleep(2) + with self.assertRaises(pymysql.OperationalError) as cm: + cur.execute("SELECT 1+1") + # error occurs while reading, not writing because of socket buffer. + # self.assertEqual(cm.exception.args[0], 2006) + self.assertIn(cm.exception.args[0], (2006, 2013)) + + def test_init_command(self): + conn = self.connect( + init_command='SELECT "bar"; SELECT "baz"', + client_flag=CLIENT.MULTI_STATEMENTS, + ) + c = conn.cursor() + c.execute('select "foobar";') + self.assertEqual(("foobar",), c.fetchone()) + conn.close() + with self.assertRaises(pymysql.err.Error): + conn.ping(reconnect=False) + + def test_read_default_group(self): + conn = self.connect( + read_default_group="client", + ) + self.assertTrue(conn.open) + + def test_set_charset(self): + c = self.connect() + c.set_charset("utf8mb4") + # TODO validate setting here + + def test_defer_connect(self): + import socket + + d = self.databases[0].copy() + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(d["unix_socket"]) + except KeyError: + sock.close() + sock = socket.create_connection( + (d.get("host", "localhost"), d.get("port", 3306)) + ) + for k in ["unix_socket", "host", "port"]: + try: + del d[k] + except KeyError: + pass + + c = pymysql.connect(defer_connect=True, **d) + self.assertFalse(c.open) + c.connect(sock) + c.close() + sock.close() + + def test_ssl_connect(self): + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl={ + "ca": "ca", + "cert": "cert", + "key": "key", + "cipher": "cipher", + }, + defer_connect=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_called_with("cipher") + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl={ + "ca": "ca", + "cert": "cert", + "key": "key", + }, + defer_connect=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl={ + "ca": "ca", + "cert": "cert", + "key": "key", + "password": "password", + }, + defer_connect=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password="password", + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_ca="ca", + defer_connect=True, + ) + assert create_default_context.called + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_not_called + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_ca="ca", + ssl_cert="cert", + ssl_key="key", + defer_connect=True, + ) + assert create_default_context.called + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + for ssl_verify_cert in (True, "1", "yes", "true"): + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_cert="cert", + ssl_key="key", + ssl_verify_cert=ssl_verify_cert, + defer_connect=True, + ) + assert create_default_context.called + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_REQUIRED + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + for ssl_verify_cert in (None, False, "0", "no", "false"): + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_cert="cert", + ssl_key="key", + ssl_verify_cert=ssl_verify_cert, + defer_connect=True, + ) + assert create_default_context.called + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + for ssl_ca in ("ca", None): + for ssl_verify_cert in ("foo", "bar", ""): + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_ca=ssl_ca, + ssl_cert="cert", + ssl_key="key", + ssl_verify_cert=ssl_verify_cert, + defer_connect=True, + ) + assert create_default_context.called + assert not dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ( + ssl.CERT_REQUIRED if ssl_ca is not None else ssl.CERT_NONE + ), (ssl_ca, ssl_verify_cert) + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_ca="ca", + ssl_cert="cert", + ssl_key="key", + ssl_verify_identity=True, + defer_connect=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password=None, + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_ca="ca", + ssl_cert="cert", + ssl_key="key", + ssl_key_password="password", + ssl_verify_identity=True, + defer_connect=True, + ) + assert create_default_context.called + assert dummy_ssl_context.check_hostname + assert dummy_ssl_context.verify_mode == ssl.CERT_NONE + dummy_ssl_context.load_cert_chain.assert_called_with( + "cert", + keyfile="key", + password="password", + ) + dummy_ssl_context.set_ciphers.assert_not_called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_disabled=True, + ssl={ + "ca": "ca", + "cert": "cert", + "key": "key", + }, + defer_connect=True, + ) + assert not create_default_context.called + + dummy_ssl_context = mock.Mock(options=0, verify_flags=0) + with mock.patch( + "pymysql.connections.ssl.create_default_context", + new=mock.Mock(return_value=dummy_ssl_context), + ) as create_default_context: + pymysql.connect( + ssl_disabled=True, + ssl_ca="ca", + ssl_cert="cert", + ssl_key="key", + defer_connect=True, + ) + assert not create_default_context.called + + +# A custom type and function to escape it +class Foo: + value = "bar" + + +def escape_foo(x, d): + return x.value + + +class TestEscape(base.PyMySQLTestCase): + def test_escape_string(self): + con = self.connect() + cur = con.cursor() + + self.assertEqual(con.escape("foo'bar"), "'foo\\'bar'") + # added NO_AUTO_CREATE_USER as not including it in 5.7 generates warnings + # mysql-8.0 removes the option however + if self.mysql_server_is(con, (8, 0, 0)): + cur.execute("SET sql_mode='NO_BACKSLASH_ESCAPES'") + else: + cur.execute("SET sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'") + self.assertEqual(con.escape("foo'bar"), "'foo''bar'") + + def test_escape_builtin_encoders(self): + con = self.connect() + + val = datetime.datetime(2012, 3, 4, 5, 6) + self.assertEqual(con.escape(val, con.encoders), "'2012-03-04 05:06:00'") + + def test_escape_custom_object(self): + con = self.connect() + + mapping = {Foo: escape_foo} + self.assertEqual(con.escape(Foo(), mapping), "bar") + + def test_escape_fallback_encoder(self): + con = self.connect() + + class Custom(str): + pass + + mapping = {str: pymysql.converters.escape_string} + self.assertEqual(con.escape(Custom("foobar"), mapping), "'foobar'") + + def test_escape_no_default(self): + con = self.connect() + + self.assertRaises(TypeError, con.escape, 42, {}) + + def test_escape_dict_raise_typeerror(self): + """con.escape(dict) should raise TypeError""" + con = self.connect() + + mapping = con.encoders.copy() + mapping[Foo] = escape_foo + # self.assertEqual(con.escape({"foo": Foo()}, mapping), {"foo": "bar"}) + with self.assertRaises(TypeError): + con.escape({"foo": Foo()}) + + def test_escape_list_item(self): + con = self.connect() + + mapping = con.encoders.copy() + mapping[Foo] = escape_foo + self.assertEqual(con.escape([Foo()], mapping), "(bar)") + + def test_previous_cursor_not_closed(self): + con = self.connect( + init_command='SELECT "bar"; SELECT "baz"', + client_flag=CLIENT.MULTI_STATEMENTS, + ) + cur1 = con.cursor() + cur1.execute("SELECT 1; SELECT 2") + cur2 = con.cursor() + cur2.execute("SELECT 3") + self.assertEqual(cur2.fetchone()[0], 3) + + def test_commit_during_multi_result(self): + con = self.connect(client_flag=CLIENT.MULTI_STATEMENTS) + cur = con.cursor() + cur.execute("SELECT 1; SELECT 2") + con.commit() + cur.execute("SELECT 3") + self.assertEqual(cur.fetchone()[0], 3) + + def test_force_close_closes_socketio(self): + con = self.connect() + sock = con._sock + fileno = sock.fileno() + rfile = con._rfile + + con._force_close() + assert rfile.closed + assert sock._closed + assert sock.fileno() != fileno # should be set to -1 + + def test_socket_closed_on_exception_in_connect(self): + con = self.connect(defer_connect=True) + sock = None + rfile = None + fileno = -1 + + def _request_authentication(): + nonlocal sock, rfile, fileno + sock = con._sock + assert sock is not None + fileno = sock.fileno() + rfile = con._rfile + assert rfile is not None + raise TypeError + + con._request_authentication = _request_authentication + + with pytest.raises(TypeError): + con.connect() + assert not con.open + assert con._rfile is None + assert con._sock is None + assert rfile.closed + assert sock._closed + assert sock.fileno() != fileno # should be set to -1 diff --git a/pymysql/tests/test_converters.py b/pymysql/tests/test_converters.py new file mode 100644 index 000000000..b36ee4b39 --- /dev/null +++ b/pymysql/tests/test_converters.py @@ -0,0 +1,54 @@ +import datetime +from unittest import TestCase +from pymysql import converters + + +__all__ = ["TestConverter"] + + +class TestConverter(TestCase): + def test_escape_string(self): + self.assertEqual(converters.escape_string("foo\nbar"), "foo\\nbar") + + def test_convert_datetime(self): + expected = datetime.datetime(2007, 2, 24, 23, 6, 20) + dt = converters.convert_datetime("2007-02-24 23:06:20") + self.assertEqual(dt, expected) + + def test_convert_datetime_with_fsp(self): + expected = datetime.datetime(2007, 2, 24, 23, 6, 20, 511581) + dt = converters.convert_datetime("2007-02-24 23:06:20.511581") + self.assertEqual(dt, expected) + + def _test_convert_timedelta(self, with_negate=False, with_fsp=False): + d = {"hours": 789, "minutes": 12, "seconds": 34} + s = "%(hours)s:%(minutes)s:%(seconds)s" % d + if with_fsp: + d["microseconds"] = 511581 + s += ".%(microseconds)s" % d + + expected = datetime.timedelta(**d) + if with_negate: + expected = -expected + s = "-" + s + + tdelta = converters.convert_timedelta(s) + self.assertEqual(tdelta, expected) + + def test_convert_timedelta(self): + self._test_convert_timedelta(with_negate=False, with_fsp=False) + self._test_convert_timedelta(with_negate=True, with_fsp=False) + + def test_convert_timedelta_with_fsp(self): + self._test_convert_timedelta(with_negate=False, with_fsp=True) + self._test_convert_timedelta(with_negate=False, with_fsp=True) + + def test_convert_time(self): + expected = datetime.time(23, 6, 20) + time_obj = converters.convert_time("23:06:20") + self.assertEqual(time_obj, expected) + + def test_convert_time_with_fsp(self): + expected = datetime.time(23, 6, 20, 511581) + time_obj = converters.convert_time("23:06:20.511581") + self.assertEqual(time_obj, expected) diff --git a/pymysql/tests/test_cursor.py b/pymysql/tests/test_cursor.py new file mode 100644 index 000000000..2e267fb6a --- /dev/null +++ b/pymysql/tests/test_cursor.py @@ -0,0 +1,222 @@ +from pymysql.constants import ER +from pymysql.tests import base +import pymysql.cursors + +import pytest + + +class CursorTest(base.PyMySQLTestCase): + def setUp(self): + super().setUp() + + conn = self.connect() + self.safe_create_table( + conn, + "test", + "create table test (data varchar(10))", + ) + cursor = conn.cursor() + cursor.execute( + "insert into test (data) values ('row1'), ('row2'), ('row3'), ('row4'), ('row5')" + ) + conn.commit() + cursor.close() + self.test_connection = pymysql.connect(**self.databases[0]) + self.addCleanup(self.test_connection.close) + + def test_cursor_is_iterator(self): + """Test that the cursor is an iterator""" + conn = self.test_connection + cursor = conn.cursor() + cursor.execute("select * from test") + self.assertEqual(cursor.__iter__(), cursor) + self.assertEqual(cursor.__next__(), ("row1",)) + + def test_cleanup_rows_unbuffered(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.SSCursor) + + cursor.execute("select * from test as t1, test as t2") + for counter, row in enumerate(cursor): + if counter > 10: + break + + del cursor + + c2 = conn.cursor() + + c2.execute("select 1") + self.assertEqual(c2.fetchone(), (1,)) + self.assertIsNone(c2.fetchone()) + + def test_cleanup_rows_buffered(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.Cursor) + + cursor.execute("select * from test as t1, test as t2") + for counter, row in enumerate(cursor): + if counter > 10: + break + + del cursor + + c2 = conn.cursor() + c2.execute("select 1") + + self.assertEqual(c2.fetchone(), (1,)) + self.assertIsNone(c2.fetchone()) + + def test_executemany(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.Cursor) + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%s, %s)" + ) + self.assertIsNotNone(m, "error parse %s") + self.assertEqual(m.group(3), "", "group 3 not blank, bug in RE_INSERT_VALUES?") + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)" + ) + self.assertIsNotNone(m, "error parse %(name)s") + self.assertEqual(m.group(3), "", "group 3 not blank, bug in RE_INSERT_VALUES?") + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)" + ) + self.assertIsNotNone(m, "error parse %(id_name)s") + self.assertEqual(m.group(3), "", "group 3 not blank, bug in RE_INSERT_VALUES?") + + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update" + ) + self.assertIsNotNone(m, "error parse %(id_name)s") + self.assertEqual( + m.group(3), + " ON duplicate update", + "group 3 not ON duplicate update, bug in RE_INSERT_VALUES?", + ) + + # https://github.com/PyMySQL/PyMySQL/pull/597 + m = pymysql.cursors.RE_INSERT_VALUES.match( + "INSERT INTO bloup(foo, bar)VALUES(%s, %s)" + ) + assert m is not None + + # cursor._executed must bee "insert into test (data) + # values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)" + # list args + data = range(10) + cursor.executemany("insert into test (data) values (%s)", data) + self.assertTrue( + cursor._executed.endswith(b",(7),(8),(9)"), + "execute many with %s not in one query", + ) + + # dict args + data_dict = [{"data": i} for i in range(10)] + cursor.executemany("insert into test (data) values (%(data)s)", data_dict) + self.assertTrue( + cursor._executed.endswith(b",(7),(8),(9)"), + "execute many with %(data)s not in one query", + ) + + # %% in column set + cursor.execute( + """\ + CREATE TABLE percent_test ( + `A%` INTEGER, + `B%` INTEGER)""" + ) + try: + q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)" + self.assertIsNotNone(pymysql.cursors.RE_INSERT_VALUES.match(q)) + cursor.executemany(q, [(3, 4), (5, 6)]) + self.assertTrue( + cursor._executed.endswith(b"(3, 4),(5, 6)"), + "executemany with %% not in one query", + ) + finally: + cursor.execute("DROP TABLE IF EXISTS percent_test") + + def test_execution_time_limit(self): + # this method is similarly implemented in test_SScursor + + conn = self.test_connection + db_type = self.get_mysql_vendor(conn) + + with conn.cursor(pymysql.cursors.Cursor) as cur: + # MySQL MAX_EXECUTION_TIME takes ms + # MariaDB max_statement_time takes seconds as int/float, introduced in 10.1 + + # this will sleep 0.01 seconds per row + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + + cur.execute(sql) + # unlike SSCursor, Cursor returns a tuple of tuples here + self.assertEqual( + cur.fetchall(), + ( + ("row1", 0), + ("row2", 0), + ("row3", 0), + ("row4", 0), + ("row5", 0), + ), + ) + + if db_type == "mysql": + sql = ( + "SELECT /*+ MAX_EXECUTION_TIME(2000) */ data, sleep(0.01) FROM test" + ) + else: + sql = "SET STATEMENT max_statement_time=2 FOR SELECT data, sleep(0.01) FROM test" + cur.execute(sql) + self.assertEqual(cur.fetchone(), ("row1", 0)) + + # this discards the previous unfinished query + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + if db_type == "mysql": + sql = "SELECT /*+ MAX_EXECUTION_TIME(1) */ data, sleep(1) FROM test" + else: + sql = "SET STATEMENT max_statement_time=0.001 FOR SELECT data, sleep(1) FROM test" + with pytest.raises(pymysql.err.OperationalError) as cm: + # in a buffered cursor this should reliably raise an + # OperationalError + cur.execute(sql) + + if db_type == "mysql": + # this constant was only introduced in MySQL 5.7, not sure + # what was returned before, may have been ER_QUERY_INTERRUPTED + self.assertEqual(cm.value.args[0], ER.QUERY_TIMEOUT) + else: + self.assertEqual(cm.value.args[0], ER.STATEMENT_TIMEOUT) + + # connection should still be fine at this point + cur.execute("SELECT 1") + self.assertEqual(cur.fetchone(), (1,)) + + def test_warnings(self): + con = self.connect() + cur = con.cursor() + cur.execute("DROP TABLE IF EXISTS `no_exists_table`") + self.assertEqual(cur.warning_count, 1) + + cur.execute("SHOW WARNINGS") + w = cur.fetchone() + self.assertEqual(w[1], ER.BAD_TABLE_ERROR) + self.assertIn( + "no_exists_table", + w[2], + ) + + cur.execute("SELECT 1") + self.assertEqual(cur.warning_count, 0) diff --git a/pymysql/tests/test_err.py b/pymysql/tests/test_err.py new file mode 100644 index 000000000..6eb0f987d --- /dev/null +++ b/pymysql/tests/test_err.py @@ -0,0 +1,16 @@ +import pytest +from pymysql import err + + +def test_raise_mysql_exception(): + data = b"\xff\x15\x04#28000Access denied" + with pytest.raises(err.OperationalError) as cm: + err.raise_mysql_exception(data) + assert cm.type == err.OperationalError + assert cm.value.args == (1045, "Access denied") + + data = b"\xff\x10\x04Too many connections" + with pytest.raises(err.OperationalError) as cm: + err.raise_mysql_exception(data) + assert cm.type == err.OperationalError + assert cm.value.args == (1040, "Too many connections") diff --git a/pymysql/tests/test_example.py b/pymysql/tests/test_example.py deleted file mode 100644 index 2da05db31..000000000 --- a/pymysql/tests/test_example.py +++ /dev/null @@ -1,32 +0,0 @@ -import pymysql -from pymysql.tests import base - -class TestExample(base.PyMySQLTestCase): - def test_example(self): - conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='', db='mysql') - - - cur = conn.cursor() - - cur.execute("SELECT Host,User FROM user") - - # print cur.description - - # r = cur.fetchall() - # print r - # ...or... - u = False - - for r in cur.fetchall(): - u = u or conn.user in r - - self.assertTrue(u) - - cur.close() - conn.close() - -__all__ = ["TestExample"] - -if __name__ == "__main__": - import unittest - unittest.main() diff --git a/pymysql/tests/test_issues.py b/pymysql/tests/test_issues.py index e1e3cf05a..f1fe8dd48 100644 --- a/pymysql/tests/test_issues.py +++ b/pymysql/tests/test_issues.py @@ -1,29 +1,29 @@ -import pymysql -from pymysql.tests import base -import unittest +import datetime +import time +import warnings -import sys +import pytest -try: - import imp - reload = imp.reload -except AttributeError: - pass +import pymysql +from pymysql.tests import base -import datetime +__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"] -# backwards compatibility: -if not hasattr(unittest, "skip"): - unittest.skip = lambda message: lambda f: f class TestOldIssues(base.PyMySQLTestCase): def test_issue_3(self): - """ undefined methods datetime_or_None, date_or_None """ - conn = self.connections[0] + """undefined methods datetime_or_None, date_or_None""" + conn = self.connect() c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue3") c.execute("create table issue3 (d date, t time, dt datetime, ts timestamp)") try: - c.execute("insert into issue3 (d, t, dt, ts) values (%s,%s,%s,%s)", (None, None, None, None)) + c.execute( + "insert into issue3 (d, t, dt, ts) values (%s,%s,%s,%s)", + (None, None, None, None), + ) c.execute("select d from issue3") self.assertEqual(None, c.fetchone()[0]) c.execute("select t from issue3") @@ -31,14 +31,21 @@ def test_issue_3(self): c.execute("select dt from issue3") self.assertEqual(None, c.fetchone()[0]) c.execute("select ts from issue3") - self.assertTrue(isinstance(c.fetchone()[0], datetime.datetime)) + self.assertIn( + type(c.fetchone()[0]), + (type(None), datetime.datetime), + "expected Python type None or datetime from SQL timestamp", + ) finally: c.execute("drop table issue3") def test_issue_4(self): - """ can't retrieve TIMESTAMP fields """ - conn = self.connections[0] + """can't retrieve TIMESTAMP fields""" + conn = self.connect() c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue4") c.execute("create table issue4 (ts timestamp)") try: c.execute("insert into issue4 (ts) values (now())") @@ -48,26 +55,34 @@ def test_issue_4(self): c.execute("drop table issue4") def test_issue_5(self): - """ query on information_schema.tables fails """ - con = self.connections[0] + """query on information_schema.tables fails""" + con = self.connect() cur = con.cursor() cur.execute("select * from information_schema.tables") def test_issue_6(self): - """ exception: TypeError: ord() expected a character, but string of length 0 found """ - conn = pymysql.connect(host="localhost",user="root",passwd="",db="mysql") + """exception: TypeError: ord() expected a character, but string of length 0 found""" + # ToDo: this test requires access to db 'mysql'. + kwargs = self.databases[0].copy() + kwargs["database"] = "mysql" + conn = pymysql.connect(**kwargs) c = conn.cursor() c.execute("select * from user") conn.close() def test_issue_8(self): - """ Primary Key and Index error when selecting data """ - conn = self.connections[0] + """Primary Key and Index error when selecting data""" + conn = self.connect() c = conn.cursor() - c.execute("""CREATE TABLE `test` (`station` int(10) NOT NULL DEFAULT '0', `dh` -datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `echeance` int(1) NOT NULL + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists test") + c.execute( + """CREATE TABLE `test` (`station` int NOT NULL DEFAULT '0', `dh` +datetime NOT NULL DEFAULT '2015-01-01 00:00:00', `echeance` int NOT NULL DEFAULT '0', `me` double DEFAULT NULL, `mo` double DEFAULT NULL, PRIMARY -KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") +KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""" + ) try: self.assertEqual(0, c.execute("SELECT * FROM test")) c.execute("ALTER TABLE `test` ADD INDEX `idx_station` (`station`)") @@ -75,29 +90,17 @@ def test_issue_8(self): finally: c.execute("drop table test") - def test_issue_9(self): - """ sets DeprecationWarning in Python 2.6 """ - try: - reload(pymysql) - except DeprecationWarning: - self.fail() - - def test_issue_10(self): - """ Allocate a variable to return when the exception handler is permissive """ - conn = self.connections[0] - conn.errorhandler = lambda cursor, errorclass, errorvalue: None - cur = conn.cursor() - cur.execute( "create table t( n int )" ) - cur.execute( "create table t( n int )" ) - def test_issue_13(self): - """ can't handle large result fields """ - conn = self.connections[0] + """can't handle large result fields""" + conn = self.connect() cur = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("drop table if exists issue13") try: cur.execute("create table issue13 (t text)") # ticket says 18k - size = 18*1024 + size = 18 * 1024 cur.execute("insert into issue13 (t) values (%s)", ("x" * size,)) cur.execute("select t from issue13") # use assertTrue so that obscenely huge error messages don't print @@ -106,52 +109,63 @@ def test_issue_13(self): finally: cur.execute("drop table issue13") - def test_issue_14(self): - """ typo in converters.py """ - self.assertEqual('1', pymysql.converters.escape_item(1, "utf8")) - self.assertEqual('1', pymysql.converters.escape_item(1L, "utf8")) - - self.assertEqual('1', pymysql.converters.escape_object(1)) - self.assertEqual('1', pymysql.converters.escape_object(1L)) - def test_issue_15(self): - """ query should be expanded before perform character encoding """ - conn = self.connections[0] + """query should be expanded before perform character encoding""" + conn = self.connect() c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue15") c.execute("create table issue15 (t varchar(32))") try: - c.execute("insert into issue15 (t) values (%s)", (u'\xe4\xf6\xfc',)) + c.execute("insert into issue15 (t) values (%s)", ("\xe4\xf6\xfc",)) c.execute("select t from issue15") - self.assertEqual(u'\xe4\xf6\xfc', c.fetchone()[0]) + self.assertEqual("\xe4\xf6\xfc", c.fetchone()[0]) finally: c.execute("drop table issue15") def test_issue_16(self): - """ Patch for string and tuple escaping """ - conn = self.connections[0] + """Patch for string and tuple escaping""" + conn = self.connect() c = conn.cursor() - c.execute("create table issue16 (name varchar(32) primary key, email varchar(32))") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue16") + c.execute( + "create table issue16 (name varchar(32) primary key, email varchar(32))" + ) try: - c.execute("insert into issue16 (name, email) values ('pete', 'floydophone')") + c.execute( + "insert into issue16 (name, email) values ('pete', 'floydophone')" + ) c.execute("select email from issue16 where name=%s", ("pete",)) self.assertEqual("floydophone", c.fetchone()[0]) finally: c.execute("drop table issue16") - @unittest.skip("test_issue_17() requires a custom, legacy MySQL configuration and will not be run.") + @pytest.mark.skip( + "test_issue_17() requires a custom, legacy MySQL configuration and will not be run." + ) def test_issue_17(self): - """ could not connect mysql use passwod """ - conn = self.connections[0] + """could not connect mysql use password""" + conn = self.connect() host = self.databases[0]["host"] - db = self.databases[0]["db"] + db = self.databases[0]["database"] c = conn.cursor() + # grant access to a table to a user with a password try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue17") c.execute("create table issue17 (x varchar(32) primary key)") c.execute("insert into issue17 (x) values ('hello, world!')") - c.execute("grant all privileges on %s.issue17 to 'issue17user'@'%%' identified by '1234'" % db) + c.execute( + "grant all privileges on %s.issue17 to 'issue17user'@'%%' identified by '1234'" + % db + ) conn.commit() - + conn2 = pymysql.connect(host=host, user="issue17user", passwd="1234", db=db) c2 = conn2.cursor() c2.execute("select x from issue17") @@ -159,74 +173,76 @@ def test_issue_17(self): finally: c.execute("drop table issue17") -def _uni(s, e): - # hack for py3 - if sys.version_info[0] > 2: - return unicode(bytes(s, sys.getdefaultencoding()), e) - else: - return unicode(s, e) class TestNewIssues(base.PyMySQLTestCase): def test_issue_34(self): try: pymysql.connect(host="localhost", port=1237, user="root") self.fail() - except pymysql.OperationalError, e: + except pymysql.OperationalError as e: self.assertEqual(2003, e.args[0]) - except: + except Exception: self.fail() def test_issue_33(self): - conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8") + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, "hei\xdfe", "create table hei\xdfe (name varchar(32))" + ) c = conn.cursor() - try: - c.execute(_uni("create table hei\xc3\x9fe (name varchar(32))", "utf8")) - c.execute(_uni("insert into hei\xc3\x9fe (name) values ('Pi\xc3\xb1ata')", "utf8")) - c.execute(_uni("select name from hei\xc3\x9fe", "utf8")) - self.assertEqual(_uni("Pi\xc3\xb1ata","utf8"), c.fetchone()[0]) - finally: - c.execute(_uni("drop table hei\xc3\x9fe", "utf8")) + c.execute("insert into hei\xdfe (name) values ('Pi\xdfata')") + c.execute("select name from hei\xdfe") + self.assertEqual("Pi\xdfata", c.fetchone()[0]) - @unittest.skip("This test requires manual intervention") + @pytest.mark.skip("This test requires manual intervention") def test_issue_35(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() - print "sudo killall -9 mysqld within the next 10 seconds" + print("sudo killall -9 mysqld within the next 10 seconds") try: c.execute("select sleep(10)") self.fail() - except pymysql.OperationalError, e: + except pymysql.OperationalError as e: self.assertEqual(2013, e.args[0]) def test_issue_36(self): - conn = self.connections[0] + # connection 0 is super user, connection 1 isn't + conn = self.connections[1] c = conn.cursor() - # kill connections[0] c.execute("show processlist") kill_id = None - for id,user,host,db,command,time,state,info in c.fetchall(): + for row in c.fetchall(): + id = row[0] + info = row[7] if info == "show processlist": kill_id = id break + self.assertEqual(kill_id, conn.thread_id()) # now nuke the connection - conn.kill(kill_id) + self.connections[0].kill(kill_id) # make sure this connection has broken try: c.execute("show tables") self.fail() - except: + except Exception: pass + c.close() + conn.close() + # check the process list from the other connection try: - c = self.connections[1].cursor() + # Wait since Travis-CI sometimes fail this test. + time.sleep(0.1) + + c = self.connections[0].cursor() c.execute("show processlist") ids = [row[0] for row in c.fetchall()] self.assertFalse(kill_id in ids) finally: - del self.connections[0] + del self.connections[1] def test_issue_37(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() self.assertEqual(1, c.execute("SELECT @foo")) self.assertEqual((None,), c.fetchone()) @@ -234,21 +250,27 @@ def test_issue_37(self): c.execute("set @foo = 'bar'") def test_issue_38(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() - datum = "a" * 1024 * 1023 # reduced size for most default mysql installs - + datum = "a" * 1024 * 1023 # reduced size for most default mysql installs + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue38") c.execute("create table issue38 (id integer, data mediumblob)") c.execute("insert into issue38 values (1, %s)", (datum,)) finally: c.execute("drop table issue38") def disabled_test_issue_54(self): - conn = self.connections[0] + conn = self.connect() c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue54") big_sql = "select * from issue54 where " - big_sql += " and ".join("%d=%d" % (i,i) for i in xrange(0, 100000)) + big_sql += " and ".join("%d=%d" % (i, i) for i in range(0, 100000)) try: c.execute("create table issue54 (id integer primary key)") @@ -258,21 +280,78 @@ def disabled_test_issue_54(self): finally: c.execute("drop table issue54") + class TestGitHubIssues(base.PyMySQLTestCase): def test_issue_66(self): - conn = self.connections[0] + """'Connection' object has no attribute 'insert_id'""" + conn = self.connect() c = conn.cursor() self.assertEqual(0, conn.insert_id()) try: - c.execute("create table issue66 (id integer primary key auto_increment, x integer)") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue66") + c.execute( + "create table issue66 (id integer primary key auto_increment, x integer)" + ) c.execute("insert into issue66 (x) values (1)") c.execute("insert into issue66 (x) values (1)") self.assertEqual(2, conn.insert_id()) finally: c.execute("drop table issue66") + def test_issue_79(self): + """Duplicate field overwrites the previous one in the result of DictCursor""" + conn = self.connect() + c = conn.cursor(pymysql.cursors.DictCursor) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists a") + c.execute("drop table if exists b") + c.execute("""CREATE TABLE a (id int, value int)""") + c.execute("""CREATE TABLE b (id int, value int)""") + + a = (1, 11) + b = (1, 22) + try: + c.execute("insert into a values (%s, %s)", a) + c.execute("insert into b values (%s, %s)", b) + + c.execute("SELECT * FROM a inner join b on a.id = b.id") + r = c.fetchall()[0] + self.assertEqual(r["id"], 1) + self.assertEqual(r["value"], 11) + self.assertEqual(r["b.value"], 22) + finally: + c.execute("drop table a") + c.execute("drop table b") + + def test_issue_95(self): + """Leftover trailing OK packet for "CALL my_sp" queries""" + conn = self.connect() + cur = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("DROP PROCEDURE IF EXISTS `foo`") + cur.execute( + """CREATE PROCEDURE `foo` () + BEGIN + SELECT 1; + END""" + ) + try: + cur.execute("""CALL foo()""") + cur.execute("""SELECT 1""") + self.assertEqual(cur.fetchone()[0], 1) + finally: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("DROP PROCEDURE IF EXISTS `foo`") + def test_issue_114(self): - conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8") + """autocommit is not set after reconnecting with ping()""" + conn = pymysql.connect(charset="utf8", **self.databases[0]) conn.autocommit(False) c = conn.cursor() c.execute("""select @@autocommit;""") @@ -284,7 +363,7 @@ def test_issue_114(self): conn.close() # Ensure autocommit() is still working - conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8") + conn = pymysql.connect(charset="utf8", **self.databases[0]) c = conn.cursor() c.execute("""select @@autocommit;""") self.assertFalse(c.fetchone()[0]) @@ -295,8 +374,125 @@ def test_issue_114(self): self.assertTrue(c.fetchone()[0]) conn.close() -__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"] + def test_issue_175(self): + """The number of fields returned by server is read in wrong way""" + conn = self.connect() + cur = conn.cursor() + for length in (200, 300): + columns = ", ".join(f"c{i} integer" for i in range(length)) + sql = f"create table test_field_count ({columns})" + try: + cur.execute(sql) + cur.execute("select * from test_field_count") + assert len(cur.description) == length + finally: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("drop table if exists test_field_count") + + def test_issue_321(self): + """Test iterable as query argument.""" + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, + "issue321", + "create table issue321 (value_1 varchar(1), value_2 varchar(1))", + ) + + sql_insert = "insert into issue321 (value_1, value_2) values (%s, %s)" + sql_dict_insert = ( + "insert into issue321 (value_1, value_2) values (%(value_1)s, %(value_2)s)" + ) + sql_select = "select * from issue321 where value_1 in %s and value_2=%s" + data = [ + [("a",), "\u0430"], + [["b"], "\u0430"], + {"value_1": [["c"]], "value_2": "\u0430"}, + ] + cur = conn.cursor() + self.assertEqual(cur.execute(sql_insert, data[0]), 1) + self.assertEqual(cur.execute(sql_insert, data[1]), 1) + self.assertEqual(cur.execute(sql_dict_insert, data[2]), 1) + self.assertEqual(cur.execute(sql_select, [("a", "b", "c"), "\u0430"]), 3) + self.assertEqual(cur.fetchone(), ("a", "\u0430")) + self.assertEqual(cur.fetchone(), ("b", "\u0430")) + self.assertEqual(cur.fetchone(), ("c", "\u0430")) + + def test_issue_364(self): + """Test mixed unicode/binary arguments in executemany.""" + conn = pymysql.connect(charset="utf8mb4", **self.databases[0]) + self.safe_create_table( + conn, + "issue364", + "create table issue364 (value_1 binary(3), value_2 varchar(3)) " + "engine=InnoDB default charset=utf8mb4", + ) + + sql = "insert into issue364 (value_1, value_2) values (_binary %s, %s)" + usql = "insert into issue364 (value_1, value_2) values (_binary %s, %s)" + values = [pymysql.Binary(b"\x00\xff\x00"), "\xe4\xf6\xfc"] + + # test single insert and select + cur = conn.cursor() + cur.execute(sql, args=values) + cur.execute("select * from issue364") + self.assertEqual(cur.fetchone(), tuple(values)) + + # test single insert unicode query + cur.execute(usql, args=values) + + # test multi insert and select + cur.executemany(sql, args=(values, values, values)) + cur.execute("select * from issue364") + for row in cur.fetchall(): + self.assertEqual(row, tuple(values)) + + # test multi insert with unicode query + cur.executemany(usql, args=(values, values, values)) + + def test_issue_363(self): + """Test binary / geometry types.""" + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, + "issue363", + "CREATE TABLE issue363 ( " + "id INTEGER PRIMARY KEY, geom LINESTRING NOT NULL /*!80003 SRID 0 */, " + "SPATIAL KEY geom (geom)) " + "ENGINE=MyISAM", + ) + + cur = conn.cursor() + query = ( + "INSERT INTO issue363 (id, geom) VALUES" + "(1998, ST_GeomFromText('LINESTRING(1.1 1.1,2.2 2.2)'))" + ) + cur.execute(query) + + # select WKT + query = "SELECT ST_AsText(geom) FROM issue363" + cur.execute(query) + row = cur.fetchone() + self.assertEqual(row, ("LINESTRING(1.1 1.1,2.2 2.2)",)) + + # select WKB + query = "SELECT ST_AsBinary(geom) FROM issue363" + cur.execute(query) + row = cur.fetchone() + self.assertEqual( + row, + ( + b"\x01\x02\x00\x00\x00\x02\x00\x00\x00" + b"\x9a\x99\x99\x99\x99\x99\xf1?" + b"\x9a\x99\x99\x99\x99\x99\xf1?" + b"\x9a\x99\x99\x99\x99\x99\x01@" + b"\x9a\x99\x99\x99\x99\x99\x01@", + ), + ) -if __name__ == "__main__": - import unittest - unittest.main() + # select internal binary + cur.execute("SELECT geom FROM issue363") + row = cur.fetchone() + # don't assert the exact internal binary value, as it could + # vary across implementations + self.assertTrue(isinstance(row[0], bytes)) diff --git a/pymysql/tests/test_load_local.py b/pymysql/tests/test_load_local.py new file mode 100644 index 000000000..509221420 --- /dev/null +++ b/pymysql/tests/test_load_local.py @@ -0,0 +1,104 @@ +from pymysql import cursors, OperationalError +from pymysql.constants import ER +from pymysql.tests import base + +import os + +__all__ = ["TestLoadLocal"] + + +class TestLoadLocal(base.PyMySQLTestCase): + def test_no_file(self): + """Test load local infile when the file does not exist""" + conn = self.connect() + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + try: + self.assertRaises( + OperationalError, + c.execute, + ( + "LOAD DATA LOCAL INFILE 'no_data.txt' INTO TABLE " + "test_load_local fields terminated by ','" + ), + ) + finally: + c.execute("DROP TABLE test_load_local") + c.close() + + def test_load_file(self): + """Test load local infile with a valid file""" + conn = self.connect() + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "data", "load_local_data.txt" + ) + try: + c.execute( + f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local" + + " FIELDS TERMINATED BY ','" + ) + c.execute("SELECT COUNT(*) FROM test_load_local") + self.assertEqual(22749, c.fetchone()[0]) + finally: + c.execute("DROP TABLE test_load_local") + + def test_unbuffered_load_file(self): + """Test unbuffered load local infile with a valid file""" + conn = self.connect() + c = conn.cursor(cursors.SSCursor) + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "data", "load_local_data.txt" + ) + try: + c.execute( + f"LOAD DATA LOCAL INFILE '{filename}' INTO TABLE test_load_local" + + " FIELDS TERMINATED BY ','" + ) + c.execute("SELECT COUNT(*) FROM test_load_local") + self.assertEqual(22749, c.fetchone()[0]) + finally: + c.close() + conn.close() + conn.connect() + c = conn.cursor() + c.execute("DROP TABLE test_load_local") + + def test_load_warnings(self): + """Test load local infile produces the appropriate warnings""" + conn = self.connect() + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "data", + "load_local_warn_data.txt", + ) + try: + c.execute( + ( + "LOAD DATA LOCAL INFILE '{0}' INTO TABLE " + + "test_load_local FIELDS TERMINATED BY ','" + ).format(filename) + ) + self.assertEqual(1, c.warning_count) + + c.execute("SHOW WARNINGS") + w = c.fetchone() + + self.assertEqual(ER.TRUNCATED_WRONG_VALUE_FOR_FIELD, w[1]) + self.assertIn( + "incorrect integer value", + w[2].lower(), + ) + finally: + c.execute("DROP TABLE test_load_local") + c.close() + + +if __name__ == "__main__": + import unittest + + unittest.main() diff --git a/pymysql/tests/test_nextset.py b/pymysql/tests/test_nextset.py new file mode 100644 index 000000000..a10f8d5b7 --- /dev/null +++ b/pymysql/tests/test_nextset.py @@ -0,0 +1,83 @@ +import pytest + +import pymysql +from pymysql.tests import base +from pymysql.constants import CLIENT + + +class TestNextset(base.PyMySQLTestCase): + def test_nextset(self): + con = self.connect( + init_command='SELECT "bar"; SELECT "baz"', + client_flag=CLIENT.MULTI_STATEMENTS, + ) + cur = con.cursor() + cur.execute("SELECT 1; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + + r = cur.nextset() + self.assertTrue(r) + + self.assertEqual([(2,)], list(cur)) + self.assertIsNone(cur.nextset()) + + def test_skip_nextset(self): + cur = self.connect(client_flag=CLIENT.MULTI_STATEMENTS).cursor() + cur.execute("SELECT 1; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + + cur.execute("SELECT 42") + self.assertEqual([(42,)], list(cur)) + + def test_nextset_error(self): + con = self.connect(client_flag=CLIENT.MULTI_STATEMENTS) + cur = con.cursor() + + for i in range(3): + cur.execute("SELECT %s; xyzzy;", (i,)) + self.assertEqual([(i,)], list(cur)) + with self.assertRaises(pymysql.ProgrammingError): + cur.nextset() + self.assertEqual([], cur.fetchall()) + + def test_ok_and_next(self): + cur = self.connect(client_flag=CLIENT.MULTI_STATEMENTS).cursor() + cur.execute("SELECT 1; commit; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + self.assertTrue(cur.nextset()) + self.assertTrue(cur.nextset()) + self.assertEqual([(2,)], list(cur)) + self.assertFalse(bool(cur.nextset())) + + @pytest.mark.xfail + def test_multi_cursor(self): + con = self.connect(client_flag=CLIENT.MULTI_STATEMENTS) + cur1 = con.cursor() + cur2 = con.cursor() + + cur1.execute("SELECT 1; SELECT 2;") + cur2.execute("SELECT 42") + + self.assertEqual([(1,)], list(cur1)) + self.assertEqual([(42,)], list(cur2)) + + r = cur1.nextset() + self.assertTrue(r) + + self.assertEqual([(2,)], list(cur1)) + self.assertIsNone(cur1.nextset()) + + def test_multi_statement_warnings(self): + con = self.connect( + init_command='SELECT "bar"; SELECT "baz"', + client_flag=CLIENT.MULTI_STATEMENTS, + ) + cursor = con.cursor() + + try: + cursor.execute("DROP TABLE IF EXISTS a; DROP TABLE IF EXISTS b;") + except TypeError: + self.fail() + + # TODO: How about SSCursor and nextset? + # It's very hard to implement correctly... diff --git a/pymysql/tests/test_optionfile.py b/pymysql/tests/test_optionfile.py new file mode 100644 index 000000000..d13553dda --- /dev/null +++ b/pymysql/tests/test_optionfile.py @@ -0,0 +1,24 @@ +from io import StringIO +from unittest import TestCase +from pymysql.optionfile import Parser + + +__all__ = ["TestParser"] + + +_cfg_file = r""" +[default] +string = foo +quoted = "bar" +single_quoted = 'foobar' +skip-slave-start +""" + + +class TestParser(TestCase): + def test_string(self): + parser = Parser() + parser.read_file(StringIO(_cfg_file)) + self.assertEqual(parser.get("default", "string"), "foo") + self.assertEqual(parser.get("default", "quoted"), "bar") + self.assertEqual(parser.get("default", "single-quoted"), "foobar") diff --git a/pymysql/tests/thirdparty/__init__.py b/pymysql/tests/thirdparty/__init__.py index bfcc075fc..d5f053711 100644 --- a/pymysql/tests/thirdparty/__init__.py +++ b/pymysql/tests/thirdparty/__init__.py @@ -1,5 +1,6 @@ -from test_MySQLdb import * +from .test_MySQLdb import * if __name__ == "__main__": import unittest + unittest.main() diff --git a/pymysql/tests/thirdparty/test_MySQLdb/__init__.py b/pymysql/tests/thirdparty/test_MySQLdb/__init__.py index b64f273cf..501bfd2db 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/__init__.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/__init__.py @@ -1,7 +1,6 @@ -from test_MySQLdb_capabilities import test_MySQLdb as test_capabilities -from test_MySQLdb_nonstandard import * -from test_MySQLdb_dbapi20 import test_MySQLdb as test_dbapi2 +from .test_MySQLdb_nonstandard import * if __name__ == "__main__": import unittest + unittest.main() diff --git a/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py b/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py index ddd012330..bb47cc5f6 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py @@ -1,292 +1,309 @@ -#!/usr/bin/env python -O """ Script to test database capabilities and the DB-API interface for functionality and memory leaks. Adapted from a script by M-A Lemburg. - + """ from time import time -import array import unittest class DatabaseTest(unittest.TestCase): - db_module = None connect_args = () - connect_kwargs = dict(use_unicode=True, charset="utf8") - create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" + connect_kwargs = dict(use_unicode=True, charset="utf8mb4", binary_prefix=True) + create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8MB4" rows = 10 debug = False - + def setUp(self): - import gc db = self.db_module.connect(*self.connect_args, **self.connect_kwargs) self.connection = db self.cursor = db.cursor() - self.BLOBText = ''.join([chr(i) for i in range(256)] * 100); - self.BLOBUText = u''.join([unichr(i) for i in range(16834)]) - self.BLOBBinary = self.db_module.Binary(''.join([chr(i) for i in range(256)] * 16)) + self.BLOBText = "".join([chr(i) for i in range(256)] * 100) + self.BLOBUText = "".join(chr(i) for i in range(16834)) + data = bytearray(range(256)) * 16 + self.BLOBBinary = self.db_module.Binary(data) leak_test = True - + def tearDown(self): if self.leak_test: import gc + del self.cursor orphans = gc.collect() - self.assertFalse(orphans, "%d orphaned objects found after deleting cursor" % orphans) - + self.assertFalse( + orphans, "%d orphaned objects found after deleting cursor" % orphans + ) + del self.connection orphans = gc.collect() - self.assertFalse(orphans, "%d orphaned objects found after deleting connection" % orphans) - + self.assertFalse( + orphans, "%d orphaned objects found after deleting connection" % orphans + ) + def table_exists(self, name): try: - self.cursor.execute('select * from %s where 1=0' % name) - except: + self.cursor.execute("select * from %s where 1=0" % name) + except Exception: return False else: return True def quote_identifier(self, ident): return '"%s"' % ident - + def new_table_name(self): i = id(self.cursor) while True: - name = self.quote_identifier('tb%08x' % i) + name = self.quote_identifier("tb%08x" % i) if not self.table_exists(name): return name i = i + 1 def create_table(self, columndefs): + """ + Create a table using a list of column definitions given in columndefs. - """ Create a table using a list of column definitions given in - columndefs. - - generator must be a function taking arguments (row_number, - col_number) returning a suitable data object for insertion - into the table. - + generator must be a function taking arguments (row_number, + col_number) returning a suitable data object for insertion + into the table. """ self.table = self.new_table_name() - self.cursor.execute('CREATE TABLE %s (%s) %s' % - (self.table, - ',\n'.join(columndefs), - self.create_table_extra)) + self.cursor.execute( + "CREATE TABLE %s (%s) %s" + % (self.table, ",\n".join(columndefs), self.create_table_extra) + ) def check_data_integrity(self, columndefs, generator): # insert self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] + insert_statement = "INSERT INTO %s VALUES (%s)" % ( + self.table, + ",".join(["%s"] * len(columndefs)), + ) + data = [ + [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) + ] if self.debug: - print data + print(data) self.cursor.executemany(insert_statement, data) self.connection.commit() # verify - self.cursor.execute('select * from %s' % self.table) + self.cursor.execute("select * from %s" % self.table) l = self.cursor.fetchall() if self.debug: - print l - self.assertEquals(len(l), self.rows) + print(l) + self.assertEqual(len(l), self.rows) try: for i in range(self.rows): for j in range(len(columndefs)): - self.assertEquals(l[i][j], generator(i,j)) + self.assertEqual(l[i][j], generator(i, j)) finally: if not self.debug: - self.cursor.execute('drop table %s' % (self.table)) + self.cursor.execute("drop table %s" % (self.table)) def test_transactions(self): - columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') + columndefs = ("col1 INT", "col2 VARCHAR(255)") + def generator(row, col): - if col == 0: return row - else: return ('%i' % (row%10))*255 + if col == 0: + return row + else: + return ("%i" % (row % 10)) * 255 + self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] + insert_statement = "INSERT INTO %s VALUES (%s)" % ( + self.table, + ",".join(["%s"] * len(columndefs)), + ) + data = [ + [generator(i, j) for j in range(len(columndefs))] for i in range(self.rows) + ] self.cursor.executemany(insert_statement, data) # verify self.connection.commit() - self.cursor.execute('select * from %s' % self.table) + self.cursor.execute("select * from %s" % self.table) l = self.cursor.fetchall() - self.assertEquals(len(l), self.rows) + self.assertEqual(len(l), self.rows) for i in range(self.rows): for j in range(len(columndefs)): - self.assertEquals(l[i][j], generator(i,j)) - delete_statement = 'delete from %s where col1=%%s' % self.table + self.assertEqual(l[i][j], generator(i, j)) + delete_statement = "delete from %s where col1=%%s" % self.table self.cursor.execute(delete_statement, (0,)) - self.cursor.execute('select col1 from %s where col1=%s' % \ - (self.table, 0)) + self.cursor.execute("select col1 from %s where col1=%s" % (self.table, 0)) l = self.cursor.fetchall() self.assertFalse(l, "DELETE didn't work") self.connection.rollback() - self.cursor.execute('select col1 from %s where col1=%s' % \ - (self.table, 0)) + self.cursor.execute("select col1 from %s where col1=%s" % (self.table, 0)) l = self.cursor.fetchall() self.assertTrue(len(l) == 1, "ROLLBACK didn't work") - self.cursor.execute('drop table %s' % (self.table)) + self.cursor.execute("drop table %s" % (self.table)) def test_truncation(self): - columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') + columndefs = ("col1 INT", "col2 VARCHAR(255)") + def generator(row, col): - if col == 0: return row - else: return ('%i' % (row%10))*((255-self.rows/2)+row) + if col == 0: + return row + else: + return ("%i" % (row % 10)) * ((255 - self.rows // 2) + row) + self.create_table(columndefs) - insert_statement = ('INSERT INTO %s VALUES (%s)' % - (self.table, - ','.join(['%s'] * len(columndefs)))) + insert_statement = "INSERT INTO %s VALUES (%s)" % ( + self.table, + ",".join(["%s"] * len(columndefs)), + ) try: - self.cursor.execute(insert_statement, (0, '0'*256)) + self.cursor.execute(insert_statement, (0, "0" * 256)) except Warning: - if self.debug: print self.cursor.messages + if self.debug: + print(self.cursor.messages) except self.connection.DataError: pass else: - self.fail("Over-long column did not generate warnings/exception with single insert") + self.fail( + "Over-long column did not generate warnings/exception with single insert" + ) self.connection.rollback() - + try: for i in range(self.rows): data = [] for j in range(len(columndefs)): - data.append(generator(i,j)) - self.cursor.execute(insert_statement,tuple(data)) + data.append(generator(i, j)) + self.cursor.execute(insert_statement, tuple(data)) except Warning: - if self.debug: print self.cursor.messages + if self.debug: + print(self.cursor.messages) except self.connection.DataError: pass else: - self.fail("Over-long columns did not generate warnings/exception with execute()") + self.fail( + "Over-long columns did not generate warnings/exception with execute()" + ) self.connection.rollback() - + try: - data = [ [ generator(i,j) for j in range(len(columndefs)) ] - for i in range(self.rows) ] + data = [ + [generator(i, j) for j in range(len(columndefs))] + for i in range(self.rows) + ] self.cursor.executemany(insert_statement, data) except Warning: - if self.debug: print self.cursor.messages + if self.debug: + print(self.cursor.messages) except self.connection.DataError: pass else: - self.fail("Over-long columns did not generate warnings/exception with executemany()") + self.fail( + "Over-long columns did not generate warnings/exception with executemany()" + ) self.connection.rollback() - self.cursor.execute('drop table %s' % (self.table)) + self.cursor.execute("drop table %s" % (self.table)) def test_CHAR(self): # Character data - def generator(row,col): - return ('%i' % ((row+col) % 10)) * 255 - self.check_data_integrity( - ('col1 char(255)','col2 char(255)'), - generator) + def generator(row, col): + return ("%i" % ((row + col) % 10)) * 255 + + self.check_data_integrity(("col1 char(255)", "col2 char(255)"), generator) def test_INT(self): # Number data - def generator(row,col): - return row*row - self.check_data_integrity( - ('col1 INT',), - generator) + def generator(row, col): + return row * row + + self.check_data_integrity(("col1 INT",), generator) def test_DECIMAL(self): # DECIMAL - def generator(row,col): + def generator(row, col): from decimal import Decimal + return Decimal("%d.%02d" % (row, col)) - self.check_data_integrity( - ('col1 DECIMAL(5,2)',), - generator) + + self.check_data_integrity(("col1 DECIMAL(5,2)",), generator) def test_DATE(self): ticks = time() - def generator(row,col): - return self.db_module.DateFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 DATE',), - generator) + + def generator(row, col): + return self.db_module.DateFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 DATE",), generator) def test_TIME(self): ticks = time() - def generator(row,col): - return self.db_module.TimeFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 TIME',), - generator) + + def generator(row, col): + return self.db_module.TimeFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 TIME",), generator) def test_DATETIME(self): ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 DATETIME',), - generator) + + def generator(row, col): + return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 DATETIME",), generator) def test_TIMESTAMP(self): ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) - self.check_data_integrity( - ('col1 TIMESTAMP',), - generator) + + def generator(row, col): + return self.db_module.TimestampFromTicks(ticks + row * 86400 - col * 1313) + + self.check_data_integrity(("col1 TIMESTAMP",), generator) def test_fractional_TIMESTAMP(self): ticks = time() - def generator(row,col): - return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313+row*0.7*col/3.0) - self.check_data_integrity( - ('col1 TIMESTAMP',), - generator) + + def generator(row, col): + return self.db_module.TimestampFromTicks( + ticks + row * 86400 - col * 1313 + row * 0.7 * col / 3.0 + ) + + self.check_data_integrity(("col1 TIMESTAMP",), generator) def test_LONG(self): - def generator(row,col): + def generator(row, col): if col == 0: return row else: - return self.BLOBUText # 'BLOB Text ' * 1024 - self.check_data_integrity( - ('col1 INT', 'col2 LONG'), - generator) + return self.BLOBUText # 'BLOB Text ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 LONG"), generator) def test_TEXT(self): - def generator(row,col): + def generator(row, col): if col == 0: return row else: - return self.BLOBUText[:5192] # 'BLOB Text ' * 1024 - self.check_data_integrity( - ('col1 INT', 'col2 TEXT'), - generator) + return self.BLOBUText[:5192] # 'BLOB Text ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 TEXT"), generator) def test_LONG_BYTE(self): - def generator(row,col): + def generator(row, col): if col == 0: return row else: - return self.BLOBBinary # 'BLOB\000Binary ' * 1024 - self.check_data_integrity( - ('col1 INT','col2 LONG BYTE'), - generator) + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + + self.check_data_integrity(("col1 INT", "col2 LONG BYTE"), generator) def test_BLOB(self): - def generator(row,col): + def generator(row, col): if col == 0: return row else: - return self.BLOBBinary # 'BLOB\000Binary ' * 1024 - self.check_data_integrity( - ('col1 INT','col2 BLOB'), - generator) + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + self.check_data_integrity(("col1 INT", "col2 BLOB"), generator) diff --git a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py index a419e34a4..fff14b86f 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python -''' Python DB API 2.0 driver compliance unit test suite. - +""" Python DB API 2.0 driver compliance unit test suite. + This software is Public Domain and may be used without restrictions. "Now we have booze and barflies entering the discussion, plus rumours of @@ -9,14 +8,14 @@ this is turning out to be a thoroughly unwholesome unit test." -- Ian Bicking -''' +""" -__rcs_id__ = '$Id$' -__version__ = '$Revision$'[11:-2] -__author__ = 'Stuart Bishop ' +__rcs_id__ = "$Id$" +__version__ = "$Revision$"[11:-2] +__author__ = "Stuart Bishop " -import unittest import time +import unittest # $Log$ # Revision 1.1.2.1 2006/02/25 03:44:32 adustman @@ -52,9 +51,9 @@ # - Now a subclass of TestCase, to avoid requiring the driver stub # to use multiple inheritance # - Reversed the polarity of buggy test in test_description -# - Test exception heirarchy correctly +# - Test exception hierarchy correctly # - self.populate is now self._populate(), so if a driver stub -# overrides self.ddl1 this change propogates +# overrides self.ddl1 this change propagates # - VARCHAR columns now have a width, which will hopefully make the # DDL even more portible (this will be reversed if it causes more problems) # - cursor.rowcount being checked after various execute and fetchXXX methods @@ -64,69 +63,70 @@ # - Fix bugs in test_setoutputsize_basic and test_setinputsizes # + class DatabaseAPI20Test(unittest.TestCase): - ''' Test a database self.driver for DB API 2.0 compatibility. - This implementation tests Gadfly, but the TestCase - is structured so that other self.drivers can subclass this - test case to ensure compiliance with the DB-API. It is - expected that this TestCase may be expanded in the future - if ambiguities or edge conditions are discovered. + """Test a database self.driver for DB API 2.0 compatibility. + This implementation tests Gadfly, but the TestCase + is structured so that other self.drivers can subclass this + test case to ensure compiliance with the DB-API. It is + expected that this TestCase may be expanded in the future + if ambiguities or edge conditions are discovered. - The 'Optional Extensions' are not yet being tested. + The 'Optional Extensions' are not yet being tested. - self.drivers should subclass this test, overriding setUp, tearDown, - self.driver, connect_args and connect_kw_args. Class specification - should be as follows: + self.drivers should subclass this test, overriding setUp, tearDown, + self.driver, connect_args and connect_kw_args. Class specification + should be as follows: - import dbapi20 - class mytest(dbapi20.DatabaseAPI20Test): - [...] + import dbapi20 + class mytest(dbapi20.DatabaseAPI20Test): + [...] - Don't 'import DatabaseAPI20Test from dbapi20', or you will - confuse the unit tester - just 'import dbapi20'. - ''' + Don't 'import DatabaseAPI20Test from dbapi20', or you will + confuse the unit tester - just 'import dbapi20'. + """ # The self.driver module. This should be the module where the 'connect' # method is to be found driver = None - connect_args = () # List of arguments to pass to connect - connect_kw_args = {} # Keyword arguments for connect - table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables + connect_args = () # List of arguments to pass to connect + connect_kw_args = {} # Keyword arguments for connect + table_prefix = "dbapi20test_" # If you need to specify a prefix for tables + + ddl1 = "create table %sbooze (name varchar(20))" % table_prefix + ddl2 = "create table %sbarflys (name varchar(20))" % table_prefix + xddl1 = "drop table %sbooze" % table_prefix + xddl2 = "drop table %sbarflys" % table_prefix - ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix - ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix - xddl1 = 'drop table %sbooze' % table_prefix - xddl2 = 'drop table %sbarflys' % table_prefix + lowerfunc = "lower" # Name of stored procedure to convert string->lowercase - lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase - # Some drivers may need to override these helpers, for example adding # a 'commit' after the execute. - def executeDDL1(self,cursor): + def executeDDL1(self, cursor): cursor.execute(self.ddl1) - def executeDDL2(self,cursor): + def executeDDL2(self, cursor): cursor.execute(self.ddl2) def setUp(self): - ''' self.drivers should override this method to perform required setup - if any is necessary, such as creating the database. - ''' + """self.drivers should override this method to perform required setup + if any is necessary, such as creating the database. + """ pass def tearDown(self): - ''' self.drivers should override this method to perform required cleanup - if any is necessary, such as deleting the test database. - The default drops the tables that may be created. - ''' + """self.drivers should override this method to perform required cleanup + if any is necessary, such as deleting the test database. + The default drops the tables that may be created. + """ con = self._connect() try: cur = con.cursor() - for ddl in (self.xddl1,self.xddl2): - try: + for ddl in (self.xddl1, self.xddl2): + try: cur.execute(ddl) con.commit() - except self.driver.Error: + except self.driver.Error: # Assume table didn't exist. Other tests will check if # execute is busted. pass @@ -135,9 +135,7 @@ def tearDown(self): def _connect(self): try: - return self.driver.connect( - *self.connect_args,**self.connect_kw_args - ) + return self.driver.connect(*self.connect_args, **self.connect_kw_args) except AttributeError: self.fail("No connect method found in self.driver module") @@ -150,7 +148,7 @@ def test_apilevel(self): # Must exist apilevel = self.driver.apilevel # Must equal 2.0 - self.assertEqual(apilevel,'2.0') + self.assertEqual(apilevel, "2.0") except AttributeError: self.fail("Driver doesn't define apilevel") @@ -159,7 +157,7 @@ def test_threadsafety(self): # Must exist threadsafety = self.driver.threadsafety # Must be a valid value - self.assertTrue(threadsafety in (0,1,2,3)) + self.assertTrue(threadsafety in (0, 1, 2, 3)) except AttributeError: self.fail("Driver doesn't define threadsafety") @@ -168,38 +166,24 @@ def test_paramstyle(self): # Must exist paramstyle = self.driver.paramstyle # Must be a valid value - self.assertTrue(paramstyle in ( - 'qmark','numeric','named','format','pyformat' - )) + self.assertTrue( + paramstyle in ("qmark", "numeric", "named", "format", "pyformat") + ) except AttributeError: self.fail("Driver doesn't define paramstyle") def test_Exceptions(self): # Make sure required exceptions exist, and are in the - # defined heirarchy. - self.assertTrue(issubclass(self.driver.Warning,StandardError)) - self.assertTrue(issubclass(self.driver.Error,StandardError)) - self.assertTrue( - issubclass(self.driver.InterfaceError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.DatabaseError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.OperationalError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.IntegrityError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.InternalError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.ProgrammingError,self.driver.Error) - ) - self.assertTrue( - issubclass(self.driver.NotSupportedError,self.driver.Error) - ) + # defined hierarchy. + self.assertTrue(issubclass(self.driver.Warning, Exception)) + self.assertTrue(issubclass(self.driver.Error, Exception)) + self.assertTrue(issubclass(self.driver.InterfaceError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.DatabaseError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.OperationalError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.IntegrityError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.InternalError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.ProgrammingError, self.driver.Error)) + self.assertTrue(issubclass(self.driver.NotSupportedError, self.driver.Error)) def test_ExceptionsAsConnectionAttributes(self): # OPTIONAL EXTENSION @@ -220,7 +204,6 @@ def test_ExceptionsAsConnectionAttributes(self): self.assertTrue(con.ProgrammingError is drv.ProgrammingError) self.assertTrue(con.NotSupportedError is drv.NotSupportedError) - def test_commit(self): con = self._connect() try: @@ -233,16 +216,16 @@ def test_rollback(self): con = self._connect() # If rollback is defined, it should either work or throw # the documented exception - if hasattr(con,'rollback'): + if hasattr(con, "rollback"): try: con.rollback() except self.driver.NotSupportedError: pass - + def test_cursor(self): con = self._connect() try: - cur = con.cursor() + con.cursor() finally: con.close() @@ -254,14 +237,14 @@ def test_cursor_isolation(self): cur1 = con.cursor() cur2 = con.cursor() self.executeDDL1(cur1) - cur1.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) + cur1.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) cur2.execute("select name from %sbooze" % self.table_prefix) booze = cur2.fetchall() - self.assertEqual(len(booze),1) - self.assertEqual(len(booze[0]),1) - self.assertEqual(booze[0][0],'Victoria Bitter') + self.assertEqual(len(booze), 1) + self.assertEqual(len(booze[0]), 1) + self.assertEqual(booze[0][0], "Victoria Bitter") finally: con.close() @@ -270,31 +253,41 @@ def test_description(self): try: cur = con.cursor() self.executeDDL1(cur) - self.assertEqual(cur.description,None, - 'cursor.description should be none after executing a ' - 'statement that can return no rows (such as DDL)' - ) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(len(cur.description),1, - 'cursor.description describes too many columns' - ) - self.assertEqual(len(cur.description[0]),7, - 'cursor.description[x] tuples must have 7 elements' - ) - self.assertEqual(cur.description[0][0].lower(),'name', - 'cursor.description[x][0] must return column name' - ) - self.assertEqual(cur.description[0][1],self.driver.STRING, - 'cursor.description[x][1] must return column type. Got %r' - % cur.description[0][1] - ) + self.assertEqual( + cur.description, + None, + "cursor.description should be none after executing a " + "statement that can return no rows (such as DDL)", + ) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + len(cur.description), 1, "cursor.description describes too many columns" + ) + self.assertEqual( + len(cur.description[0]), + 7, + "cursor.description[x] tuples must have 7 elements", + ) + self.assertEqual( + cur.description[0][0].lower(), + "name", + "cursor.description[x][0] must return column name", + ) + self.assertEqual( + cur.description[0][1], + self.driver.STRING, + "cursor.description[x][1] must return column type. Got %r" + % cur.description[0][1], + ) # Make sure self.description gets reset self.executeDDL2(cur) - self.assertEqual(cur.description,None, - 'cursor.description not being set to None when executing ' - 'no-result statements (eg. DDL)' - ) + self.assertEqual( + cur.description, + None, + "cursor.description not being set to None when executing " + "no-result statements (eg. DDL)", + ) finally: con.close() @@ -303,47 +296,49 @@ def test_rowcount(self): try: cur = con.cursor() self.executeDDL1(cur) - self.assertEqual(cur.rowcount,-1, - 'cursor.rowcount should be -1 after executing no-result ' - 'statements' - ) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number or rows inserted, or ' - 'set to -1 after executing an insert statement' - ) + self.assertEqual( + cur.rowcount, + -1, + "cursor.rowcount should be -1 after executing no-result statements", + ) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number or rows inserted, or " + "set to -1 after executing an insert statement", + ) cur.execute("select name from %sbooze" % self.table_prefix) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number of rows returned, or ' - 'set to -1 after executing a select statement' - ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number of rows returned, or " + "set to -1 after executing a select statement", + ) self.executeDDL2(cur) - self.assertEqual(cur.rowcount,-1, - 'cursor.rowcount not being reset to -1 after executing ' - 'no-result statements' - ) + self.assertEqual( + cur.rowcount, + -1, + "cursor.rowcount not being reset to -1 after executing " + "no-result statements", + ) finally: con.close() - lower_func = 'lower' + lower_func = "lower" + def test_callproc(self): con = self._connect() try: cur = con.cursor() - if self.lower_func and hasattr(cur,'callproc'): - r = cur.callproc(self.lower_func,('FOO',)) - self.assertEqual(len(r),1) - self.assertEqual(r[0],'FOO') + if self.lower_func and hasattr(cur, "callproc"): + r = cur.callproc(self.lower_func, ("FOO",)) + self.assertEqual(len(r), 1) + self.assertEqual(r[0], "FOO") r = cur.fetchall() - self.assertEqual(len(r),1,'callproc produced no result set') - self.assertEqual(len(r[0]),1, - 'callproc produced invalid result set' - ) - self.assertEqual(r[0][0],'foo', - 'callproc produced invalid results' - ) + self.assertEqual(len(r), 1, "callproc produced no result set") + self.assertEqual(len(r[0]), 1, "callproc produced invalid result set") + self.assertEqual(r[0][0], "foo", "callproc produced invalid results") finally: con.close() @@ -356,14 +351,14 @@ def test_close(self): # cursor.execute should raise an Error if called after connection # closed - self.assertRaises(self.driver.Error,self.executeDDL1,cur) + self.assertRaises(self.driver.Error, self.executeDDL1, cur) # connection.commit should raise an Error if called after connection' # closed.' - self.assertRaises(self.driver.Error,con.commit) + self.assertRaises(self.driver.Error, con.commit) # connection.close should raise an Error if called more than once - self.assertRaises(self.driver.Error,con.close) + self.assertRaises(self.driver.Error, con.close) def test_execute(self): con = self._connect() @@ -373,105 +368,99 @@ def test_execute(self): finally: con.close() - def _paraminsert(self,cur): + def _paraminsert(self, cur): self.executeDDL1(cur) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertTrue(cur.rowcount in (-1,1)) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertTrue(cur.rowcount in (-1, 1)) - if self.driver.paramstyle == 'qmark': + if self.driver.paramstyle == "qmark": cur.execute( - 'insert into %sbooze values (?)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'numeric': + "insert into %sbooze values (?)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "numeric": cur.execute( - 'insert into %sbooze values (:1)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'named': + "insert into %sbooze values (:1)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "named": cur.execute( - 'insert into %sbooze values (:beer)' % self.table_prefix, - {'beer':"Cooper's"} - ) - elif self.driver.paramstyle == 'format': + "insert into %sbooze values (:beer)" % self.table_prefix, + {"beer": "Cooper's"}, + ) + elif self.driver.paramstyle == "format": cur.execute( - 'insert into %sbooze values (%%s)' % self.table_prefix, - ("Cooper's",) - ) - elif self.driver.paramstyle == 'pyformat': + "insert into %sbooze values (%%s)" % self.table_prefix, ("Cooper's",) + ) + elif self.driver.paramstyle == "pyformat": cur.execute( - 'insert into %sbooze values (%%(beer)s)' % self.table_prefix, - {'beer':"Cooper's"} - ) + "insert into %sbooze values (%%(beer)s)" % self.table_prefix, + {"beer": "Cooper's"}, + ) else: - self.fail('Invalid paramstyle') - self.assertTrue(cur.rowcount in (-1,1)) + self.fail("Invalid paramstyle") + self.assertTrue(cur.rowcount in (-1, 1)) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) res = cur.fetchall() - self.assertEqual(len(res),2,'cursor.fetchall returned too few rows') - beers = [res[0][0],res[1][0]] + self.assertEqual(len(res), 2, "cursor.fetchall returned too few rows") + beers = [res[0][0], res[1][0]] beers.sort() - self.assertEqual(beers[0],"Cooper's", - 'cursor.fetchall retrieved incorrect data, or data inserted ' - 'incorrectly' - ) - self.assertEqual(beers[1],"Victoria Bitter", - 'cursor.fetchall retrieved incorrect data, or data inserted ' - 'incorrectly' - ) + self.assertEqual( + beers[0], + "Cooper's", + "cursor.fetchall retrieved incorrect data, or data inserted incorrectly", + ) + self.assertEqual( + beers[1], + "Victoria Bitter", + "cursor.fetchall retrieved incorrect data, or data inserted incorrectly", + ) def test_executemany(self): con = self._connect() try: cur = con.cursor() self.executeDDL1(cur) - largs = [ ("Cooper's",) , ("Boag's",) ] - margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ] - if self.driver.paramstyle == 'qmark': + largs = [("Cooper's",), ("Boag's",)] + margs = [{"beer": "Cooper's"}, {"beer": "Boag's"}] + if self.driver.paramstyle == "qmark": cur.executemany( - 'insert into %sbooze values (?)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'numeric': + "insert into %sbooze values (?)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "numeric": cur.executemany( - 'insert into %sbooze values (:1)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'named': + "insert into %sbooze values (:1)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "named": cur.executemany( - 'insert into %sbooze values (:beer)' % self.table_prefix, - margs - ) - elif self.driver.paramstyle == 'format': + "insert into %sbooze values (:beer)" % self.table_prefix, margs + ) + elif self.driver.paramstyle == "format": cur.executemany( - 'insert into %sbooze values (%%s)' % self.table_prefix, - largs - ) - elif self.driver.paramstyle == 'pyformat': + "insert into %sbooze values (%%s)" % self.table_prefix, largs + ) + elif self.driver.paramstyle == "pyformat": cur.executemany( - 'insert into %sbooze values (%%(beer)s)' % ( - self.table_prefix - ), - margs - ) - else: - self.fail('Unknown paramstyle') - self.assertTrue(cur.rowcount in (-1,2), - 'insert using cursor.executemany set cursor.rowcount to ' - 'incorrect value %r' % cur.rowcount + "insert into %sbooze values (%%(beer)s)" % (self.table_prefix), + margs, ) - cur.execute('select name from %sbooze' % self.table_prefix) + else: + self.fail("Unknown paramstyle") + self.assertTrue( + cur.rowcount in (-1, 2), + "insert using cursor.executemany set cursor.rowcount to " + "incorrect value %r" % cur.rowcount, + ) + cur.execute("select name from %sbooze" % self.table_prefix) res = cur.fetchall() - self.assertEqual(len(res),2, - 'cursor.fetchall retrieved incorrect number of rows' - ) - beers = [res[0][0],res[1][0]] + self.assertEqual( + len(res), 2, "cursor.fetchall retrieved incorrect number of rows" + ) + beers = [res[0][0], res[1][0]] beers.sort() - self.assertEqual(beers[0],"Boag's",'incorrect data retrieved') - self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved') + self.assertEqual(beers[0], "Boag's", "incorrect data retrieved") + self.assertEqual(beers[1], "Cooper's", "incorrect data retrieved") finally: con.close() @@ -482,59 +471,62 @@ def test_fetchone(self): # cursor.fetchone should raise an Error if called before # executing a select-type query - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) # cursor.fetchone should raise an Error if called after - # executing a query that cannnot return rows + # executing a query that cannot return rows self.executeDDL1(cur) - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if a query retrieves ' - 'no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if a query retrieves no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) # cursor.fetchone should raise an Error if called after - # executing a query that cannnot return rows - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) - self.assertRaises(self.driver.Error,cur.fetchone) + # executing a query that cannot return rows + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + self.assertRaises(self.driver.Error, cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchone() - self.assertEqual(len(r),1, - 'cursor.fetchone should have retrieved a single row' - ) - self.assertEqual(r[0],'Victoria Bitter', - 'cursor.fetchone retrieved incorrect data' - ) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if no more rows available' - ) - self.assertTrue(cur.rowcount in (-1,1)) + self.assertEqual( + len(r), 1, "cursor.fetchone should have retrieved a single row" + ) + self.assertEqual( + r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data" + ) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if no more rows available", + ) + self.assertTrue(cur.rowcount in (-1, 1)) finally: con.close() samples = [ - 'Carlton Cold', - 'Carlton Draft', - 'Mountain Goat', - 'Redback', - 'Victoria Bitter', - 'XXXX' - ] + "Carlton Cold", + "Carlton Draft", + "Mountain Goat", + "Redback", + "Victoria Bitter", + "XXXX", + ] def _populate(self): - ''' Return a list of sql commands to setup the DB for the fetch - tests. - ''' + """Return a list of sql commands to setup the DB for the fetch + tests. + """ populate = [ - "insert into %sbooze values ('%s')" % (self.table_prefix,s) - for s in self.samples - ] + "insert into %sbooze values ('%s')" % (self.table_prefix, s) + for s in self.samples + ] return populate def test_fetchmany(self): @@ -543,78 +535,88 @@ def test_fetchmany(self): cur = con.cursor() # cursor.fetchmany should raise an Error if called without - #issuing a query - self.assertRaises(self.driver.Error,cur.fetchmany,4) + # issuing a query + self.assertRaises(self.driver.Error, cur.fetchmany, 4) self.executeDDL1(cur) for sql in self._populate(): cur.execute(sql) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchmany() - self.assertEqual(len(r),1, - 'cursor.fetchmany retrieved incorrect number of rows, ' - 'default of arraysize is one.' - ) - cur.arraysize=10 - r = cur.fetchmany(3) # Should get 3 rows - self.assertEqual(len(r),3, - 'cursor.fetchmany retrieved incorrect number of rows' - ) - r = cur.fetchmany(4) # Should get 2 more - self.assertEqual(len(r),2, - 'cursor.fetchmany retrieved incorrect number of rows' - ) - r = cur.fetchmany(4) # Should be an empty sequence - self.assertEqual(len(r),0, - 'cursor.fetchmany should return an empty sequence after ' - 'results are exhausted' + self.assertEqual( + len(r), + 1, + "cursor.fetchmany retrieved incorrect number of rows, " + "default of arraysize is one.", ) - self.assertTrue(cur.rowcount in (-1,6)) + cur.arraysize = 10 + r = cur.fetchmany(3) # Should get 3 rows + self.assertEqual( + len(r), 3, "cursor.fetchmany retrieved incorrect number of rows" + ) + r = cur.fetchmany(4) # Should get 2 more + self.assertEqual( + len(r), 2, "cursor.fetchmany retrieved incorrect number of rows" + ) + r = cur.fetchmany(4) # Should be an empty sequence + self.assertEqual( + len(r), + 0, + "cursor.fetchmany should return an empty sequence after " + "results are exhausted", + ) + self.assertTrue(cur.rowcount in (-1, 6)) # Same as above, using cursor.arraysize - cur.arraysize=4 - cur.execute('select name from %sbooze' % self.table_prefix) - r = cur.fetchmany() # Should get 4 rows - self.assertEqual(len(r),4, - 'cursor.arraysize not being honoured by fetchmany' - ) - r = cur.fetchmany() # Should get 2 more - self.assertEqual(len(r),2) - r = cur.fetchmany() # Should be an empty sequence - self.assertEqual(len(r),0) - self.assertTrue(cur.rowcount in (-1,6)) - - cur.arraysize=6 - cur.execute('select name from %sbooze' % self.table_prefix) - rows = cur.fetchmany() # Should get all rows - self.assertTrue(cur.rowcount in (-1,6)) - self.assertEqual(len(rows),6) - self.assertEqual(len(rows),6) + cur.arraysize = 4 + cur.execute("select name from %sbooze" % self.table_prefix) + r = cur.fetchmany() # Should get 4 rows + self.assertEqual( + len(r), 4, "cursor.arraysize not being honoured by fetchmany" + ) + r = cur.fetchmany() # Should get 2 more + self.assertEqual(len(r), 2) + r = cur.fetchmany() # Should be an empty sequence + self.assertEqual(len(r), 0) + self.assertTrue(cur.rowcount in (-1, 6)) + + cur.arraysize = 6 + cur.execute("select name from %sbooze" % self.table_prefix) + rows = cur.fetchmany() # Should get all rows + self.assertTrue(cur.rowcount in (-1, 6)) + self.assertEqual(len(rows), 6) + self.assertEqual(len(rows), 6) rows = [r[0] for r in rows] rows.sort() - + # Make sure we get the right data back out - for i in range(0,6): - self.assertEqual(rows[i],self.samples[i], - 'incorrect data retrieved by cursor.fetchmany' - ) - - rows = cur.fetchmany() # Should return an empty list - self.assertEqual(len(rows),0, - 'cursor.fetchmany should return an empty sequence if ' - 'called after the whole result set has been fetched' + for i in range(0, 6): + self.assertEqual( + rows[i], + self.samples[i], + "incorrect data retrieved by cursor.fetchmany", ) - self.assertTrue(cur.rowcount in (-1,6)) + + rows = cur.fetchmany() # Should return an empty list + self.assertEqual( + len(rows), + 0, + "cursor.fetchmany should return an empty sequence if " + "called after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, 6)) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) - r = cur.fetchmany() # Should get empty sequence - self.assertEqual(len(r),0, - 'cursor.fetchmany should return an empty sequence if ' - 'query retrieved no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbarflys" % self.table_prefix) + r = cur.fetchmany() # Should get empty sequence + self.assertEqual( + len(r), + 0, + "cursor.fetchmany should return an empty sequence if " + "query retrieved no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) finally: con.close() @@ -634,40 +636,45 @@ def test_fetchall(self): # cursor.fetchall should raise an Error if called # after executing a a statement that cannot return rows - self.assertRaises(self.driver.Error,cur.fetchall) + self.assertRaises(self.driver.Error, cur.fetchall) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,len(self.samples))) - self.assertEqual(len(rows),len(self.samples), - 'cursor.fetchall did not retrieve all rows' - ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) + self.assertEqual( + len(rows), + len(self.samples), + "cursor.fetchall did not retrieve all rows", + ) rows = [r[0] for r in rows] rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'cursor.fetchall retrieved incorrect rows' + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows" ) rows = cur.fetchall() self.assertEqual( - len(rows),0, - 'cursor.fetchall should return an empty list if called ' - 'after the whole result set has been fetched' - ) - self.assertTrue(cur.rowcount in (-1,len(self.samples))) + len(rows), + 0, + "cursor.fetchall should return an empty list if called " + "after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) + cur.execute("select name from %sbarflys" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,0)) - self.assertEqual(len(rows),0, - 'cursor.fetchall should return an empty list if ' - 'a select query returns no rows' - ) - + self.assertTrue(cur.rowcount in (-1, 0)) + self.assertEqual( + len(rows), + 0, + "cursor.fetchall should return an empty list if " + "a select query returns no rows", + ) + finally: con.close() - + def test_mixedfetch(self): con = self._connect() try: @@ -676,74 +683,74 @@ def test_mixedfetch(self): for sql in self._populate(): cur.execute(sql) - cur.execute('select name from %sbooze' % self.table_prefix) - rows1 = cur.fetchone() + cur.execute("select name from %sbooze" % self.table_prefix) + rows1 = cur.fetchone() rows23 = cur.fetchmany(2) - rows4 = cur.fetchone() + rows4 = cur.fetchone() rows56 = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,6)) - self.assertEqual(len(rows23),2, - 'fetchmany returned incorrect number of rows' - ) - self.assertEqual(len(rows56),2, - 'fetchall returned incorrect number of rows' - ) + self.assertTrue(cur.rowcount in (-1, 6)) + self.assertEqual( + len(rows23), 2, "fetchmany returned incorrect number of rows" + ) + self.assertEqual( + len(rows56), 2, "fetchall returned incorrect number of rows" + ) rows = [rows1[0]] - rows.extend([rows23[0][0],rows23[1][0]]) + rows.extend([rows23[0][0], rows23[1][0]]) rows.append(rows4[0]) - rows.extend([rows56[0][0],rows56[1][0]]) + rows.extend([rows56[0][0], rows56[1][0]]) rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'incorrect data retrieved or inserted' - ) + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "incorrect data retrieved or inserted" + ) finally: con.close() - def help_nextset_setUp(self,cur): - ''' Should create a procedure called deleteme - that returns two result sets, first the - number of rows in booze then "name from booze" - ''' - raise NotImplementedError,'Helper not implemented' - #sql=""" + def help_nextset_setUp(self, cur): + """Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + """ + raise NotImplementedError("Helper not implemented") + # sql=""" # create procedure deleteme as # begin # select count(*) from booze # select name from booze # end - #""" - #cur.execute(sql) + # """ + # cur.execute(sql) - def help_nextset_tearDown(self,cur): - 'If cleaning up is needed after nextSetTest' - raise NotImplementedError,'Helper not implemented' - #cur.execute("drop procedure deleteme") + def help_nextset_tearDown(self, cur): + "If cleaning up is needed after nextSetTest" + raise NotImplementedError("Helper not implemented") + # cur.execute("drop procedure deleteme") def test_nextset(self): con = self._connect() try: cur = con.cursor() - if not hasattr(cur,'nextset'): + if not hasattr(cur, "nextset"): return try: self.executeDDL1(cur) - sql=self._populate() + sql = self._populate() for sql in self._populate(): cur.execute(sql) self.help_nextset_setUp(cur) - cur.callproc('deleteme') - numberofrows=cur.fetchone() - assert numberofrows[0]== len(self.samples) + cur.callproc("deleteme") + numberofrows = cur.fetchone() + assert numberofrows[0] == len(self.samples) assert cur.nextset() - names=cur.fetchall() + names = cur.fetchall() assert len(names) == len(self.samples) - s=cur.nextset() - assert s == None,'No more return sets, should return None' + s = cur.nextset() + assert s == None, "No more return sets, should return None" finally: self.help_nextset_tearDown(cur) @@ -751,16 +758,16 @@ def test_nextset(self): con.close() def test_nextset(self): - raise NotImplementedError,'Drivers need to override this test' + raise NotImplementedError("Drivers need to override this test") def test_arraysize(self): # Not much here - rest of the tests for this are in test_fetchmany con = self._connect() try: cur = con.cursor() - self.assertTrue(hasattr(cur,'arraysize'), - 'cursor.arraysize must be defined' - ) + self.assertTrue( + hasattr(cur, "arraysize"), "cursor.arraysize must be defined" + ) finally: con.close() @@ -768,8 +775,8 @@ def test_setinputsizes(self): con = self._connect() try: cur = con.cursor() - cur.setinputsizes( (25,) ) - self._paraminsert(cur) # Make sure cursor still works + cur.setinputsizes((25,)) + self._paraminsert(cur) # Make sure cursor still works finally: con.close() @@ -779,75 +786,68 @@ def test_setoutputsize_basic(self): try: cur = con.cursor() cur.setoutputsize(1000) - cur.setoutputsize(2000,0) - self._paraminsert(cur) # Make sure the cursor still works + cur.setoutputsize(2000, 0) + self._paraminsert(cur) # Make sure the cursor still works finally: con.close() def test_setoutputsize(self): - # Real test for setoutputsize is driver dependant - raise NotImplementedError,'Driver need to override this test' + # Real test for setoutputsize is driver dependent + raise NotImplementedError("Driver need to override this test") def test_None(self): con = self._connect() try: cur = con.cursor() self.executeDDL1(cur) - cur.execute('insert into %sbooze values (NULL)' % self.table_prefix) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("insert into %sbooze values (NULL)" % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchall() - self.assertEqual(len(r),1) - self.assertEqual(len(r[0]),1) - self.assertEqual(r[0][0],None,'NULL value not returned as None') + self.assertEqual(len(r), 1) + self.assertEqual(len(r[0]), 1) + self.assertEqual(r[0][0], None, "NULL value not returned as None") finally: con.close() def test_Date(self): - d1 = self.driver.Date(2002,12,25) - d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0))) + self.driver.Date(2002, 12, 25) + self.driver.DateFromTicks(time.mktime((2002, 12, 25, 0, 0, 0, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(d1),str(d2)) def test_Time(self): - t1 = self.driver.Time(13,45,30) - t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0))) + self.driver.Time(13, 45, 30) + self.driver.TimeFromTicks(time.mktime((2001, 1, 1, 13, 45, 30, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Timestamp(self): - t1 = self.driver.Timestamp(2002,12,25,13,45,30) - t2 = self.driver.TimestampFromTicks( - time.mktime((2002,12,25,13,45,30,0,0,0)) - ) + self.driver.Timestamp(2002, 12, 25, 13, 45, 30) + self.driver.TimestampFromTicks(time.mktime((2002, 12, 25, 13, 45, 30, 0, 0, 0))) # Can we assume this? API doesn't specify, but it seems implied # self.assertEqual(str(t1),str(t2)) def test_Binary(self): - b = self.driver.Binary('Something') - b = self.driver.Binary('') + self.driver.Binary(b"Something") + self.driver.Binary(b"") def test_STRING(self): - self.assertTrue(hasattr(self.driver,'STRING'), - 'module.STRING must be defined' - ) + self.assertTrue(hasattr(self.driver, "STRING"), "module.STRING must be defined") def test_BINARY(self): - self.assertTrue(hasattr(self.driver,'BINARY'), - 'module.BINARY must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "BINARY"), "module.BINARY must be defined." + ) def test_NUMBER(self): - self.assertTrue(hasattr(self.driver,'NUMBER'), - 'module.NUMBER must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "NUMBER"), "module.NUMBER must be defined." + ) def test_DATETIME(self): - self.assertTrue(hasattr(self.driver,'DATETIME'), - 'module.DATETIME must be defined.' - ) + self.assertTrue( + hasattr(self.driver, "DATETIME"), "module.DATETIME must be defined." + ) def test_ROWID(self): - self.assertTrue(hasattr(self.driver,'ROWID'), - 'module.ROWID must be defined.' - ) - + self.assertTrue(hasattr(self.driver, "ROWID"), "module.ROWID must be defined.") diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py index e0bc93439..6a2894a5a 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py @@ -1,115 +1,107 @@ -#!/usr/bin/env python -import capabilities -import unittest +from . import capabilities import pymysql from pymysql.tests import base import warnings -warnings.filterwarnings('error') +warnings.filterwarnings("error") -class test_MySQLdb(capabilities.DatabaseTest): +class test_MySQLdb(capabilities.DatabaseTest): db_module = pymysql connect_args = () connect_kwargs = base.PyMySQLTestCase.databases[0].copy() - connect_kwargs.update(dict(read_default_file='~/.my.cnf', - use_unicode=True, - charset='utf8', sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")) + connect_kwargs.update( + dict( + read_default_file="~/.my.cnf", + use_unicode=True, + binary_prefix=True, + charset="utf8mb4", + sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL", + ) + ) - create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" leak_test = False - + def quote_identifier(self, ident): return "`%s`" % ident def test_TIME(self): from datetime import timedelta - def generator(row,col): - return timedelta(0, row*8000) - self.check_data_integrity( - ('col1 TIME',), - generator) + + def generator(row, col): + return timedelta(0, row * 8000) + + self.check_data_integrity(("col1 TIME",), generator) def test_TINYINT(self): # Number data - def generator(row,col): - v = (row*row) % 256 + def generator(row, col): + v = (row * row) % 256 if v > 127: - v = v-256 + v = v - 256 return v - self.check_data_integrity( - ('col1 TINYINT',), - generator) - + + self.check_data_integrity(("col1 TINYINT",), generator) + def test_stored_procedures(self): db = self.connection c = self.cursor try: - self.create_table(('pos INT', 'tree CHAR(20)')) - c.executemany("INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, - list(enumerate('ash birch cedar larch pine'.split()))) + self.create_table(("pos INT", "tree CHAR(20)")) + c.executemany( + "INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, + list(enumerate("ash birch cedar larch pine".split())), + ) db.commit() - - c.execute(""" + + c.execute( + """ CREATE PROCEDURE test_sp(IN t VARCHAR(255)) BEGIN SELECT pos FROM %s WHERE tree = t; END - """ % self.table) + """ + % self.table + ) db.commit() - - c.callproc('test_sp', ('larch',)) + + c.callproc("test_sp", ("larch",)) rows = c.fetchall() - self.assertEquals(len(rows), 1) - self.assertEquals(rows[0][0], 3) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], 3) c.nextset() finally: c.execute("DROP PROCEDURE IF EXISTS test_sp") - c.execute('drop table %s' % (self.table)) + c.execute("drop table %s" % (self.table)) def test_small_CHAR(self): # Character data - def generator(row,col): - i = ((row+1)*(col+1)+62)%256 - if i == 62: return '' - if i == 63: return None + def generator(row, col): + i = ((row + 1) * (col + 1) + 62) % 256 + if i == 62: + return "" + if i == 63: + return None return chr(i) - self.check_data_integrity( - ('col1 char(1)','col2 char(1)'), - generator) + + self.check_data_integrity(("col1 char(1)", "col2 char(1)"), generator) def test_bug_2671682(self): from pymysql.constants import ER + try: - self.cursor.execute("describe some_non_existent_table"); - except self.connection.ProgrammingError, msg: - self.assertTrue(msg.args[0] == ER.NO_SUCH_TABLE) - - def test_insert_values(self): - from pymysql.cursors import insert_values - query = """INSERT FOO (a, b, c) VALUES (a, b, c)""" - matched = insert_values.search(query) - self.assertTrue(matched) - values = matched.group(1) - self.assertTrue(values == "(a, b, c)") - + self.cursor.execute("describe some_non_existent_table") + except self.connection.ProgrammingError as msg: + self.assertEqual(msg.args[0], ER.NO_SUCH_TABLE) + def test_ping(self): self.connection.ping() def test_literal_int(self): self.assertTrue("2" == self.connection.literal(2)) - + def test_literal_float(self): - self.assertTrue("3.1415" == self.connection.literal(3.1415)) - + self.assertEqual("3.1415e0", self.connection.literal(3.1415)) + def test_literal_string(self): self.assertTrue("'foo'" == self.connection.literal("foo")) - - -if __name__ == '__main__': - if test_MySQLdb.leak_test: - import gc - gc.enable() - gc.set_debug(gc.DEBUG_LEAK) - unittest.main() - diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py index 59626e386..5c34d40d1 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py @@ -1,30 +1,31 @@ -#!/usr/bin/env python -import dbapi20 -import unittest +from . import dbapi20 import pymysql from pymysql.tests import base -# compatibility: -if not hasattr(unittest, "expectedFailure"): - unittest.expectedFailure = lambda f: f class test_MySQLdb(dbapi20.DatabaseAPI20Test): driver = pymysql connect_args = () connect_kw_args = base.PyMySQLTestCase.databases[0].copy() - connect_kw_args.update(dict(read_default_file='~/.my.cnf', - charset='utf8', - sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")) - - def test_setoutputsize(self): pass - def test_setoutputsize_basic(self): pass - def test_nextset(self): pass + connect_kw_args.update( + dict( + read_default_file="~/.my.cnf", + charset="utf8", + sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL", + ) + ) + + def test_setoutputsize(self): + pass + + def test_setoutputsize_basic(self): + pass """The tests on fetchone and fetchall and rowcount bogusly test for an exception if the statement cannot return a result set. MySQL always returns a result set; it's just that some things return empty result sets.""" - + def test_fetchall(self): con = self._connect() try: @@ -40,40 +41,45 @@ def test_fetchall(self): # cursor.fetchall should raise an Error if called # after executing a a statement that cannot return rows -## self.assertRaises(self.driver.Error,cur.fetchall) + ## self.assertRaises(self.driver.Error,cur.fetchall) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,len(self.samples))) - self.assertEqual(len(rows),len(self.samples), - 'cursor.fetchall did not retrieve all rows' - ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) + self.assertEqual( + len(rows), + len(self.samples), + "cursor.fetchall did not retrieve all rows", + ) rows = [r[0] for r in rows] rows.sort() - for i in range(0,len(self.samples)): - self.assertEqual(rows[i],self.samples[i], - 'cursor.fetchall retrieved incorrect rows' + for i in range(0, len(self.samples)): + self.assertEqual( + rows[i], self.samples[i], "cursor.fetchall retrieved incorrect rows" ) rows = cur.fetchall() self.assertEqual( - len(rows),0, - 'cursor.fetchall should return an empty list if called ' - 'after the whole result set has been fetched' - ) - self.assertTrue(cur.rowcount in (-1,len(self.samples))) + len(rows), + 0, + "cursor.fetchall should return an empty list if called " + "after the whole result set has been fetched", + ) + self.assertTrue(cur.rowcount in (-1, len(self.samples))) self.executeDDL2(cur) - cur.execute('select name from %sbarflys' % self.table_prefix) + cur.execute("select name from %sbarflys" % self.table_prefix) rows = cur.fetchall() - self.assertTrue(cur.rowcount in (-1,0)) - self.assertEqual(len(rows),0, - 'cursor.fetchall should return an empty list if ' - 'a select query returns no rows' - ) - + self.assertTrue(cur.rowcount in (-1, 0)) + self.assertEqual( + len(rows), + 0, + "cursor.fetchall should return an empty list if " + "a select query returns no rows", + ) + finally: con.close() - + def test_fetchone(self): con = self._connect() try: @@ -81,39 +87,40 @@ def test_fetchone(self): # cursor.fetchone should raise an Error if called before # executing a select-type query - self.assertRaises(self.driver.Error,cur.fetchone) + self.assertRaises(self.driver.Error, cur.fetchone) # cursor.fetchone should raise an Error if called after - # executing a query that cannnot return rows + # executing a query that cannot return rows self.executeDDL1(cur) -## self.assertRaises(self.driver.Error,cur.fetchone) + ## self.assertRaises(self.driver.Error,cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) - self.assertEqual(cur.fetchone(),None, - 'cursor.fetchone should return None if a query retrieves ' - 'no rows' - ) - self.assertTrue(cur.rowcount in (-1,0)) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertEqual( + cur.fetchone(), + None, + "cursor.fetchone should return None if a query retrieves no rows", + ) + self.assertTrue(cur.rowcount in (-1, 0)) # cursor.fetchone should raise an Error if called after - # executing a query that cannnot return rows - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) -## self.assertRaises(self.driver.Error,cur.fetchone) + # executing a query that cannot return rows + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + ## self.assertRaises(self.driver.Error,cur.fetchone) - cur.execute('select name from %sbooze' % self.table_prefix) + cur.execute("select name from %sbooze" % self.table_prefix) r = cur.fetchone() - self.assertEqual(len(r),1, - 'cursor.fetchone should have retrieved a single row' - ) - self.assertEqual(r[0],'Victoria Bitter', - 'cursor.fetchone retrieved incorrect data' - ) -## self.assertEqual(cur.fetchone(),None, -## 'cursor.fetchone should return None if no more rows available' -## ) - self.assertTrue(cur.rowcount in (-1,1)) + self.assertEqual( + len(r), 1, "cursor.fetchone should have retrieved a single row" + ) + self.assertEqual( + r[0], "Victoria Bitter", "cursor.fetchone retrieved incorrect data" + ) + ## self.assertEqual(cur.fetchone(),None, + ## 'cursor.fetchone should return None if no more rows available' + ## ) + self.assertTrue(cur.rowcount in (-1, 1)) finally: con.close() @@ -123,88 +130,86 @@ def test_rowcount(self): try: cur = con.cursor() self.executeDDL1(cur) -## self.assertEqual(cur.rowcount,-1, -## 'cursor.rowcount should be -1 after executing no-result ' -## 'statements' -## ) - cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( - self.table_prefix - )) -## self.assertTrue(cur.rowcount in (-1,1), -## 'cursor.rowcount should == number or rows inserted, or ' -## 'set to -1 after executing an insert statement' -## ) + ## self.assertEqual(cur.rowcount,-1, + ## 'cursor.rowcount should be -1 after executing no-result ' + ## 'statements' + ## ) + cur.execute( + "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix) + ) + ## self.assertTrue(cur.rowcount in (-1,1), + ## 'cursor.rowcount should == number or rows inserted, or ' + ## 'set to -1 after executing an insert statement' + ## ) cur.execute("select name from %sbooze" % self.table_prefix) - self.assertTrue(cur.rowcount in (-1,1), - 'cursor.rowcount should == number of rows returned, or ' - 'set to -1 after executing a select statement' - ) + self.assertTrue( + cur.rowcount in (-1, 1), + "cursor.rowcount should == number of rows returned, or " + "set to -1 after executing a select statement", + ) self.executeDDL2(cur) -## self.assertEqual(cur.rowcount,-1, -## 'cursor.rowcount not being reset to -1 after executing ' -## 'no-result statements' -## ) + ## self.assertEqual(cur.rowcount,-1, + ## 'cursor.rowcount not being reset to -1 after executing ' + ## 'no-result statements' + ## ) finally: con.close() def test_callproc(self): - pass # performed in test_MySQL_capabilities - - def help_nextset_setUp(self,cur): - ''' Should create a procedure called deleteme - that returns two result sets, first the - number of rows in booze then "name from booze" - ''' - sql=""" + pass # performed in test_MySQL_capabilities + + def help_nextset_setUp(self, cur): + """Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + """ + sql = """ create procedure deleteme() begin select count(*) from %(tp)sbooze; select name from %(tp)sbooze; end - """ % dict(tp=self.table_prefix) + """ % dict( + tp=self.table_prefix + ) cur.execute(sql) - def help_nextset_tearDown(self,cur): - 'If cleaning up is needed after nextSetTest' + def help_nextset_tearDown(self, cur): + "If cleaning up is needed after nextSetTest" cur.execute("drop procedure deleteme") - @unittest.expectedFailure def test_nextset(self): - from warnings import warn con = self._connect() try: cur = con.cursor() - if not hasattr(cur,'nextset'): + if not hasattr(cur, "nextset"): return try: self.executeDDL1(cur) - sql=self._populate() + sql = self._populate() for sql in self._populate(): cur.execute(sql) self.help_nextset_setUp(cur) - cur.callproc('deleteme') - numberofrows=cur.fetchone() - assert numberofrows[0]== len(self.samples) + cur.callproc("deleteme") + numberofrows = cur.fetchone() + assert numberofrows[0] == len(self.samples) assert cur.nextset() - names=cur.fetchall() + names = cur.fetchall() assert len(names) == len(self.samples) - s=cur.nextset() + s = cur.nextset() if s: empty = cur.fetchall() - self.assertEquals(len(empty), 0, - "non-empty result set after other result sets") - #warn("Incompatibility: MySQL returns an empty result set for the CALL itself", + self.assertEqual( + len(empty), 0, "non-empty result set after other result sets" + ) + # warn("Incompatibility: MySQL returns an empty result set for the CALL itself", # Warning) - #assert s == None,'No more return sets, should return None' + # assert s == None,'No more return sets, should return None' finally: self.help_nextset_tearDown(cur) finally: con.close() - - -if __name__ == '__main__': - unittest.main() diff --git a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py index f49369cb4..1545fbb5e 100644 --- a/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +++ b/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py @@ -1,6 +1,7 @@ import unittest import pymysql + _mysql = pymysql from pymysql.constants import FIELD_TYPE from pymysql.tests import base @@ -25,7 +26,7 @@ class CoreModule(unittest.TestCase): def test_NULL(self): """Should have a NULL constant.""" - self.assertEqual(_mysql.NULL, 'NULL') + self.assertEqual(_mysql.NULL, "NULL") def test_version(self): """Version information sanity.""" @@ -54,37 +55,45 @@ def tearDown(self): def test_thread_id(self): tid = self.conn.thread_id() - self.assertTrue(isinstance(tid, int), - "thread_id didn't return an int.") + self.assertTrue( + isinstance(tid, int), "thread_id didn't return an integral value." + ) - self.assertRaises(TypeError, self.conn.thread_id, ('evil',), - "thread_id shouldn't accept arguments.") + self.assertRaises( + TypeError, + self.conn.thread_id, + ("evil",), + "thread_id shouldn't accept arguments.", + ) def test_affected_rows(self): - self.assertEquals(self.conn.affected_rows(), 0, - "Should return 0 before we do anything.") - + self.assertEqual( + self.conn.affected_rows(), 0, "Should return 0 before we do anything." + ) - #def test_debug(self): - ## FIXME Only actually tests if you lack SUPER - #self.assertRaises(pymysql.OperationalError, - #self.conn.dump_debug_info) + # def test_debug(self): + ## FIXME Only actually tests if you lack SUPER + # self.assertRaises(pymysql.OperationalError, + # self.conn.dump_debug_info) def test_charset_name(self): - self.assertTrue(isinstance(self.conn.character_set_name(), str), - "Should return a string.") + self.assertTrue( + isinstance(self.conn.character_set_name(), str), "Should return a string." + ) def test_host_info(self): - self.assertTrue(isinstance(self.conn.get_host_info(), str), - "Should return a string.") + assert isinstance(self.conn.get_host_info(), str), "should return a string" def test_proto_info(self): - self.assertTrue(isinstance(self.conn.get_proto_info(), int), - "Should return an int.") + self.assertTrue( + isinstance(self.conn.get_proto_info(), int), "Should return an int." + ) def test_server_info(self): - self.assertTrue(isinstance(self.conn.get_server_info(), basestring), - "Should return an str.") + self.assertTrue( + isinstance(self.conn.get_server_info(), str), "Should return an str." + ) + if __name__ == "__main__": unittest.main() diff --git a/pymysql/times.py b/pymysql/times.py index c47db09eb..4497dacf6 100644 --- a/pymysql/times.py +++ b/pymysql/times.py @@ -1,16 +1,20 @@ from time import localtime from datetime import date, datetime, time, timedelta + Date = date Time = time TimeDelta = timedelta Timestamp = datetime + def DateFromTicks(ticks): return date(*localtime(ticks)[:3]) + def TimeFromTicks(ticks): return time(*localtime(ticks)[3:6]) + def TimestampFromTicks(ticks): return datetime(*localtime(ticks)[:6]) diff --git a/pymysql/util.py b/pymysql/util.py deleted file mode 100644 index cc622e57b..000000000 --- a/pymysql/util.py +++ /dev/null @@ -1,19 +0,0 @@ -import struct - -def byte2int(b): - if isinstance(b, int): - return b - else: - return struct.unpack("!B", b)[0] - -def int2byte(i): - return struct.pack("!B", i) - -def join_bytes(bs): - if len(bs) == 0: - return "" - else: - rv = bs[0] - for b in bs[1:]: - rv += b - return rv diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..ee103916f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[project] +name = "PyMySQL" +description = "Pure Python MySQL Driver" +authors = [ + {name = "Inada Naoki", email = "songofacandy@gmail.com"}, + {name = "Yutaka Matsubara", email = "yutaka.matsubara@gmail.com"} +] +dependencies = [] + +requires-python = ">=3.7" +readme = "README.md" +license = {text = "MIT License"} +keywords = ["MySQL"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: Database", +] +dynamic = ["version"] + +[project.optional-dependencies] +"rsa" = [ + "cryptography" +] +"ed25519" = [ + "PyNaCl>=1.4.0" +] + +[project.urls] +"Project" = "https://github.com/PyMySQL/PyMySQL" +"Documentation" = "https://pymysql.readthedocs.io/" + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +namespaces = false +include = ["pymysql*"] +exclude = ["tests*", "pymysql.tests*"] + +[tool.setuptools.dynamic] +version = {attr = "pymysql.VERSION_STRING"} + +[tool.ruff] +exclude = [ + "pymysql/tests/thirdparty", +] + +[tool.ruff.lint] +ignore = ["E721"] + +[tool.pdm.dev-dependencies] +dev = [ + "pytest-cov>=4.0.0", +] diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..09e16da6b --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "dependencyDashboard": false +} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..140d37067 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +cryptography +PyNaCl>=1.4.0 +pytest +pytest-cov diff --git a/setup.py b/setup.py deleted file mode 100644 index b9c17fd73..000000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -try: - from setuptools import setup, Command -except ImportError: - from distutils.core import setup, Command - -import sys - -class TestCommand(Command): - user_options = [ ] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - ''' - Finds all the tests modules in tests/, and runs them. - ''' - from pymysql import tests - import unittest - unittest.main(tests, argv=sys.argv[:1]) - -version_tuple = __import__('pymysql').VERSION - -if version_tuple[2] is not None: - version = "%d.%d_%s" % version_tuple -else: - version = "%d.%d" % version_tuple[:2] - -setup( - name = "PyMySQL", - version = version, - url = 'https://github.com/petehunt/PyMySQL/', - author = 'yutaka.matsubara', - author_email = 'yutaka.matsubara@gmail.com', - maintainer = 'Pete Hunt', - maintainer_email = 'floydophone@gmail.com', - description = 'Pure Python MySQL Driver ', - license = "MIT", - packages = ['pymysql', 'pymysql.constants', 'pymysql.tests'], - cmdclass = {'test': TestCommand}, -) diff --git a/setup.py.py3k.patch b/setup.py.py3k.patch deleted file mode 100644 index 6b4799409..000000000 --- a/setup.py.py3k.patch +++ /dev/null @@ -1,4 +0,0 @@ -15c15 -< name = "PyMySQL", ---- -> name = "PyMySQL3", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 000000000..d7a0e82fc --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,120 @@ +"""Test for auth methods supported by MySQL 8""" + +import os +import pymysql + +# pymysql.connections.DEBUG = True +# pymysql._auth.DEBUG = True + +host = "127.0.0.1" +port = 3306 + +ca = os.path.expanduser("~/ca.pem") +ssl = {"ca": ca, "check_hostname": False} + +pass_sha256 = "pass_sha256_01234567890123456789" +pass_caching_sha2 = "pass_caching_sha2_01234567890123456789" + + +def test_sha256_no_password(): + con = pymysql.connect(user="nopass_sha256", host=host, port=port, ssl=None) + con.close() + + +def test_sha256_no_passowrd_ssl(): + con = pymysql.connect(user="nopass_sha256", host=host, port=port, ssl=ssl) + con.close() + + +def test_sha256_password(): + con = pymysql.connect( + user="user_sha256", password=pass_sha256, host=host, port=port, ssl=None + ) + con.close() + + +def test_sha256_password_ssl(): + con = pymysql.connect( + user="user_sha256", password=pass_sha256, host=host, port=port, ssl=ssl + ) + con.close() + + +def test_caching_sha2_no_password(): + con = pymysql.connect(user="nopass_caching_sha2", host=host, port=port, ssl=None) + con.close() + + +def test_caching_sha2_no_password_ssl(): + con = pymysql.connect(user="nopass_caching_sha2", host=host, port=port, ssl=ssl) + con.close() + + +def test_caching_sha2_password(): + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=None, + ) + con.close() + + # Fast path of caching sha2 + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=None, + ) + con.query("FLUSH PRIVILEGES") + con.close() + + # Fast path after auth_switch_request + pymysql.connections._DEFAULT_AUTH_PLUGIN = "mysql_native_password" + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, + ) + con.query("FLUSH PRIVILEGES") + con.close() + pymysql.connections._DEFAULT_AUTH_PLUGIN = None + + +def test_caching_sha2_password_ssl(): + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, + ) + con.close() + + # Fast path of caching sha2 + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, + ) + con.query("FLUSH PRIVILEGES") + con.close() + + # Fast path after auth_switch_request + pymysql.connections._DEFAULT_AUTH_PLUGIN = "mysql_native_password" + con = pymysql.connect( + user="user_caching_sha2", + password=pass_caching_sha2, + host=host, + port=port, + ssl=ssl, + ) + con.query("FLUSH PRIVILEGES") + con.close() + pymysql.connections._DEFAULT_AUTH_PLUGIN = None