Skip to content

Commit 551ab21

Browse files
AhmadMasryclaude
andcommitted
refactor(sqlalchemy): adopt SA dialect+driver URL convention
Switch the wrapper's SQLAlchemy dialect registration from a parallel top-level dialect name (aws_wrapper_postgresql+psycopg, aws_wrapper_mysql+ mysqlconnector, ...) to SA's idiomatic <dialect>+<driver> form, so the wrapper plugs in as a driver under the stock postgresql/mysql dialects. This matches the AWS-approved ORM PR aws#1224 convention and removes the two-conventions-for-one-wrapper inconsistency the merge would otherwise leave. Done now because the branch is unreleased -- no public URL is broken. Entry points (3, was 8): - postgresql.aws_wrapper_psycopg -> sync AND async - mysql.aws_wrapper_mysqlconnector -> sync - mysql.aws_wrapper_aiomysql -> async PostgreSQL serves both sync and async from ONE URL (postgresql+aws_wrapper_psycopg://): psycopg3 is a single DBAPI that does both, so AwsWrapperPGPsycopgDialect implements get_async_dialect_cls to return the async dialect. create_async_engine drives URL.get_dialect(_is_async=True) -> that hook (verified against SA 2.0.49); create_engine uses the sync class. Mirrors stock postgresql+psycopg. MySQL needs two driver names because its sync (mysql-connector-python) and async (aiomysql) paths are different DBAPIs that cannot share a URL. The async name drops the redundant _async suffix (aiomysql is async-only), mirroring stock mysql+aiomysql. Dialect classes: driver attrs updated (aws_wrapper_psycopg / aws_wrapper_mysqlconnector / aws_wrapper_aiomysql); PG async keeps the sync driver name since it is reached via the hook, not a distinct URL. Updated: unit tests (test_sqlalchemy_dialects, test_aio_sqlalchemy_dialect, test_aio_aiomysql) -- the async-PG registry-key tests become get_async_dialect_cls resolution tests; integration helpers + test URLs; SqlAlchemySupport.md (rewrote the Naming section), IntegrationTests.md, the *AsyncFailover examples, and dialect/submodule docstrings. Note: editable installs must `poetry install` to refresh entry_points.txt after this change (stale metadata otherwise resolves the old names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 635d860 commit 551ab21

18 files changed

Lines changed: 153 additions & 108 deletions

aws_advanced_python_wrapper/aio/aiomysql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
1717
Async counterpart of :mod:`aws_advanced_python_wrapper.mysql_connector`.
1818
Enables SQLAlchemy's ``create_async_engine`` with the custom dialect
19-
``aws_wrapper_mysql+aiomysql_async`` registered in Task 2-C.
19+
``mysql+aws_wrapper_aiomysql``.
2020
2121
Module-level attributes are populated via :func:`_dbapi.install`;
2222
PEP 562 ``__getattr__`` forwards missing attrs to the real

