pymysql.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # dialects/mysql/pymysql.py
  2. # Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. r"""
  8. .. dialect:: mysql+pymysql
  9. :name: PyMySQL
  10. :dbapi: pymysql
  11. :connectstring: mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
  12. :url: https://pymysql.readthedocs.io/
  13. Unicode
  14. -------
  15. Please see :ref:`mysql_unicode` for current recommendations on unicode
  16. handling.
  17. .. _pymysql_ssl:
  18. SSL Connections
  19. ------------------
  20. The PyMySQL DBAPI accepts the same SSL arguments as that of MySQLdb,
  21. described at :ref:`mysqldb_ssl`. See that section for additional examples.
  22. If the server uses an automatically-generated certificate that is self-signed
  23. or does not match the host name (as seen from the client), it may also be
  24. necessary to indicate ``ssl_check_hostname=false`` in PyMySQL::
  25. connection_uri = (
  26. "mysql+pymysql://scott:tiger@192.168.0.134/test"
  27. "?ssl_ca=/home/gord/client-ssl/ca.pem"
  28. "&ssl_cert=/home/gord/client-ssl/client-cert.pem"
  29. "&ssl_key=/home/gord/client-ssl/client-key.pem"
  30. "&ssl_check_hostname=false"
  31. )
  32. MySQL-Python Compatibility
  33. --------------------------
  34. The pymysql DBAPI is a pure Python port of the MySQL-python (MySQLdb) driver,
  35. and targets 100% compatibility. Most behavioral notes for MySQL-python apply
  36. to the pymysql driver as well.
  37. """ # noqa
  38. from __future__ import annotations
  39. from typing import Any
  40. from typing import Dict
  41. from typing import Optional
  42. from typing import TYPE_CHECKING
  43. from typing import Union
  44. from .mysqldb import MySQLDialect_mysqldb
  45. from ...util import langhelpers
  46. from ...util.typing import Literal
  47. if TYPE_CHECKING:
  48. from ...engine.interfaces import ConnectArgsType
  49. from ...engine.interfaces import DBAPIConnection
  50. from ...engine.interfaces import DBAPICursor
  51. from ...engine.interfaces import DBAPIModule
  52. from ...engine.interfaces import PoolProxiedConnection
  53. from ...engine.url import URL
  54. class MySQLDialect_pymysql(MySQLDialect_mysqldb):
  55. driver = "pymysql"
  56. supports_statement_cache = True
  57. description_encoding = None
  58. @langhelpers.memoized_property
  59. def supports_server_side_cursors(self) -> bool:
  60. try:
  61. cursors = __import__("pymysql.cursors").cursors
  62. self._sscursor = cursors.SSCursor
  63. return True
  64. except (ImportError, AttributeError):
  65. return False
  66. @classmethod
  67. def import_dbapi(cls) -> DBAPIModule:
  68. return __import__("pymysql")
  69. @langhelpers.memoized_property
  70. def _send_false_to_ping(self) -> bool:
  71. """determine if pymysql has deprecated, changed the default of,
  72. or removed the 'reconnect' argument of connection.ping().
  73. See #10492 and
  74. https://github.com/PyMySQL/mysqlclient/discussions/651#discussioncomment-7308971
  75. for background.
  76. """ # noqa: E501
  77. try:
  78. Connection = __import__(
  79. "pymysql.connections"
  80. ).connections.Connection
  81. except (ImportError, AttributeError):
  82. return True
  83. else:
  84. insp = langhelpers.get_callable_argspec(Connection.ping)
  85. try:
  86. reconnect_arg = insp.args[1]
  87. except IndexError:
  88. return False
  89. else:
  90. return reconnect_arg == "reconnect" and (
  91. not insp.defaults or insp.defaults[0] is not False
  92. )
  93. def do_ping(self, dbapi_connection: DBAPIConnection) -> Literal[True]:
  94. if self._send_false_to_ping:
  95. dbapi_connection.ping(False)
  96. else:
  97. dbapi_connection.ping()
  98. return True
  99. def create_connect_args(
  100. self, url: URL, _translate_args: Optional[Dict[str, Any]] = None
  101. ) -> ConnectArgsType:
  102. if _translate_args is None:
  103. _translate_args = dict(username="user")
  104. return super().create_connect_args(
  105. url, _translate_args=_translate_args
  106. )
  107. def is_disconnect(
  108. self,
  109. e: DBAPIModule.Error,
  110. connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
  111. cursor: Optional[DBAPICursor],
  112. ) -> bool:
  113. if super().is_disconnect(e, connection, cursor):
  114. return True
  115. elif isinstance(e, self.loaded_dbapi.Error):
  116. str_e = str(e).lower()
  117. return (
  118. "already closed" in str_e or "connection was killed" in str_e
  119. )
  120. else:
  121. return False
  122. def _extract_error_code(self, exception: BaseException) -> Any:
  123. if isinstance(exception.args[0], Exception):
  124. exception = exception.args[0]
  125. return exception.args[0]
  126. dialect = MySQLDialect_pymysql