dbx-tools-postgres 0.6.89__tar.gz → 0.6.91__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbx-tools-postgres
3
- Version: 0.6.89
3
+ Version: 0.6.91
4
4
  Summary: WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus
5
- Requires-Dist: asyncpg>=0.30,<1
6
- Requires-Dist: databricks-sdk>=0.63.0,<1
7
- Requires-Dist: greenlet>=3.2,<4
8
- Requires-Dist: dbx-tools-core==0.6.89
9
- Requires-Dist: psycopg[binary]>=3.2.9,<4
10
- Requires-Dist: sqlalchemy>=2.0.41,<3
5
+ Requires-Dist: asyncpg>=0.30
6
+ Requires-Dist: databricks-sdk>=0.63.0
7
+ Requires-Dist: greenlet>=3.2
8
+ Requires-Dist: dbx-tools-core==0.6.91
9
+ Requires-Dist: psycopg[binary]>=3.2.9
10
+ Requires-Dist: sqlalchemy>=2.0.41
11
11
  Requires-Python: >=3.10
12
12
  Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/postgres
13
13
  Description-Content-Type: text/markdown
@@ -39,9 +39,11 @@ Key features:
39
39
  `WorkspaceClient.api_client`;
40
40
  - resolves provisioned Lakebase instance DNS through
41
41
  `WorkspaceClient.database`;
42
- - injects a fresh database credential on SQLAlchemy's `do_connect` event rather
42
+ - injects a cached database credential on SQLAlchemy's `do_connect` event rather
43
43
  than storing an expiring password in the engine URL, using the SDK's
44
44
  provisioned-instance API or the Autoscaling `/postgres/credentials` endpoint;
45
+ - refreshes built-in credential providers ahead of expiry with a process-local
46
+ check-lock-check load, so concurrent pool connections share one mint;
45
47
  - supports sync psycopg and asyncpg SQLAlchemy engines;
46
48
  - derives advisory-lock ids from the same stable structured keys as the Node
47
49
  package and holds one checked-out connection for the full critical section;
@@ -63,7 +65,8 @@ engine = create_async_engine(
63
65
  ```
64
66
 
65
67
  Pass `credential_provider=` to either engine factory to inject credentials from
66
- another source while retaining the same connect-time rotation behavior.
68
+ another source. A custom provider owns its own cache, expiry, and refresh
69
+ serialization policy.
67
70
 
68
71
  ```python
69
72
  from dbx_tools.postgres import advisory_transaction_lock
@@ -25,9 +25,11 @@ Key features:
25
25
  `WorkspaceClient.api_client`;
26
26
  - resolves provisioned Lakebase instance DNS through
27
27
  `WorkspaceClient.database`;
28
- - injects a fresh database credential on SQLAlchemy's `do_connect` event rather
28
+ - injects a cached database credential on SQLAlchemy's `do_connect` event rather
29
29
  than storing an expiring password in the engine URL, using the SDK's
30
30
  provisioned-instance API or the Autoscaling `/postgres/credentials` endpoint;
31
+ - refreshes built-in credential providers ahead of expiry with a process-local
32
+ check-lock-check load, so concurrent pool connections share one mint;
31
33
  - supports sync psycopg and asyncpg SQLAlchemy engines;
32
34
  - derives advisory-lock ids from the same stable structured keys as the Node
33
35
  package and holds one checked-out connection for the full critical section;
@@ -49,7 +51,8 @@ engine = create_async_engine(
49
51
  ```
50
52
 
51
53
  Pass `credential_provider=` to either engine factory to inject credentials from
52
- another source while retaining the same connect-time rotation behavior.
54
+ another source. A custom provider owns its own cache, expiry, and refresh
55
+ serialization policy.
53
56
 
54
57
  ```python
55
58
  from dbx_tools.postgres import advisory_transaction_lock
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "dbx-tools-postgres"
3
- version = "0.6.89"
3
+ version = "0.6.91"
4
4
  description = "WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "asyncpg>=0.30,<1",
9
- "databricks-sdk>=0.63.0,<1",
10
- "greenlet>=3.2,<4",
11
- "dbx-tools-core==0.6.89",
12
- "psycopg[binary]>=3.2.9,<4",
13
- "sqlalchemy>=2.0.41,<3",
8
+ "asyncpg>=0.30",
9
+ "databricks-sdk>=0.63.0",
10
+ "greenlet>=3.2",
11
+ "dbx-tools-core==0.6.91",
12
+ "psycopg[binary]>=3.2.9",
13
+ "sqlalchemy>=2.0.41",
14
14
  ]
15
15
 
16
16
  [project.urls]
@@ -2,17 +2,17 @@
2
2
 
3
3
  [project]
4
4
  name = "dbx-tools-postgres"
5
- version = "0.6.89"
5
+ version = "0.6.91"
6
6
  description = "WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
- "asyncpg>=0.30,<1",
11
- "databricks-sdk>=0.63.0,<1",
12
- "greenlet>=3.2,<4",
13
- "dbx-tools-core==0.6.89",
14
- "psycopg[binary]>=3.2.9,<4",
15
- "sqlalchemy>=2.0.41,<3"
10
+ "asyncpg>=0.30",
11
+ "databricks-sdk>=0.63.0",
12
+ "greenlet>=3.2",
13
+ "dbx-tools-core==0.6.91",
14
+ "psycopg[binary]>=3.2.9",
15
+ "sqlalchemy>=2.0.41"
16
16
  ]
