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.
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/PKG-INFO +12 -9
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/README.md +5 -2
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/pyproject.toml +7 -7
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/pyproject.toml.orig +7 -7
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/engine.py +77 -6
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/__init__.py +0 -0
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/address.py +0 -0
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/advisory_lock.py +0 -0
- {dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/topic_bus.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dbx-tools-postgres
|
|
3
|
-
Version: 0.6.
|
|
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
|
|
6
|
-
Requires-Dist: databricks-sdk>=0.63.0
|
|
7
|
-
Requires-Dist: greenlet>=3.2
|
|
8
|
-
Requires-Dist: dbx-tools-core==0.6.
|
|
9
|
-
Requires-Dist: psycopg[binary]>=3.2.9
|
|
10
|
-
Requires-Dist: sqlalchemy>=2.0.41
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
9
|
-
"databricks-sdk>=0.63.0
|
|
10
|
-
"greenlet>=3.2
|
|
11
|
-
"dbx-tools-core==0.6.
|
|
12
|
-
"psycopg[binary]>=3.2.9
|
|
13
|
-
"sqlalchemy>=2.0.41
|
|
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.
|
|
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
|
|
11
|
-
"databricks-sdk>=0.63.0
|
|
12
|
-
"greenlet>=3.2
|
|
13
|
-
"dbx-tools-core==0.6.
|
|
14
|
-
"psycopg[binary]>=3.2.9
|
|
15
|
-
"sqlalchemy>=2.0.41
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
File without changes
|
|
File without changes
|
{dbx_tools_postgres-0.6.89 → dbx_tools_postgres-0.6.91}/src/dbx_tools/postgres/advisory_lock.py
RENAMED
|
File without changes
|
|
File without changes
|