Skip to content

Commit 4572140

Browse files
affandarCopilot
andcommitted
chore(release): duroxide-python 0.1.25
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5b068f6 commit 4572140

7 files changed

Lines changed: 262 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.1.24] - 2026-05-03
8+
## [0.1.25] - 2026-05-09
9+
10+
### Added
11+
12+
- **`PostgresEntraOptions`** — new Python class for tuning Microsoft Entra ID
13+
(Azure AD) authentication: keyword-only optional fields `audience`,
14+
`max_connections`, `acquire_timeout_ms`, and `refresh_interval_ms`.
15+
- **`PostgresProvider.connect_with_entra(host, port, database, user, options=None)`**
16+
connect to Azure Database for PostgreSQL Flexible Server via Entra token auth.
17+
Tokens are fetched and refreshed automatically via the `DefaultAzureCredential`
18+
chain (managed identity, environment variables, Azure CLI, etc.).
19+
- **`PostgresProvider.connect_with_schema_and_entra(host, port, database, user, schema, options=None)`**
20+
same as above with a custom schema for multi-tenant isolation.
21+
22+
### Changed
23+
24+
- **Bumped `duroxide` dependency**`0.1.28``0.1.29`
25+
- **Bumped `duroxide-pg` dependency**`0.1.30``0.1.32`
26+
27+
928

1029
### Added
1130

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
[package]
22
name = "duroxide-python"
3-
version = "0.1.24"
3+
version = "0.1.25"
44
edition = "2021"
55

66
[lib]
77
name = "duroxide_python"
88
crate-type = ["cdylib"]
99

1010
[dependencies]
11-
duroxide = { version = "0.1.28", features = ["sqlite"] }
12-
duroxide-pg = "0.1.30"
11+
duroxide = { version = "0.1.29", features = ["sqlite"] }
12+
duroxide-pg = "0.1.32"
1313
pyo3 = { version = "0.23", features = ["extension-module"] }
1414
async-trait = "0.1"
1515
tokio = { version = "1", features = ["full"] }

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Write durable workflows as Python generators. The Rust runtime handles replay, p
1414
- **Fan-out/Fan-in**`ctx.all()` for parallel execution, `ctx.race()` for first-to-complete
1515
- **Continue-as-new** — long-running orchestrations with bounded history
1616
- **Deterministic replay** — safe resume after crashes
17-
- **SQLite & PostgreSQL** — pluggable storage providers
17+
- **SQLite & PostgreSQL** — pluggable storage providers, including Microsoft Entra ID auth for Azure Database for PostgreSQL
1818
- **Custom Status**`ctx.set_custom_status()` / `ctx.reset_custom_status()` for orchestration progress reporting, `client.wait_for_status_change()` for efficient polling
1919
- **KV Store** — durable per-instance state via `ctx.set_kv_value()` / `ctx.get_kv_value()` / `ctx.get_kv_all_values()` / `ctx.get_kv_all_keys()` / `ctx.get_kv_length()` / `ctx.clear_kv_value()` / `ctx.clear_all_kv_values()` / `ctx.prune_kv_values_updated_before()`, plus `client.get_kv_value()` / `client.wait_for_kv_value()`
2020
- **Event Queues**`ctx.dequeue_event(queue_name)` for FIFO mailbox-style message passing, `client.enqueue_event()` to send messages
@@ -122,7 +122,7 @@ def send_email(ctx, input):
122122
## PostgreSQL Provider
123123

124124
```python
125-
from duroxide import PostgresProvider, Client, Runtime
125+
from duroxide import PostgresEntraOptions, PostgresProvider, Client, Runtime
126126

127127
provider = PostgresProvider.connect("postgresql://user:pass@localhost:5432/mydb")
128128
# or with custom schema:
@@ -132,6 +132,31 @@ runtime = Runtime(provider)
132132
client = Client(provider)
133133
```
134134

135+
### PostgreSQL with Microsoft Entra ID
136+
137+
For Azure Database for PostgreSQL Flexible Server, use Entra ID token authentication instead of a password:
138+
139+
```python
140+
provider = PostgresProvider.connect_with_entra(
141+
host="my-server.postgres.database.azure.com",
142+
port=5432,
143+
database="appdb",
144+
user="my-managed-identity",
145+
options=PostgresEntraOptions(max_connections=10),
146+
)
147+
148+
provider = PostgresProvider.connect_with_schema_and_entra(
149+
host="my-server.postgres.database.azure.com",
150+
port=5432,
151+
database="appdb",
152+
user="my-managed-identity",
153+
schema="duroxide_python",
154+
options=PostgresEntraOptions(refresh_interval_ms=1_200_000),
155+
)
156+
```
157+
158+
`PostgresEntraOptions` also accepts `audience`, `acquire_timeout_ms`, and `refresh_interval_ms`.
159+
135160
## Admin APIs
136161