17
17
 
18
18
  [project.urls]
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime as dt
3
4
  import os
5
+ import threading
4
6
  import uuid
5
7
  from collections.abc import Callable, Mapping
6
8
  from dataclasses import dataclass
@@ -13,9 +15,15 @@ from sqlalchemy.ext.asyncio import create_async_engine as sqlalchemy_create_asyn
13
15
 
14
16
  from .address import SSL_MODES, ParsedAddress, SslMode, parse_address, parse_resource_path
15
17
 
18
+ """Lakebase connection resolution and connect-time credential injection."""
19
+
16
20
  CredentialProvider = Callable[[], str]
21
+ CredentialLoader = Callable[[], tuple[str, dt.datetime | None]]
17
22
 
18
23
  _API_BASE = "/api/2.0/postgres"
24
+ _CREDENTIAL_REFRESH_LEAD = dt.timedelta(minutes=5)
25
+ _DEFAULT_CREDENTIAL_LIFETIME = dt.timedelta(minutes=50)
26
+ _MINIMUM_CREDENTIAL_LIFETIME = dt.timedelta(minutes=1)
19
27
  _DEFAULT_DATABASE = "databricks_postgres"
20
28
  _DEFAULT_PORT = 5432
21
29
  _DEFAULT_SSL_MODE: SslMode = "require"
@@ -72,6 +80,28 @@ class ResolvedPostgresConnection:
72
80
  )
73
81
 
74
82
 