aws_advanced_python_wrapper/aio/psycopg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from aws_advanced_python_wrapper.aio.psycopg import connect
2323
2424
engine = create_async_engine(
25-
"postgresql+psycopg_async://",
25+
"postgresql+aws_wrapper_psycopg://",
2626
async_creator=lambda: connect(
2727
"host=... user=... dbname=...",
2828
wrapper_dialect="aurora-pg",

aws_advanced_python_wrapper/sqlalchemy_dialects/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
AWS Advanced Python Wrapper.
1717
1818
Users should prefer the SA dialect registry
19-
(``create_engine("aws-wrapper-postgresql+psycopg://...")``) over importing
19+
(``create_engine("postgresql+aws_wrapper_psycopg://...")``) over importing
2020
these classes directly.
2121
"""

aws_advanced_python_wrapper/sqlalchemy_dialects/mysql.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
"""MySQL SQLAlchemy dialect bound to the AWS Advanced Python Wrapper.
1616
17-
Registered as ``aws-wrapper-mysql`` / ``aws-wrapper-mysql+mysqlconnector``
18-
via a pyproject entry-point. Subclasses SA's standard
19-
MySQLDialect_mysqlconnector and only swaps the DBAPI module to
17+
Registered as ``mysql.aws_wrapper_mysqlconnector`` via a pyproject
18+
entry-point (URL ``mysql+aws_wrapper_mysqlconnector://``). Subclasses SA's
19+
standard MySQLDialect_mysqlconnector and only swaps the DBAPI module to
2020
:mod:`aws_advanced_python_wrapper.mysql_connector`, which routes connect()
2121
through the wrapper's plugin pipeline.
2222
"""
@@ -36,7 +36,7 @@ class AwsWrapperMySQLConnectorDialect(
3636
_FailoverSuccessRewrapMixin, MySQLDialect_mysqlconnector):
3737
"""SQLAlchemy dialect that uses the AWS Advanced Python Wrapper as its DBAPI."""
3838

39-
driver = "mysqlconnector"
39+
driver = "aws_wrapper_mysqlconnector"
4040
supports_statement_cache = True
4141

4242
# See _FailoverSuccessRewrapMixin / sqlalchemy_dialects/pg.py for the

aws_advanced_python_wrapper/sqlalchemy_dialects/mysql_async.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,21 @@
1414

1515
"""Async MySQL SQLAlchemy dialect bound to the AWS Advanced Python Wrapper.
1616
17-
Registered as ``aws_wrapper_mysql_async`` /
18-
``aws_wrapper_mysql+aiomysql_async`` via pyproject entry-points.
19-
Subclasses SA's standard ``MySQLDialect_aiomysql`` and swaps the DBAPI
20-
to an adapter that routes ``connect()`` through the async plugin pipeline
21-
while preserving SA's ``AsyncAdapt_aiomysql_connection`` greenlet-bridge
22-
wrapper that the async engine expects.
17+
Registered as ``mysql.aws_wrapper_aiomysql`` via a pyproject entry-point
18+
(URL ``mysql+aws_wrapper_aiomysql://``). aiomysql is an async-only DBAPI, so
19+
unlike PG this needs a distinct driver name from the sync
20+
``mysql+aws_wrapper_mysqlconnector`` (the two are different DBAPIs and cannot
21+
share one URL). Subclasses SA's standard ``MySQLDialect_aiomysql`` and swaps
22+
the DBAPI to an adapter that routes ``connect()`` through the async plugin
23+
pipeline while preserving SA's ``AsyncAdapt_aiomysql_connection``
24+
greenlet-bridge wrapper that the async engine expects.
2325
2426
Example::
2527
2628
from sqlalchemy.ext.asyncio import create_async_engine
2729
2830
engine = create_async_engine(
29-
"aws_wrapper_mysql+aiomysql_async://user:pwd@"
31+
"mysql+aws_wrapper_aiomysql://user:pwd@"
3032
"database.cluster-xyz.us-east-1.rds.amazonaws.com:3306/db"
3133
"?wrapper_dialect=aurora-mysql&wrapper_plugins=failover"
3234
)
@@ -93,7 +95,7 @@ class AwsWrapperMySQLAiomysqlAsyncDialect(
9395
_AsyncFailoverSuccessRewrapMixin, MySQLDialect_aiomysql):
9496
"""Async SQLAlchemy dialect that uses the AWS Advanced Python Wrapper as its DBAPI."""
9597

96-
driver = "aiomysql_async"
98+
driver = "aws_wrapper_aiomysql"
9799
supports_statement_cache = True
98100
is_async = True
99101

aws_advanced_python_wrapper/sqlalchemy_dialects/pg.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
"""PostgreSQL SQLAlchemy dialect bound to the AWS Advanced Python Wrapper.
1616
17-
Registered as ``aws-wrapper-postgresql`` / ``aws-wrapper-postgresql+psycopg``
18-
via a pyproject entry-point. Subclasses SA's standard PGDialect_psycopg and
19-
only swaps the DBAPI module to :mod:`aws_advanced_python_wrapper.psycopg`,
20-
which routes connect() through the wrapper's plugin pipeline.
17+
Registered as ``postgresql.aws_wrapper_psycopg`` via a pyproject entry-point
18+
(URL ``postgresql+aws_wrapper_psycopg://``). The same URL serves both sync
19+
and async: this class implements ``get_async_dialect_cls`` so
20+
``create_async_engine`` swaps in the async dialect. Subclasses SA's standard
21+
PGDialect_psycopg and only swaps the DBAPI module to
22+
:mod:`aws_advanced_python_wrapper.psycopg`, which routes connect() through
23+
the wrapper's plugin pipeline.
2124
"""
2225

2326
from __future__ import annotations
@@ -50,7 +53,7 @@ class AwsWrapperPGPsycopgDialect(_FailoverSuccessRewrapMixin, PGDialect_psycopg)
5053
psycopg. Current overrides: ``_type_info_fetch``.
5154
"""
5255

53-
driver = "psycopg"
56+
driver = "aws_wrapper_psycopg"
5457
supports_statement_cache = True
5558

5659
# See _FailoverSuccessRewrapMixin. SA's classifier checks
@@ -69,6 +72,21 @@ def import_dbapi(cls):
6972
import aws_advanced_python_wrapper.psycopg as dbapi
7073
return dbapi
7174

75+
@classmethod
76+
def get_async_dialect_cls(cls, url):
77+
# psycopg3 is a single DBAPI that does both sync and async, so a
78+
# single ``postgresql+aws_wrapper_psycopg://`` URL serves both. SA
79+
# selects async purely by which factory the caller uses:
80+
# ``create_async_engine`` resolves the dialect via
81+
# ``URL.get_dialect(_is_async=True)`` -> this hook, while
82+
# ``create_engine`` uses this (sync) class directly. Mirrors stock
83+
# ``PGDialect_psycopg.get_async_dialect_cls``. Lazy import to avoid a
84+
# module-load cycle. MySQL cannot do this -- its sync/async paths are
85+
# different DBAPIs (mysql-connector-python vs aiomysql).
86+
from aws_advanced_python_wrapper.sqlalchemy_dialects.pg_async import \
87+
AwsWrapperPGPsycopgAsyncDialect
88+
return AwsWrapperPGPsycopgAsyncDialect
89+
7290
def create_connect_args(self, url):
7391
# SQLAlchemy's `create_engine` intercepts `plugins=` in the URL query
7492
# to load SA engine plugins, stripping it before the dialect sees it.

aws_advanced_python_wrapper/sqlalchemy_dialects/pg_async.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,21 @@
1414

1515
"""Async PostgreSQL SQLAlchemy dialect bound to the AWS Advanced Python Wrapper.
1616
17-
Registered as ``aws_wrapper_postgresql_async`` /
18-
``aws_wrapper_postgresql+psycopg_async`` via pyproject entry-points.
19-
Subclasses SA's standard ``PGDialectAsync_psycopg`` and swaps the DBAPI to
20-
an adapter that routes ``connect()`` through the async plugin pipeline
21-
while preserving SA's ``AsyncAdapt_psycopg_connection`` greenlet-bridge
22-
wrapper that the async engine expects.
17+
Reached via the sync dialect's ``get_async_dialect_cls`` hook, not a distinct
18+
URL: ``create_async_engine("postgresql+aws_wrapper_psycopg://...")`` resolves
19+
to this class (psycopg3 is a single DBAPI that does both sync and async, so
20+
one URL serves both -- mirrors stock ``postgresql+psycopg``). Subclasses SA's
21+
standard ``PGDialectAsync_psycopg`` and swaps the DBAPI to an adapter that
22+
routes ``connect()`` through the async plugin pipeline while preserving SA's
23+
``AsyncAdapt_psycopg_connection`` greenlet-bridge wrapper that the async
24+
engine expects.
2325
2426
Example::
2527
2628
from sqlalchemy.ext.asyncio import create_async_engine
2729
2830
engine = create_async_engine(
29-
"aws_wrapper_postgresql+psycopg_async://user:pwd@"
31+
"postgresql+aws_wrapper_psycopg://user:pwd@"
3032
"database.cluster-xyz.us-east-1.rds.amazonaws.com:5432/db"
3133
"?wrapper_dialect=aurora-pg&wrapper_plugins=failover,host_monitoring_v2"
3234
)
@@ -130,7 +132,10 @@ class AwsWrapperPGPsycopgAsyncDialect(
130132
to psycopg. Current overrides: ``_type_info_fetch``.
131133
"""
132134

133-
driver = "psycopg_async"
135+
# Same driver name as the sync dialect: this class is reached via the
136+
# sync dialect's ``get_async_dialect_cls`` (not a distinct URL), mirroring
137+
# stock psycopg where both sync and async report ``driver = "psycopg"``.
138+
driver = "aws_wrapper_psycopg"
134139
supports_statement_cache = True
135140

136141
# See _AsyncFailoverSuccessRewrapMixin / sqlalchemy_dialects/pg.py.

docs/development-guide/IntegrationTests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ unset FILTER # Done testing the IAM tests, unset FILTER
6666

6767
## Running async integration tests
6868

69-
The wrapper ships async counterparts to every sync integration test file. Async tests exercise `AsyncAwsWrapperConnection` (raw) and `create_async_engine` with the wrapper's async dialects (`aws_wrapper_postgresql+psycopg_async`, `aws_wrapper_mysql+aiomysql_async`). They are invoked via dedicated Gradle tasks, independent of the sync tasks:
69+
The wrapper ships async counterparts to every sync integration test file. Async tests exercise `AsyncAwsWrapperConnection` (raw) and `create_async_engine` with the wrapper's dialects (`postgresql+aws_wrapper_psycopg` — shared with sync via `get_async_dialect_cls`; `mysql+aws_wrapper_aiomysql`). They are invoked via dedicated Gradle tasks, independent of the sync tasks:
7070

7171
| Deployment | Engine | Sync task | Async task |
7272
|---|---|---|---|

docs/examples/MySQLSQLAlchemyAsyncFailover.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"""Async SQLAlchemy + AWS Advanced Python Wrapper: failover on Aurora MySQL.
1616
1717
Uses aiomysql as the async MySQL driver. The dialect
18-
`aws_wrapper_mysql+aiomysql_async` routes create_async_engine through the
18+
`mysql+aws_wrapper_aiomysql` routes create_async_engine through the
1919
wrapper's async plugin pipeline.
2020
2121
Wrapper plugins are configured via the `wrapper_plugins` URL alias
@@ -38,7 +38,7 @@
3838

3939
def build_engine():
4040
return create_async_engine(
41-
f"aws_wrapper_mysql+aiomysql_async://{USER}:{PASSWORD}@"
41+
f"mysql+aws_wrapper_aiomysql://{USER}:{PASSWORD}@"
4242
f"{CLUSTER_ENDPOINT}:3306/{DB_NAME}"
4343
"?wrapper_dialect=aurora-mysql&wrapper_plugins=failover",
4444
)

docs/examples/PGSQLAlchemyAsyncFailover.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
"""Async SQLAlchemy + AWS Advanced Python Wrapper: failover on Aurora PostgreSQL.
1616
17-
URL-based async engine usage (SP-9). The dialect `aws_wrapper_postgresql+psycopg_async`
18-
routes `create_async_engine` through `AsyncAwsWrapperConnection`, so all wrapper
19-
plugins (failover, host_monitoring_v2, etc. -- landing in later sub-projects) are available in
20-
async apps.
17+
URL-based async engine usage (SP-9). The `postgresql+aws_wrapper_psycopg` URL is
18+
shared with the sync engine -- `create_async_engine` selects the async dialect via
19+
the sync dialect's `get_async_dialect_cls` hook -- routing through
20+
`AsyncAwsWrapperConnection`, so all wrapper plugins (failover, host_monitoring_v2,
21+
etc.) are available in async apps.
2122
2223
The wrapper's `plugins` connection property is spelled `wrapper_plugins` in the
2324
URL query string because SA reserves `plugins=` for its own engine-plugin loader.
@@ -40,7 +41,7 @@
4041

4142
def build_engine():
4243
return create_async_engine(
43-
f"aws_wrapper_postgresql+psycopg_async://{USER}:{PASSWORD}@"
44+
f"postgresql+aws_wrapper_psycopg://{USER}:{PASSWORD}@"
4445
f"{CLUSTER_ENDPOINT}:5432/{DB_NAME}"
4546
"?wrapper_dialect=aurora-pg&wrapper_plugins=failover,host_monitoring_v2",
4647
)

0 commit comments

Comments
 (0)