137162
```python

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "duroxide"
7-
version = "0.1.24"
7+
version = "0.1.25"
88
description = "Python SDK for the Duroxide durable execution runtime"
99
readme = "README.md"
1010
license = { text = "MIT" }

python/duroxide/__init__.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from duroxide._duroxide import (
99
PySqliteProvider,
1010
PyPostgresProvider,
11+
PyPostgresEntraOptions,
1112
PyClient,
1213
PyRuntime,
1314
RuntimeOptions,
@@ -133,6 +134,41 @@ def in_memory() -> "SqliteProvider":
133134
return SqliteProvider(PySqliteProvider.in_memory())
134135

135136

137+
class PostgresEntraOptions:
138+
"""Options for Entra ID (Azure AD) authentication with PostgreSQL.
139+
140+
All parameters are keyword-only and optional; omitting a parameter uses
141+
the duroxide-pg default for that setting.
142+
143+
Parameters
144+
----------
145+
audience:
146+
Token audience/scope. Override for sovereign clouds (e.g., Azure US
147+
Government: ``https://ossrdbms-aad.database.usgovcloudapi.net/.default``).
148+
max_connections:
149+
Maximum pool connection count.
150+
acquire_timeout_ms:
151+
Pool connection acquisition timeout in milliseconds.
152+
refresh_interval_ms:
153+
Upper bound on time between token refresh attempts, in milliseconds.
154+
"""
155+
156+
def __init__(
157+
self,
158+
*,
159+
audience: "str | None" = None,
160+
max_connections: "int | None" = None,
161+
acquire_timeout_ms: "int | None" = None,
162+
refresh_interval_ms: "int | None" = None,
163+
):
164+
self._native = PyPostgresEntraOptions(
165+
audience=audience,
166+
max_connections=max_connections,
167+
acquire_timeout_ms=acquire_timeout_ms,
168+
refresh_interval_ms=refresh_interval_ms,
169+
)
170+
171+
136172
class PostgresProvider:
137173
"""PostgreSQL provider for duroxide."""
138174

@@ -152,6 +188,60 @@ def connect_with_schema(database_url: str, schema: str) -> "PostgresProvider":
152188
PyPostgresProvider.connect_with_schema(database_url, schema)
153189
)
154190

191+
@staticmethod
192+
def connect_with_entra(
193+
host: str,
194+
port: int,
195+
database: str,
196+
user: str,
197+
options: "PostgresEntraOptions | None" = None,
198+
) -> "PostgresProvider":
199+
"""Connect to Azure Database for PostgreSQL using Entra ID (Azure AD) auth.
200+
201+
The SDK fetches and refreshes the access token automatically via the
202+
DefaultAzureCredential chain (managed identity, environment variables,
203+
Azure CLI, etc.).
204+
205+
Parameters
206+
----------
207+
host:
208+
PostgreSQL server hostname (e.g. ``myserver.postgres.database.azure.com``).
209+
port:
210+
PostgreSQL server port (usually 5432).
211+
database:
212+
Target database name.
213+
user:
214+
Entra principal name mapped to a PostgreSQL role on the server.
215+
options:
216+
Optional :class:`PostgresEntraOptions` for tuning. Pass ``None``
217+
(or omit) to use defaults.
218+
"""
219+
native_opts = options._native if options is not None else None
220+
return PostgresProvider(
221+
PyPostgresProvider.connect_with_entra(host, port, database, user, native_opts)
222+
)
223+
224+
@staticmethod
225+
def connect_with_schema_and_entra(
226+
host: str,
227+
port: int,
228+
database: str,
229+
user: str,
230+
schema: str,
231+
options: "PostgresEntraOptions | None" = None,
232+
) -> "PostgresProvider":
233+
"""Same as :meth:`connect_with_entra` but uses a custom schema.
234+
235+
The schema will be created if it does not exist. Useful for
236+
multi-tenant deployments where each tenant has its own schema.
237+
"""
238+
native_opts = options._native if options is not None else None
239+
return PostgresProvider(
240+
PyPostgresProvider.connect_with_schema_and_entra(
241+
host, port, database, user, schema, native_opts
242+
)
243+
)
244+
155245

156246
class Client:
157247
"""Client for starting and managing orchestration instances."""
@@ -504,6 +594,7 @@ def default_and(tags: list) -> str:
504594
__all__ = [
505595
"SqliteProvider",
506596
"PostgresProvider",
597+
"PostgresEntraOptions",
507598
"Client",
508599
"Runtime",
509600
"OrchestrationResult",

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ fn _duroxide(m: &Bound<'_, PyModule>) -> PyResult<()> {
191191
m.add_function(wrap_pyfunction!(init_tracing, m)?)?;
192192
m.add_class::<provider::PySqliteProvider>()?;
193193
m.add_class::<pg_provider::PyPostgresProvider>()?;
194+
m.add_class::<pg_provider::PyPostgresEntraOptions>()?;
194195
m.add_class::<client::PyClient>()?;
195196
m.add_class::<runtime::PyRuntime>()?;
196197
m.add_class::<runtime::PyRuntimeOptions>()?;

src/pg_provider.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,59 @@
11
use pyo3::prelude::*;
22
use std::sync::Arc;
3+
use std::time::Duration;
34

45
use crate::runtime::TOKIO_RT;
56

7+
/// Python-visible options for Entra ID (Azure AD) authentication.
8+
///
9+
/// All fields are optional; omitting a field uses the duroxide-pg default.
10+
#[pyclass]
11+
#[derive(Clone, Default)]
12+
pub struct PyPostgresEntraOptions {
13+
pub audience: Option<String>,
14+
pub max_connections: Option<u32>,
15+
pub acquire_timeout_ms: Option<u64>,
16+
pub refresh_interval_ms: Option<u64>,
17+
}
18+
19+
#[pymethods]
20+
impl PyPostgresEntraOptions {
21+
#[new]
22+
#[pyo3(signature = (*, audience=None, max_connections=None, acquire_timeout_ms=None, refresh_interval_ms=None))]
23+
fn new(
24+
audience: Option<String>,
25+
max_connections: Option<u32>,
26+
acquire_timeout_ms: Option<u64>,
27+
refresh_interval_ms: Option<u64>,
28+
) -> Self {
29+
Self {
30+
audience,
31+
max_connections,
32+
acquire_timeout_ms,
33+
refresh_interval_ms,
34+
}
35+
}
36+
}
37+
38+
impl PyPostgresEntraOptions {
39+
fn into_entra_auth_options(self) -> duroxide_pg::EntraAuthOptions {
40+
let mut opts = duroxide_pg::EntraAuthOptions::new();
41+
if let Some(aud) = self.audience {
42+
opts = opts.audience(aud);
43+
}
44+
if let Some(mc) = self.max_connections {
45+
opts = opts.max_connections(mc);
46+
}
47+
if let Some(ms) = self.acquire_timeout_ms {
48+
opts = opts.acquire_timeout(Duration::from_millis(ms));
49+
}
50+
if let Some(ms) = self.refresh_interval_ms {
51+
opts = opts.refresh_interval(Duration::from_millis(ms));
52+
}
53+
opts
54+
}
55+
}
56+
657
/// Wraps duroxide-pg's PostgresProvider for use from Python.
758
#[pyclass]
859
pub struct PyPostgresProvider {
@@ -46,4 +97,72 @@ impl PyPostgresProvider {
4697
inner: Arc::new(provider),
4798
})
4899
}
100+
101+
/// Connect to Azure Database for PostgreSQL using Microsoft Entra ID
102+
/// (Azure AD) token authentication. The runtime fetches and refreshes
103+
/// the token automatically via the DefaultAzureCredential chain.
104+
///
105+
/// `user` must be the Entra principal name mapped to a PostgreSQL role
106+
/// on the server. Pass `None` for `options` to use defaults.
107+
#[staticmethod]
108+
#[pyo3(signature = (host, port, database, user, options=None))]
109+
fn connect_with_entra(
110+
host: String,
111+
port: u16,
112+
database: String,
113+
user: String,
114+
options: Option<PyPostgresEntraOptions>,
115+
) -> PyResult<Self> {
116+
let entra_opts = options.unwrap_or_default().into_entra_auth_options();
117+
let provider = TOKIO_RT
118+
.block_on(async {
119+
duroxide_pg::PostgresProvider::new_with_entra(
120+
&host, port, &database, &user, entra_opts,
121+
)
122+
.await
123+
})
124+
.map_err(|e| {
125+
pyo3::exceptions::PyRuntimeError::new_err(format!(
126+
"Failed to connect to PostgreSQL with Entra auth: {e}"
127+
))
128+
})?;
129+
Ok(Self {
130+
inner: Arc::new(provider),
131+
})
132+
}
133+
134+
/// Same as `connect_with_entra` but uses a custom schema for tenant
135+
/// isolation. The schema will be created if it does not exist.
136+
#[staticmethod]
137+
#[pyo3(signature = (host, port, database, user, schema, options=None))]
138+
fn connect_with_schema_and_entra(
139+
host: String,
140+
port: u16,
141+
database: String,
142+
user: String,
143+
schema: String,
144+
options: Option<PyPostgresEntraOptions>,
145+
) -> PyResult<Self> {
146+
let entra_opts = options.unwrap_or_default().into_entra_auth_options();
147+
let provider = TOKIO_RT
148+
.block_on(async {
149+
duroxide_pg::PostgresProvider::new_with_schema_and_entra(
150+
&host,
151+
port,
152+
&database,
153+
&user,
154+
Some(&schema),
155+
entra_opts,
156+
)
157+
.await
158+
})
159+
.map_err(|e| {
160+
pyo3::exceptions::PyRuntimeError::new_err(format!(
161+
"Failed to connect to PostgreSQL with Entra auth: {e}"
162+
))
163+
})?;
164+
Ok(Self {
165+
inner: Arc::new(provider),
166+
})
167+
}
49168
}

0 commit comments

Comments
 (0)