83
+ class _CachedCredentialProvider:
84
+ """Cache one credential and serialize refreshes within the Python process."""
85
+
86
+ def __init__(self, load: CredentialLoader) -> None:
87
+ self._load = load
88
+ self._lock = threading.RLock()
89
+ self._token: str | None = None
90
+ self._renew_at = dt.datetime.min.replace(tzinfo=dt.timezone.utc)
91
+
92
+ def __call__(self) -> str:
93
+ token = self._token
94
+ if token is not None and _utcnow() < self._renew_at:
95
+ return token
96
+ with self._lock:
97
+ if self._token is None or _utcnow() >= self._renew_at:
98
+ token, expiration = self._load()
99
+ self._token = token
100
+ self._renew_at = _credential_renewal(expiration)
101
+ assert self._token is not None
102
+ return self._token
103
+
104
+
75
105
  def workspace_credential_provider(
76
106
  workspace_client: WorkspaceClientLike,
77
107
  instance_name: str,
@@ -79,7 +109,7 @@ def workspace_credential_provider(
79
109
  if not instance_name.strip():
80
110
  raise ValueError("instance_name must not be empty")
81
111
 
82
- def provide() -> str:
112
+ def load() -> tuple[str, dt.datetime | None]:
83
113
  credential = workspace_client.database.generate_database_credential(
84
114
  request_id=str(uuid.uuid4()),
85
115
  instance_names=[instance_name],
@@ -87,9 +117,9 @@ def workspace_credential_provider(
87
117
  token = getattr(credential, "token", None)
88
118
  if not isinstance(token, str) or not token:
89
119
  raise RuntimeError("WorkspaceClient returned no Lakebase database credential token")
90
- return token
120
+ return token, _credential_expiration(getattr(credential, "expiration_time", None))
91
121
 
92
- return provide
122
+ return _CachedCredentialProvider(load)
93
123
 
94
124
 
95
125
  def autoscaling_credential_provider(
@@ -99,7 +129,7 @@ def autoscaling_credential_provider(
99
129
  if not endpoint.strip():
100
130
  raise ValueError("endpoint must not be empty")
101
131
 
102
- def provide() -> str:
132
+ def load() -> tuple[str, dt.datetime | None]:
103
133
  credential = workspace_client.api_client.do(
104
134
  "POST",
105
135
  f"{_API_BASE}/credentials",
@@ -109,9 +139,9 @@ def autoscaling_credential_provider(
109
139
  token = credential.get("token") if isinstance(credential, dict) else None
110
140
  if not isinstance(token, str) or not token:
111
141
  raise RuntimeError("WorkspaceClient returned no Lakebase database credential token")
112
- return token
142
+ return token, _mapping_credential_expiration(credential)
113
143
 
114
- return provide
144
+ return _CachedCredentialProvider(load)
115
145
 
116
146
 
117
147
  def install_credential_injection(engine: Engine, provider: CredentialProvider) -> None:
@@ -227,6 +257,47 @@ def resolve_postgres_connection(
227
257
  )
228
258
 
229
259
 
260
+ def _credential_renewal(expiration: dt.datetime | None) -> dt.datetime:
261
+ now = _utcnow()
262
+ if expiration is None:
263
+ return now + _DEFAULT_CREDENTIAL_LIFETIME
264
+ normalized = (
265
+ expiration.astimezone(dt.timezone.utc)
266
+ if expiration.tzinfo is not None
267
+ else expiration.replace(tzinfo=dt.timezone.utc)
268
+ )
269
+ return max(normalized - _CREDENTIAL_REFRESH_LEAD, now + _MINIMUM_CREDENTIAL_LIFETIME)
270
+
271
+
272
+ def _credential_expiration(value: object) -> dt.datetime | None:
273
+ if isinstance(value, dt.datetime):
274
+ return value
275
+ if isinstance(value, (int, float)):
276
+ try:
277
+ return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc)
278
+ except (OSError, OverflowError, ValueError):
279
+ return None
280
+ if not isinstance(value, str) or not value.strip():
281
+ return None
282
+ normalized = value.strip().replace("Z", "+00:00")
283
+ try:
284
+ return dt.datetime.fromisoformat(normalized)
285
+ except ValueError:
286
+ return None
287
+
288
+
289
+ def _mapping_credential_expiration(credential: Mapping[str, object]) -> dt.datetime | None:
290
+ for key in ("expiration_time", "expirationTime", "expires_at", "expiresAt"):
291
+ expiration = _credential_expiration(credential.get(key))
292
+ if expiration is not None:
293
+ return expiration
294
+ return None
295
+
296
+
297
+ def _utcnow() -> dt.datetime:
298
+ return dt.datetime.now(tz=dt.timezone.utc)
299
+
300
+
230
301
  def _default_provider(
231
302
  workspace_client: WorkspaceClientLike,
232
303
  resolved: ResolvedPostgresConnection,