From b7479f6a3193e0082e1150d4172956c7cf588537 Mon Sep 17 00:00:00 2001 From: Dolev Date: Tue, 11 Nov 2025 21:04:08 +0200 Subject: [PATCH 1/4] feat: add password_creator for dynamic credentials (e.g. RDS IAM auth tokens) --- asyncmy/connection.pyx | 13 +++++++++++++ conftest.py | 7 ++++++- setup.py | 27 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 setup.py diff --git a/asyncmy/connection.pyx b/asyncmy/connection.pyx index 3d1d7e7..ad9f3d3 100644 --- a/asyncmy/connection.pyx +++ b/asyncmy/connection.pyx @@ -108,6 +108,9 @@ class Connection: :param host: Host where the database server is located. :param user: Username to log in as. :param password: Password to use. + :param password_creator: + Optional callable or coroutine that returns a password string + every time a new connection is established. :param database: Database to use, None to not use a particular one. :param port: MySQL port to use, default is usually OK. (default: 3306) :param unix_socket: Use a unix socket rather than TCP/IP. @@ -152,6 +155,7 @@ class Connection: *, user=None, # The first four arguments is based on DB-API 2.0 recommendation. password="", + password_creator=None, host=None, database=None, unix_socket=None, @@ -240,6 +244,7 @@ class Connection: raise ValueError("port should be of type int") self._user = user or DEFAULT_USER self._password = password or b"" + self._password_creator = password_creator if isinstance(self._password, str): self._password = self._password.encode("latin1") self._db = database @@ -549,6 +554,12 @@ class Connection: return self._reader, self._writer try: + if self._password_creator is not None: + new_pw = self._password_creator() + if asyncio.iscoroutine(new_pw): + new_pw = await new_pw + self._password = new_pw.encode("latin1") + if self._unix_socket: self._reader, self._writer = await asyncio.wait_for(asyncio.open_unix_connection(self._unix_socket), timeout=self._connect_timeout, ) @@ -1281,6 +1292,7 @@ class LoadLocalFile: def connect(user=None, password="", + password_creator=None, host=None, database=None, unix_socket=None, @@ -1310,6 +1322,7 @@ def connect(user=None, coro = _connect( user=user, password=password, + password_creator=password_creator, host=host, database=database, unix_socket=unix_socket, diff --git a/conftest.py b/conftest.py index 00e30e9..dd623d9 100644 --- a/conftest.py +++ b/conftest.py @@ -7,11 +7,16 @@ from asyncmy import connect from asyncmy.cursors import DictCursor +def mysql_password_creator(): + """Return the MySQL password dynamically""" + return os.getenv("MYSQL_PASS") or "123456" + + connection_kwargs = dict( host="127.0.0.1", port=3306, user="root", - password=os.getenv("MYSQL_PASS") or "123456", + password_creator=mysql_password_creator, # Using password_creator instead of password echo=True, ) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..97df78e --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = \ +['asyncmy', 'asyncmy.constants', 'asyncmy.replication'] + +package_data = \ +{'': ['*']} + +setup_kwargs = { + 'name': 'asyncmy', + 'version': '0.2.10', + 'description': 'A fast asyncio MySQL driver', + 'long_description': '# asyncmy - A fast asyncio MySQL/MariaDB driver\n\n[![image](https://img.shields.io/pypi/v/asyncmy.svg?style=flat)](https://pypi.python.org/pypi/asyncmy)\n[![image](https://img.shields.io/github/license/long2ice/asyncmy)](https://github.com/long2ice/asyncmy)\n[![pypi](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml)\n[![ci](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml)\n\n## Introduction\n\n`asyncmy` is a fast asyncio MySQL/MariaDB driver, which reuse most of [pymysql](https://github.com/PyMySQL/PyMySQL)\nand [aiomysql](https://github.com/aio-libs/aiomysql) but rewrite core protocol with [cython](https://cython.org/) to\nspeedup.\n\n## Features\n\n- API compatible with [aiomysql](https://github.com/aio-libs/aiomysql).\n- Faster by [cython](https://cython.org/).\n- MySQL replication protocol support with `asyncio`.\n- Tested both MySQL and MariaDB in [CI](https://github.com/long2ice/asyncmy/blob/dev/.github/workflows/ci.yml).\n\n## Benchmark\n\nThe result comes from [benchmark](./benchmark).\n\n> The device is iMac Pro(2017) i9 3.6GHz 48G and MySQL version is 8.0.26.\n\n![benchmark](./images/benchmark.png)\n\n### Conclusion\n\n- There is no doubt that `mysqlclient` is the fastest MySQL driver.\n- All kinds of drivers have a small gap except `select`.\n- `asyncio` could enhance `insert`.\n- `asyncmy` performs remarkable when compared to other drivers.\n\n## Install\n\n```shell\npip install asyncmy\n```\n\n### Installing on Windows\n\nTo install asyncmy on Windows, you need to install the tools needed to build it.\n\n1. Download *Microsoft C++ Build Tools* from https://visualstudio.microsoft.com/visual-cpp-build-tools/\n2. Run CMD as Admin (not required but recommended) and navigate to the folder when your installer is downloaded\n3. Installer executable should look like this `vs_buildtools__XXXXXXXXX.XXXXXXXXXX.exe`, it will be easier if you rename\n it to just `vs_buildtools.exe`\n4. Run this command (Make sure you have about 5-6GB of free storage)\n\n```shell\nvs_buildtools.exe --norestart --passive --downloadThenInstall --includeRecommended --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools\n```\n\n5. Wait until the installation is finished\n6. After installation will finish, restart your computer\n7. Install asyncmy via PIP\n\n```shell\npip install asyncmy\n```\n\nNow you can uninstall previously installed tools.\n\n## Usage\n\n### Use `connect`\n\n`asyncmy` provides a way to connect to MySQL database with simple factory function `asyncmy.connect()`. Use this\nfunction if you want just one connection to the database, consider connection pool for multiple connections.\n\n```py\nimport asyncio\nimport os\n\nfrom asyncmy import connect\nfrom asyncmy.cursors import DictCursor\n\n\nasync def run():\n conn = await connect(user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD", ""))\n async with conn.cursor(cursor=DictCursor) as cursor:\n await cursor.execute("CREATE DATABASE IF NOT EXISTS test")\n await cursor.execute("""\n """\nCREATE TABLE IF NOT EXISTS test.`asyncmy` (\n `id` int primary key AUTO_INCREMENT,\n `decimal` decimal(10, 2),\n `date` date,\n `datetime` datetime,\n `float` float,\n `string` varchar(200),\n `tinyint` tinyint\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci\n """.strip()\n )\n await conn.ensure_closed()\n\n\nif __name__ == "__main__":\n asyncio.run(run())\n```\n\n### Use `pool`\n\n`asyncmy` provides connection pool as well as plain Connection objects.\n\n```py\nimport asyncmy\nimport asyncio\n\n\nasync def run():\n pool = await asyncmy.create_pool()\n async with pool.acquire() as conn:\n async with conn.cursor() as cursor:\n await cursor.execute("SELECT 1")\n ret = await cursor.fetchone()\n assert ret == (1,)\n pool.close()\n await pool.wait_closed()\n\nif __name__ == \'__main__\':\n asyncio.run(run())\n```\n\n## Replication\n\n`asyncmy` supports MySQL replication protocol\nlike [python-mysql-replication](https://github.com/noplay/python-mysql-replication), but powered by `asyncio`.\n\n```py\nfrom asyncmy import connect\nfrom asyncmy.replication import BinLogStream\nimport asyncio\n\n\nasync def run():\n conn = await connect()\n ctl_conn = await connect()\n\n stream = BinLogStream(\n conn,\n ctl_conn,\n 1,\n master_log_file="binlog.000172",\n master_log_position=2235312,\n resume_stream=True,\n blocking=True,\n )\n async for event in stream:\n print(event)\n await conn.ensure_closed()\n await ctl_conn.ensure_closed()\n\n\nif __name__ == \'__main__\':\n asyncio.run(run())\n```\n\n## ThanksTo\n\n> asyncmy is build on top of these awesome projects.\n\n- [pymysql](https://github/pymysql/PyMySQL), a pure python MySQL client.\n- [aiomysql](https://github.com/aio-libs/aiomysql), a library for accessing a MySQL database from the asyncio.\n- [python-mysql-replication](https://github.com/noplay/python-mysql-replication), pure Python Implementation of MySQL\n replication protocol build on top of PyMYSQL.\n\n## License\n\nThis project is licensed under the [Apache-2.0](./LICENSE) License.\n', + 'author': 'long2ice', + 'author_email': 'long2ice@gmail.com', + 'maintainer': 'None', + 'maintainer_email': 'None', + 'url': 'https://github.com/long2ice/asyncmy', + 'packages': packages, + 'package_data': package_data, + 'python_requires': '>=3.9', +} +from build import * +build(setup_kwargs) + +setup(**setup_kwargs) From 88c5a7d732af5772b69ff34ea7e38c404691e0a5 Mon Sep 17 00:00:00 2001 From: Dolev Date: Tue, 11 Nov 2025 21:36:06 +0200 Subject: [PATCH 2/4] Add password_creator parameter to allow dynamic password fetching from external sources like secrets managers. Supports both sync and async callables. --- conftest.py | 29 +++++++++++++++++++---------- tests/test_connection.py | 5 ++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/conftest.py b/conftest.py index dd623d9..2574c60 100644 --- a/conftest.py +++ b/conftest.py @@ -12,13 +12,22 @@ def mysql_password_creator(): return os.getenv("MYSQL_PASS") or "123456" -connection_kwargs = dict( - host="127.0.0.1", - port=3306, - user="root", - password_creator=mysql_password_creator, # Using password_creator instead of password - echo=True, -) +@pytest_asyncio.fixture(params=["static", "creator"], scope="session") +def connection_kwargs(request): + """Provide connection args for both static and dynamic password modes.""" + base = dict( + host="127.0.0.1", + port=3306, + user="root", + echo=True, + ) + + if request.param == "static": + base["password"] = os.getenv("MYSQL_PASS") or "123456" + else: + base["password_creator"] = mysql_password_creator + + return base @pytest_asyncio.fixture(scope="session") @@ -35,7 +44,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") -async def connection(): +async def connection(connection_kwargs): # Add connection_kwargs as parameter conn = await connect(**connection_kwargs) yield conn await conn.ensure_closed() @@ -68,8 +77,8 @@ async def truncate_table(connection): @pytest_asyncio.fixture(scope="session") -async def pool(): +async def pool(connection_kwargs): # Add connection_kwargs as parameter pool = await asyncmy.create_pool(**connection_kwargs) yield pool pool.close() - await pool.wait_closed() + await pool.wait_closed() \ No newline at end of file diff --git a/tests/test_connection.py b/tests/test_connection.py index c677951..628bcf5 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -4,11 +4,10 @@ from asyncmy.connection import Connection from asyncmy.errors import OperationalError -from conftest import connection_kwargs @pytest.mark.asyncio -async def test_connect(): +async def test_connect(connection_kwargs): connection = Connection(**connection_kwargs) await connection.connect() assert connection._connected @@ -22,7 +21,7 @@ async def test_connect(): @pytest.mark.asyncio -async def test_read_timeout(): +async def test_read_timeout(connection_kwargs): with pytest.raises(OperationalError): connection = Connection(read_timeout=1, **connection_kwargs) await connection.connect() From e4d9d4bbf4c041de748ab01014c4064106c9892d Mon Sep 17 00:00:00 2001 From: Dolev Date: Tue, 11 Nov 2025 21:42:31 +0200 Subject: [PATCH 3/4] Removed setup.py. --- setup.py | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 97df78e..0000000 --- a/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from setuptools import setup - -packages = \ -['asyncmy', 'asyncmy.constants', 'asyncmy.replication'] - -package_data = \ -{'': ['*']} - -setup_kwargs = { - 'name': 'asyncmy', - 'version': '0.2.10', - 'description': 'A fast asyncio MySQL driver', - 'long_description': '# asyncmy - A fast asyncio MySQL/MariaDB driver\n\n[![image](https://img.shields.io/pypi/v/asyncmy.svg?style=flat)](https://pypi.python.org/pypi/asyncmy)\n[![image](https://img.shields.io/github/license/long2ice/asyncmy)](https://github.com/long2ice/asyncmy)\n[![pypi](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/pypi.yml)\n[![ci](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml/badge.svg)](https://github.com/long2ice/asyncmy/actions/workflows/ci.yml)\n\n## Introduction\n\n`asyncmy` is a fast asyncio MySQL/MariaDB driver, which reuse most of [pymysql](https://github.com/PyMySQL/PyMySQL)\nand [aiomysql](https://github.com/aio-libs/aiomysql) but rewrite core protocol with [cython](https://cython.org/) to\nspeedup.\n\n## Features\n\n- API compatible with [aiomysql](https://github.com/aio-libs/aiomysql).\n- Faster by [cython](https://cython.org/).\n- MySQL replication protocol support with `asyncio`.\n- Tested both MySQL and MariaDB in [CI](https://github.com/long2ice/asyncmy/blob/dev/.github/workflows/ci.yml).\n\n## Benchmark\n\nThe result comes from [benchmark](./benchmark).\n\n> The device is iMac Pro(2017) i9 3.6GHz 48G and MySQL version is 8.0.26.\n\n![benchmark](./images/benchmark.png)\n\n### Conclusion\n\n- There is no doubt that `mysqlclient` is the fastest MySQL driver.\n- All kinds of drivers have a small gap except `select`.\n- `asyncio` could enhance `insert`.\n- `asyncmy` performs remarkable when compared to other drivers.\n\n## Install\n\n```shell\npip install asyncmy\n```\n\n### Installing on Windows\n\nTo install asyncmy on Windows, you need to install the tools needed to build it.\n\n1. Download *Microsoft C++ Build Tools* from https://visualstudio.microsoft.com/visual-cpp-build-tools/\n2. Run CMD as Admin (not required but recommended) and navigate to the folder when your installer is downloaded\n3. Installer executable should look like this `vs_buildtools__XXXXXXXXX.XXXXXXXXXX.exe`, it will be easier if you rename\n it to just `vs_buildtools.exe`\n4. Run this command (Make sure you have about 5-6GB of free storage)\n\n```shell\nvs_buildtools.exe --norestart --passive --downloadThenInstall --includeRecommended --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools\n```\n\n5. Wait until the installation is finished\n6. After installation will finish, restart your computer\n7. Install asyncmy via PIP\n\n```shell\npip install asyncmy\n```\n\nNow you can uninstall previously installed tools.\n\n## Usage\n\n### Use `connect`\n\n`asyncmy` provides a way to connect to MySQL database with simple factory function `asyncmy.connect()`. Use this\nfunction if you want just one connection to the database, consider connection pool for multiple connections.\n\n```py\nimport asyncio\nimport os\n\nfrom asyncmy import connect\nfrom asyncmy.cursors import DictCursor\n\n\nasync def run():\n conn = await connect(user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD", ""))\n async with conn.cursor(cursor=DictCursor) as cursor:\n await cursor.execute("CREATE DATABASE IF NOT EXISTS test")\n await cursor.execute("""\n """\nCREATE TABLE IF NOT EXISTS test.`asyncmy` (\n `id` int primary key AUTO_INCREMENT,\n `decimal` decimal(10, 2),\n `date` date,\n `datetime` datetime,\n `float` float,\n `string` varchar(200),\n `tinyint` tinyint\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci\n """.strip()\n )\n await conn.ensure_closed()\n\n\nif __name__ == "__main__":\n asyncio.run(run())\n```\n\n### Use `pool`\n\n`asyncmy` provides connection pool as well as plain Connection objects.\n\n```py\nimport asyncmy\nimport asyncio\n\n\nasync def run():\n pool = await asyncmy.create_pool()\n async with pool.acquire() as conn:\n async with conn.cursor() as cursor:\n await cursor.execute("SELECT 1")\n ret = await cursor.fetchone()\n assert ret == (1,)\n pool.close()\n await pool.wait_closed()\n\nif __name__ == \'__main__\':\n asyncio.run(run())\n```\n\n## Replication\n\n`asyncmy` supports MySQL replication protocol\nlike [python-mysql-replication](https://github.com/noplay/python-mysql-replication), but powered by `asyncio`.\n\n```py\nfrom asyncmy import connect\nfrom asyncmy.replication import BinLogStream\nimport asyncio\n\n\nasync def run():\n conn = await connect()\n ctl_conn = await connect()\n\n stream = BinLogStream(\n conn,\n ctl_conn,\n 1,\n master_log_file="binlog.000172",\n master_log_position=2235312,\n resume_stream=True,\n blocking=True,\n )\n async for event in stream:\n print(event)\n await conn.ensure_closed()\n await ctl_conn.ensure_closed()\n\n\nif __name__ == \'__main__\':\n asyncio.run(run())\n```\n\n## ThanksTo\n\n> asyncmy is build on top of these awesome projects.\n\n- [pymysql](https://github/pymysql/PyMySQL), a pure python MySQL client.\n- [aiomysql](https://github.com/aio-libs/aiomysql), a library for accessing a MySQL database from the asyncio.\n- [python-mysql-replication](https://github.com/noplay/python-mysql-replication), pure Python Implementation of MySQL\n replication protocol build on top of PyMYSQL.\n\n## License\n\nThis project is licensed under the [Apache-2.0](./LICENSE) License.\n', - 'author': 'long2ice', - 'author_email': 'long2ice@gmail.com', - 'maintainer': 'None', - 'maintainer_email': 'None', - 'url': 'https://github.com/long2ice/asyncmy', - 'packages': packages, - 'package_data': package_data, - 'python_requires': '>=3.9', -} -from build import * -build(setup_kwargs) - -setup(**setup_kwargs) From 32b0d470d2e02bf685926bbd351a0fc2e8eb85ae Mon Sep 17 00:00:00 2001 From: Dolev Date: Tue, 11 Nov 2025 21:43:31 +0200 Subject: [PATCH 4/4] Removed comments. --- conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 2574c60..6216ad1 100644 --- a/conftest.py +++ b/conftest.py @@ -44,7 +44,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") -async def connection(connection_kwargs): # Add connection_kwargs as parameter +async def connection(connection_kwargs): conn = await connect(**connection_kwargs) yield conn await conn.ensure_closed() @@ -77,7 +77,7 @@ async def truncate_table(connection): @pytest_asyncio.fixture(scope="session") -async def pool(connection_kwargs): # Add connection_kwargs as parameter +async def pool(connection_kwargs): pool = await asyncmy.create_pool(**connection_kwargs) yield pool pool.close()