-
Notifications
You must be signed in to change notification settings - Fork 1
Update Python support #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7448f51
004fb8c
e7f836b
99ef432
ab1db2c
e3987eb
f66f56d
9079dec
3967f04
2a150e5
8cb5c5f
e2a036b
0f0d796
4b2f09d
597504a
093ae17
6db50b5
c7cd50f
a62a171
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| name: PR Python Checks | ||
| on: | ||
| pull_request: | ||
| branches: [ main ] | ||
| paths: | ||
| - 'python/**' | ||
| - '.github/workflows/pr-python.yml' | ||
|
|
||
| jobs: | ||
| python-quality: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Install deps | ||
| working-directory: python | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install .[all] | ||
| - name: Ruff Lint | ||
| working-directory: python | ||
| run: python -m ruff check src tests | ||
| - name: Type check | ||
| working-directory: python | ||
| run: python -m mypy src/azurepg_entra/ | ||
| - name: Tests | ||
| if: ${{ always() }} # adjust if you add tests | ||
| working-directory: python | ||
| run: python -m pytest tests --import-mode=importlib -q |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,93 +101,44 @@ Choose the driver that best fits your project needs: | |
|
|
||
| - **psycopg3**: Modern PostgreSQL driver (recommended for new projects) | ||
| - **psycopg2**: Legacy PostgreSQL driver (for existing projects) | ||
| - **SQLAlchemy**: High-level ORM/Core interface using psycopg3 backend | ||
| - **SQLAlchemy**: High-level ORM/Core interface | ||
|
|
||
| --- | ||
|
|
||
| ## psycopg2 Driver (Legacy Support) | ||
|
|
||
| > **Note**: psycopg2 is in maintenance mode. For new projects, consider using psycopg3 instead. | ||
|
|
||
| The psycopg2 integration provides both synchronous (psycopg2) and asynchronous (aiopg) connection support with Azure Entra ID authentication. | ||
| The psycopg2 integration provides synchronous connection support with Azure Entra ID authentication through connection pooling. | ||
|
|
||
| ### Installation | ||
| ```bash | ||
| pip install "azurepg-entra[psycopg2]" | ||
| ``` | ||
|
|
||
| ### Synchronous Connection (psycopg2) | ||
| ### Connection Pooling (Recommended) | ||
|
|
||
| ```python | ||
| from azurepg_entra.psycopg2 import connect_with_entra | ||
| from psycopg2 import pool | ||
|
|
||
| def main(): | ||
| # Direct connection | ||
| conn = connect_with_entra( | ||
| host="your-server.postgres.database.azure.com", | ||
| port=5432, | ||
| dbname="your_database" | ||
| ) | ||
|
|
||
| try: | ||
| with conn.cursor() as cur: | ||
| cur.execute("SELECT current_user, now()") | ||
| user, time = cur.fetchone() | ||
| print(f"Connected as: {user} at {time}") | ||
| finally: | ||
| conn.close() | ||
|
|
||
| # Connection pooling | ||
| def entra_connection_factory(*args, **kwargs): | ||
| return connect_with_entra( | ||
| host="your-server.postgres.database.azure.com", | ||
| port=5432, | ||
| dbname="your_database" | ||
| ) | ||
|
|
||
| connection_pool = pool.ThreadedConnectionPool( | ||
| minconn=1, maxconn=5, | ||
| connection_factory=entra_connection_factory | ||
| ) | ||
|
|
||
| conn = connection_pool.getconn() | ||
| try: | ||
| with conn.cursor() as cur: | ||
| cur.execute("SELECT current_user") | ||
| print(f"Pool connection as: {cur.fetchone()[0]}") | ||
| finally: | ||
| connection_pool.putconn(conn) | ||
| connection_pool.closeall() | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| from azurepg_entra.psycopg2 import EntraConnection # import library | ||
| from psycopg2 import pool # import to use pooling | ||
|
|
||
| with pool.ThreadedConnectionPool( | ||
| minconn=1, | ||
| maxconn=5, | ||
| host="your-server.postgres.database.azure.com", | ||
| database="your_database", | ||
| connection_factory=EntraConnection | ||
| ) as connection_pool: | ||
| ``` | ||
|
|
||
| ### Asynchronous Connection (aiopg) | ||
| ### Direct Connection | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from azurepg_entra.psycopg2 import connect_with_entra_async | ||
|
|
||
| async def main(): | ||
| # Direct async connection | ||
| conn = await connect_with_entra_async( | ||
| host="your-server.postgres.database.azure.com", | ||
| port=5432, | ||
| dbname="your_database" | ||
| ) | ||
|
|
||
| try: | ||
| async with conn.cursor() as cur: | ||
| await cur.execute("SELECT current_user, now()") | ||
| user, time = await cur.fetchone() | ||
| print(f"Async connected as: {user} at {time}") | ||
| finally: | ||
| conn.close() | ||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
| from azurepg_entra.psycopg2 import EntraConnection # import library | ||
|
|
||
| with EntraConnection( | ||
| "postgresql://your-server.postgres.database.azure.com:5432/your_database" | ||
| ) as conn | ||
| ``` | ||
|
|
||
| --- | ||
|
|
@@ -204,82 +155,38 @@ pip install "azurepg-entra[psycopg3]" | |
| ### Synchronous Connection | ||
|
|
||
| ```python | ||
| from azurepg_entra.psycopg3 import SyncEntraConnection | ||
| from psycopg_pool import ConnectionPool | ||
|
|
||
| def main(): | ||
| # Direct connection | ||
| with SyncEntraConnection.connect( | ||
| "postgresql://your-server.postgres.database.azure.com:5432/your_database" | ||
| ) as conn: | ||
| with conn.cursor() as cur: | ||
| cur.execute("SELECT current_user, now()") | ||
| user, time = cur.fetchone() | ||
| print(f"Connected as: {user} at {time}") | ||
|
|
||
| # Connection pooling (recommended for production) | ||
| with ConnectionPool( | ||
| conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", | ||
| connection_class=SyncEntraConnection, | ||
| min_size=1, # keep at least 1 connection always open | ||
| max_size=5, # allow up to 5 concurrent connections | ||
| max_waiting=10, # seconds to wait if pool is full | ||
| ) as pool: | ||
| with pool.connection() as conn: | ||
| with conn.cursor() as cur: | ||
| cur.execute("SELECT current_user, now()") | ||
| user, time = cur.fetchone() | ||
| print(f"Pool connection as: {user} at {time}") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| from azurepg_entra.psycopg3 import EntraConnection # import library | ||
| from psycopg_pool import ConnectionPool # import to use pooling | ||
|
|
||
| with ConnectionPool( | ||
| conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", | ||
| connection_class=EntraConnection, | ||
| min_size=1, # keep at least 1 connection always open | ||
| max_size=5, # allow up to 5 concurrent connections | ||
| ) as pool | ||
| ``` | ||
|
|
||
| ### Asynchronous Connection | ||
|
|
||
| ```python | ||
| import asyncio | ||
| import sys | ||
| from azurepg_entra.psycopg3 import AsyncEntraConnection | ||
| from psycopg_pool import AsyncConnectionPool | ||
|
|
||
| async def main(): | ||
| # Direct async connection | ||
| async with await AsyncEntraConnection.connect( | ||
| "postgresql://your-server.postgres.database.azure.com:5432/your_database" | ||
| ) as conn: | ||
| async with conn.cursor() as cur: | ||
| await cur.execute("SELECT current_user, now()") | ||
| user, time = await cur.fetchone() | ||
| print(f"Async connected as: {user} at {time}") | ||
|
|
||
| # Async connection pooling (recommended for production) | ||
| async with AsyncConnectionPool( | ||
| conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", | ||
| connection_class=AsyncEntraConnection, | ||
| min_size=1, # keep at least 1 connection always open | ||
| max_size=5, # allow up to 5 concurrent connections | ||
| max_waiting=10, # seconds to wait if pool is full | ||
| ) as pool: | ||
| async with pool.connection() as conn: | ||
| async with conn.cursor() as cur: | ||
| await cur.execute("SELECT current_user, now()") | ||
| user, time = await cur.fetchone() | ||
| print(f"Pool connection as: {user} at {time}") | ||
|
|
||
| if __name__ == "__main__": | ||
| # Windows compatibility for async operations | ||
| if sys.platform == "win32": | ||
| asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) | ||
|
|
||
| asyncio.run(main()) | ||
| from azurepg_entra.psycopg3 import AsyncEntraConnection # import library | ||
| from psycopg_pool import AsyncConnectionPool # import to use pooling | ||
|
|
||
| async with AsyncConnectionPool( | ||
| conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", | ||
| connection_class=AsyncEntraConnection, | ||
| min_size=1, # keep at least 1 connection always open | ||
| max_size=5, # allow up to 5 concurrent connections | ||
| ) as pool | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## SQLAlchemy Integration | ||
|
|
||
| SQLAlchemy integration uses psycopg3 as the backend driver with automatic Entra ID authentication. | ||
| SQLAlchemy integration uses psycopg3 as the backend driver with automatic Entra ID authentication through event listeners. | ||
|
|
||
| > **For more information**: See SQLAlchemy's documentation on [controlling how parameters are passed to the DBAPI connect function](https://docs.sqlalchemy.org/en/20/core/engines.html#controlling-how-parameters-are-passed-to-the-dbapi-connect-function). | ||
|
|
||
| ### Installation | ||
| ```bash | ||
|
|
@@ -289,73 +196,37 @@ pip install "azurepg-entra[sqlalchemy]" | |
| ### Synchronous Engine | ||
|
|
||
| ```python | ||
| from azurepg_entra.sqlalchemy import create_entra_engine | ||
| from sqlalchemy import text | ||
|
|
||
| def main(): | ||
| # Create synchronous engine with Entra ID authentication | ||
| engine = create_entra_engine( | ||
| "postgresql+psycopg://your-server.postgres.database.azure.com:5432/your_database" | ||
| ) | ||
| from sqlalchemy import create_engine | ||
| from azurepg_entra.sqlalchemy import enable_entra_authentication # import library | ||
|
|
||
| with create_engine("postgresql+psycopg://your-server.postgres.database.azure.com/your_database") as engine: | ||
| # Enable Entra ID authentication | ||
| enable_entra_authentication(engine) | ||
|
|
||
| # Core usage | ||
| with engine.connect() as conn: | ||
| result = conn.execute(text("SELECT current_user, now()")) | ||
| user, time = result.fetchone() | ||
| print(f"SQLAlchemy connected as: {user} at {time}") | ||
|
|
||
|
|
||
| # ORM usage | ||
| from sqlalchemy.orm import sessionmaker | ||
| Session = sessionmaker(bind=engine) | ||
|
|
||
| with Session() as session: | ||
| result = session.execute(text("SELECT current_database()")) | ||
| db_name = result.scalar() | ||
| print(f"Connected to database: {db_name}") | ||
|
|
||
| engine.dispose() | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| ``` | ||
|
|
||
| ### Asynchronous Engine | ||
|
|
||
| ```python | ||
| import asyncio | ||
| import sys | ||
| from azurepg_entra.sqlalchemy import create_async_entra_engine | ||
| from sqlalchemy import text | ||
| from sqlalchemy.ext.asyncio import async_sessionmaker | ||
|
|
||
| async def main(): | ||
| # Create asynchronous engine with Entra ID authentication | ||
| engine = await create_async_entra_engine( | ||
| "postgresql+psycopg://your-server.postgres.database.azure.com:5432/your_database" | ||
| ) | ||
| from sqlalchemy.ext.asyncio import create_async_engine | ||
| from azurepg_entra.sqlalchemy import enable_entra_authentication_async # import library | ||
|
|
||
| async with create_async_engine("postgresql+psycopg://your-server.postgres.database.azure.com/your_database") as engine: | ||
| # Enable Entra ID authentication for async | ||
| enable_entra_authentication_async(engine) | ||
|
|
||
| # Async Core usage | ||
| async with engine.connect() as conn: | ||
| result = await conn.execute(text("SELECT current_user, now()")) | ||
| user, time = result.fetchone() | ||
| print(f"Async SQLAlchemy connected as: {user} at {time}") | ||
|
|
||
| # Async ORM usage | ||
| from sqlalchemy.ext.asyncio import async_sessionmaker | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to have this type of usage as an example; move to samples and have brief samples in README to give reader context, and link out to samples for more broad treatment.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the example with sessionmaker to |
||
| AsyncSession = async_sessionmaker(engine, expire_on_commit=False) | ||
|
|
||
| async with AsyncSession() as session: | ||
| result = await session.execute(text("SELECT current_database()")) | ||
| db_name = result.scalar() | ||
| print(f"Async connected to database: {db_name}") | ||
|
|
||
| await engine.dispose() | ||
|
|
||
| if __name__ == "__main__": | ||
| # Windows compatibility for async operations | ||
| if sys.platform == "win32": | ||
| asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) | ||
|
|
||
| asyncio.run(main()) | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
@@ -380,8 +251,6 @@ The package automatically requests the correct OAuth2 scopes: | |
| - **⏰ Automatic expiration**: Tokens expire and are refreshed automatically | ||
| - **🛡️ SSL enforcement**: All connections require SSL encryption | ||
| - **🔑 Principle of least privilege**: Only database-specific scopes are requested | ||
| - **📋 Audit logging**: Authentication events are logged by Azure Database for PostgreSQL | ||
|
|
||
| --- | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth linking to the sqlalchemy docs for the event hooks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done