dbx-tools-postgres 0.6.222__tar.gz → 0.6.224__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.222 → dbx_tools_postgres-0.6.224}/PKG-INFO +9 -22
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/README.md +5 -18
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/pyproject.toml +4 -4
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/pyproject.toml.orig +4 -4
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/address.py +5 -5
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/engine.py +54 -91
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/LICENSE +0 -0
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/__init__.py +0 -0
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/advisory_lock.py +0 -0
- {dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/topic_bus.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbx-tools-postgres
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.224
|
|
4
4
|
Summary: WorkspaceClient-backed Lakebase Postgres resolution, SQLAlchemy engines, advisory locks, and LISTEN/NOTIFY topic bus
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Dist: asyncpg>=0.30
|
|
8
|
-
Requires-Dist: databricks-sdk>=0.
|
|
8
|
+
Requires-Dist: databricks-sdk>=0.123.0
|
|
9
9
|
Requires-Dist: greenlet>=3.2
|
|
10
10
|
Requires-Dist: psycopg[binary]>=3.2.9
|
|
11
11
|
Requires-Dist: sqlalchemy>=2.0.41
|
|
12
|
-
Requires-Dist: dbx-tools-core==0.6.
|
|
13
|
-
Requires-Dist: dbx-tools-core-rs==0.6.
|
|
12
|
+
Requires-Dist: dbx-tools-core==0.6.224
|
|
13
|
+
Requires-Dist: dbx-tools-core-rs==0.6.224
|
|
14
14
|
Requires-Python: >=3.11, <4
|
|
15
15
|
Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/postgres
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -39,12 +39,13 @@ Key features:
|
|
|
39
39
|
- accepts the same Postgres URI, Lakebase resource path, hostname, and project-id
|
|
40
40
|
address shapes as `@dbx-tools/appkit` through the native Rust parser;
|
|
41
41
|
- resolves missing autoscaling endpoint fields through
|
|
42
|
-
`WorkspaceClient.
|
|
42
|
+
`WorkspaceClient.postgres`, including SDK-owned pagination and workspace-id
|
|
43
|
+
headers;
|
|
43
44
|
- resolves provisioned Lakebase instance DNS through
|
|
44
45
|
`WorkspaceClient.database`;
|
|
45
46
|
- injects a cached database credential on SQLAlchemy's `do_connect` event rather
|
|
46
47
|
than storing an expiring password in the engine URL, using the SDK's
|
|
47
|
-
provisioned-instance
|
|
48
|
+
provisioned-instance or typed Autoscaling credential API;
|
|
48
49
|
- refreshes built-in credential providers ahead of expiry with a process-local
|
|
49
50
|
check-lock-check load, so concurrent pool connections share one mint;
|
|
50
51
|
- supports sync psycopg and asyncpg SQLAlchemy engines;
|
|
@@ -143,7 +144,6 @@ driver mints the Lakebase token once and the UDF closes over it, so the executor
|
|
|
143
144
|
builds a plain SQLAlchemy engine and installs the token as its provider:
|
|
144
145
|
|
|
145
146
|
```python
|
|
146
|
-
from sqlalchemy import URL
|
|
147
147
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
148
148
|
from dbx_tools.postgres import (
|
|
149
149
|
PostgresTopicBus,
|
|
@@ -153,26 +153,13 @@ from dbx_tools.postgres import (
|
|
|
153
153
|
|
|
154
154
|
# driver: resolve once, capture in the closure
|
|
155
155
|
resolved = resolve_postgres_connection(workspace_client, config)
|
|
156
|
-
token = workspace_client.
|
|
157
|
-
"POST",
|
|
158
|
-
"/api/2.0/postgres/credentials",
|
|
159
|
-
body={"endpoint": resolved.endpoint},
|
|
160
|
-
)["token"]
|
|
156
|
+
token = workspace_client.postgres.generate_database_credential(resolved.endpoint).token
|
|
161
157
|
|
|
162
158
|
|
|
163
159
|
@udf(returnType=StringType())
|
|
164
160
|
def publish(key: str) -> str:
|
|
165
161
|
async def run() -> str:
|
|
166
|
-
engine = create_async_engine(
|
|
167
|
-
URL.create(
|
|
168
|
-
"postgresql+asyncpg",
|
|
169
|
-
username=resolved.user,
|
|
170
|
-
host=resolved.host,
|
|
171
|
-
port=resolved.port,
|
|
172
|
-
database=resolved.database,
|
|
173
|
-
query={"ssl": resolved.ssl_mode},
|
|
174
|
-
)
|
|
175
|
-
)
|
|
162
|
+
engine = create_async_engine(resolved.url("postgresql+asyncpg"))
|
|
176
163
|
install_credential_injection(engine.sync_engine, lambda: token)
|
|
177
164
|
bus = PostgresTopicBus(engine, channel="app-events")
|
|
178
165
|
try:
|
|
@@ -22,12 +22,13 @@ Key features:
|
|
|
22
22
|
- accepts the same Postgres URI, Lakebase resource path, hostname, and project-id
|
|
23
23
|
address shapes as `@dbx-tools/appkit` through the native Rust parser;
|
|
24
24
|
- resolves missing autoscaling endpoint fields through
|
|
25
|
-
`WorkspaceClient.
|
|
25
|
+
`WorkspaceClient.postgres`, including SDK-owned pagination and workspace-id
|
|
26
|
+
headers;
|
|
26
27
|
- resolves provisioned Lakebase instance DNS through
|
|
27
28
|
`WorkspaceClient.database`;
|
|
28
29
|
- injects a cached database credential on SQLAlchemy's `do_connect` event rather
|
|
29
30
|
than storing an expiring password in the engine URL, using the SDK's
|
|
30
|
-
provisioned-instance
|
|
31
|
+
provisioned-instance or typed Autoscaling credential API;
|
|
31
32
|
- refreshes built-in credential providers ahead of expiry with a process-local
|
|
32
33
|
check-lock-check load, so concurrent pool connections share one mint;
|
|
33
34
|
- supports sync psycopg and asyncpg SQLAlchemy engines;
|
|
@@ -126,7 +127,6 @@ driver mints the Lakebase token once and the UDF closes over it, so the executor
|
|
|
126
127
|
builds a plain SQLAlchemy engine and installs the token as its provider:
|
|
127
128
|
|
|
128
129
|
```python
|
|
129
|
-
from sqlalchemy import URL
|
|
130
130
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
131
131
|
from dbx_tools.postgres import (
|
|
132
132
|
PostgresTopicBus,
|
|
@@ -136,26 +136,13 @@ from dbx_tools.postgres import (
|
|
|
136
136
|
|
|
137
137
|
# driver: resolve once, capture in the closure
|
|
138
138
|
resolved = resolve_postgres_connection(workspace_client, config)
|
|
139
|
-
token = workspace_client.
|
|
140
|
-
"POST",
|
|
141
|
-
"/api/2.0/postgres/credentials",
|
|
142
|
-
body={"endpoint": resolved.endpoint},
|
|
143
|
-
)["token"]
|
|
139
|
+
token = workspace_client.postgres.generate_database_credential(resolved.endpoint).token
|
|
144
140
|
|
|
145
141
|
|
|
146
142
|
@udf(returnType=StringType())
|
|
147
143
|
def publish(key: str) -> str:
|
|
148
144
|
async def run() -> str:
|
|
149
|
-
engine = create_async_engine(
|
|
150
|
-
URL.create(
|
|
151
|
-
"postgresql+asyncpg",
|
|
152
|
-
username=resolved.user,
|
|
153
|
-
host=resolved.host,
|
|
154
|
-
port=resolved.port,
|
|
155
|
-
database=resolved.database,
|
|
156
|
-
query={"ssl": resolved.ssl_mode},
|
|
157
|
-
)
|
|
158
|
-
)
|
|
145
|
+
engine = create_async_engine(resolved.url("postgresql+asyncpg"))
|
|
159
146
|
install_credential_injection(engine.sync_engine, lambda: token)
|
|
160
147
|
bus = PostgresTopicBus(engine, channel="app-events")
|
|
161
148
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dbx-tools-postgres"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.224"
|
|
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.11,<4"
|
|
@@ -8,12 +8,12 @@ license = "Apache-2.0"
|
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
dependencies = [
|
|
10
10
|
"asyncpg>=0.30",
|
|
11
|
-
"databricks-sdk>=0.
|
|
11
|
+
"databricks-sdk>=0.123.0",
|
|
12
12
|
"greenlet>=3.2",
|
|
13
13
|
"psycopg[binary]>=3.2.9",
|
|
14
14
|
"sqlalchemy>=2.0.41",
|
|
15
|
-
"dbx-tools-core==0.6.
|
|
16
|
-
"dbx-tools-core-rs==0.6.
|
|
15
|
+
"dbx-tools-core==0.6.224",
|
|
16
|
+
"dbx-tools-core-rs==0.6.224",
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
[project.urls]
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "dbx-tools-postgres"
|
|
5
|
-
version = "0.6.
|
|
5
|
+
version = "0.6.224"
|
|
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.11,<4"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
|
-
license-files = ["LICENSE"]
|
|
11
|
-
dependencies = ["asyncpg>=0.30", "databricks-sdk>=0.
|
|
10
|
+
license-files = [ "LICENSE" ]
|
|
11
|
+
dependencies = [ "asyncpg>=0.30", "databricks-sdk>=0.123.0", "greenlet>=3.2", "psycopg[binary]>=3.2.9", "sqlalchemy>=2.0.41", "dbx-tools-core==0.6.224", "dbx-tools-core-rs==0.6.224" ]
|
|
12
12
|
|
|
13
13
|
[project.urls]
|
|
14
14
|
Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/postgres"
|
|
15
15
|
|
|
16
16
|
[build-system]
|
|
17
|
-
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
17
|
+
requires = [ "uv_build>=0.11.28,<0.12.0" ]
|
|
18
18
|
build-backend = "uv_build"
|
|
19
19
|
|
|
20
20
|
[tool.uv.build-backend]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Lakebase address types and parsers backed by the Databricks Rust bindings."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import TypeAlias
|
|
4
4
|
|
|
5
5
|
from dbx_tools.core_rs.bindings import (
|
|
6
6
|
ParsedAddress,
|
|
@@ -11,11 +11,11 @@ from dbx_tools.core_rs.bindings import (
|
|
|
11
11
|
SslMode as NativeSslMode,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
SslMode =
|
|
15
|
-
"""
|
|
14
|
+
SslMode: TypeAlias = NativeSslMode
|
|
15
|
+
"""Generated PostgreSQL SSL mode enum."""
|
|
16
16
|
|
|
17
|
-
SSL_MODES: tuple[
|
|
18
|
-
"""
|
|
17
|
+
SSL_MODES: tuple[str, ...] = tuple(mode.name.lower() for mode in NativeSslMode)
|
|
18
|
+
"""Accepted configuration spellings derived from :class:`SslMode`."""
|
|
19
19
|
|
|
20
20
|
LakebaseConnectionInputs: TypeAlias = ParsedAddress
|
|
21
21
|
"""Resolved address inputs returned by the native parser."""
|
|
@@ -10,6 +10,11 @@ from collections.abc import Callable, Mapping
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import Any, Protocol
|
|
12
12
|
|
|
13
|
+
from databricks.sdk.service.postgres import (
|
|
14
|
+
Endpoint,
|
|
15
|
+
EndpointType,
|
|
16
|
+
PostgresAPI,
|
|
17
|
+
)
|
|
13
18
|
from sqlalchemy import URL, Engine, event
|
|
14
19
|
from sqlalchemy import create_engine as sqlalchemy_create_engine
|
|
15
20
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
@@ -30,27 +35,21 @@ CredentialProvider = Callable[[], str]
|
|
|
30
35
|
CredentialLoader = Callable[[], tuple[str, dt.datetime | None]]
|
|
31
36
|
"""Load a database credential and its optional absolute expiration time."""
|
|
32
37
|
|
|
33
|
-
_API_BASE = "/api/2.0/postgres"
|
|
34
38
|
_CREDENTIAL_REFRESH_LEAD = dt.timedelta(minutes=5)
|
|
35
39
|
_DEFAULT_CREDENTIAL_LIFETIME = dt.timedelta(minutes=50)
|
|
36
40
|
_MINIMUM_CREDENTIAL_LIFETIME = dt.timedelta(minutes=1)
|
|
37
41
|
_DEFAULT_DATABASE = "databricks_postgres"
|
|
38
42
|
_DEFAULT_PORT = 5432
|
|
39
|
-
_DEFAULT_SSL_MODE: SslMode =
|
|
40
|
-
_READ_WRITE_ENDPOINT_TYPE = "ENDPOINT_TYPE_READ_WRITE"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class WorkspaceApiClient(Protocol):
|
|
44
|
-
def do(self, method: str, path: str, **kwargs: Any) -> Any: ...
|
|
43
|
+
_DEFAULT_SSL_MODE: SslMode = NativeSslMode.REQUIRE
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
class WorkspaceClientLike(Protocol):
|
|
48
47
|
"""WorkspaceClient surface required for Lakebase discovery and credentials."""
|
|
49
48
|
|
|
50
|
-
api_client: WorkspaceApiClient
|
|
51
49
|
config: Any
|
|
52
50
|
current_user: Any
|
|
53
51
|
database: Any
|
|
52
|
+
postgres: PostgresAPI
|
|
54
53
|
|
|
55
54
|
|
|
56
55
|
@dataclass(frozen=True, slots=True)
|
|
@@ -71,7 +70,7 @@ class PostgresEngineConfig:
|
|
|
71
70
|
database: str | None = None
|
|
72
71
|
host: str | None = None
|
|
73
72
|
port: int | None = None
|
|
74
|
-
ssl_mode: SslMode | None = None
|
|
73
|
+
ssl_mode: SslMode | str | None = None
|
|
75
74
|
user: str | None = None
|
|
76
75
|
|
|
77
76
|
|
|
@@ -104,7 +103,7 @@ class ResolvedPostgresConnection:
|
|
|
104
103
|
host=self.host,
|
|
105
104
|
port=self.port,
|
|
106
105
|
database=self.database,
|
|
107
|
-
query={ssl_parameter: self.ssl_mode},
|
|
106
|
+
query={ssl_parameter: self.ssl_mode.name.lower()},
|
|
108
107
|
)
|
|
109
108
|
|
|
110
109
|
|
|
@@ -163,24 +162,19 @@ def autoscaling_credential_provider(
|
|
|
163
162
|
) -> CredentialProvider:
|
|
164
163
|
"""Create a cached Autoscaling-Lakebase endpoint credential provider.
|
|
165
164
|
|
|
166
|
-
Tokens come from
|
|
167
|
-
|
|
165
|
+
Tokens come from the typed Postgres SDK and use the same process-local
|
|
166
|
+
early-refresh and refresh-lock policy as provisioned tokens.
|
|
168
167
|
"""
|
|
169
168
|
|
|
170
169
|
if not endpoint.strip():
|
|
171
170
|
raise ValueError("endpoint must not be empty")
|
|
172
171
|
|
|
173
172
|
def load() -> tuple[str, dt.datetime | None]:
|
|
174
|
-
credential = workspace_client.
|
|
175
|
-
|
|
176
|
-
f"{_API_BASE}/credentials",
|
|
177
|
-
headers={"Accept": "application/json", "Content-Type": "application/json"},
|
|
178
|
-
body={"endpoint": endpoint},
|
|
179
|
-
)
|
|
180
|
-
token = credential.get("token") if isinstance(credential, dict) else None
|
|
173
|
+
credential = workspace_client.postgres.generate_database_credential(endpoint)
|
|
174
|
+
token = credential.token
|
|
181
175
|
if not isinstance(token, str) or not token:
|
|
182
176
|
raise RuntimeError("WorkspaceClient returned no Lakebase database credential token")
|
|
183
|
-
return token,
|
|
177
|
+
return token, _credential_expiration(credential.expire_time)
|
|
184
178
|
|
|
185
179
|
return _CachedCredentialProvider(load)
|
|
186
180
|
|
|
@@ -295,15 +289,14 @@ def resolve_postgres_connection(
|
|
|
295
289
|
if endpoint:
|
|
296
290
|
endpoint_parts = parse_address(endpoint)
|
|
297
291
|
if endpoint_parts.project and endpoint_parts.branch and endpoint_parts.endpoint_id:
|
|
298
|
-
response =
|
|
299
|
-
|
|
300
|
-
f"{
|
|
301
|
-
f"{endpoint_parts.branch}/endpoints/{endpoint_parts.endpoint_id}",
|
|
292
|
+
response = workspace_client.postgres.get_endpoint(
|
|
293
|
+
f"projects/{endpoint_parts.project}/branches/"
|
|
294
|
+
f"{endpoint_parts.branch}/endpoints/{endpoint_parts.endpoint_id}"
|
|
302
295
|
)
|
|
303
296
|
host = _endpoint_host(response)
|
|
304
297
|
if not host:
|
|
305
298
|
endpoint_record = _pick_endpoint(workspace_client, project, branch)
|
|
306
|
-
endpoint = _string(endpoint_record.
|
|
299
|
+
endpoint = _string(endpoint_record.name)
|
|
307
300
|
host = _endpoint_host(endpoint_record)
|
|
308
301
|
|
|
309
302
|
if not database and project and branch:
|
|
@@ -343,6 +336,9 @@ def _credential_renewal(expiration: dt.datetime | None) -> dt.datetime:
|
|
|
343
336
|
def _credential_expiration(value: object) -> dt.datetime | None:
|
|
344
337
|
if isinstance(value, dt.datetime):
|
|
345
338
|
return value
|
|
339
|
+
to_datetime = getattr(value, "ToDatetime", None)
|
|
340
|
+
if callable(to_datetime):
|
|
341
|
+
return to_datetime(tzinfo=dt.UTC)
|
|
346
342
|
if isinstance(value, (int, float)):
|
|
347
343
|
try:
|
|
348
344
|
return dt.datetime.fromtimestamp(value, tz=dt.UTC)
|
|
@@ -357,14 +353,6 @@ def _credential_expiration(value: object) -> dt.datetime | None:
|
|
|
357
353
|
return None
|
|
358
354
|
|
|
359
355
|
|
|
360
|
-
def _mapping_credential_expiration(credential: Mapping[str, object]) -> dt.datetime | None:
|
|
361
|
-
for key in ("expiration_time", "expirationTime", "expires_at", "expiresAt"):
|
|
362
|
-
expiration = _credential_expiration(credential.get(key))
|
|
363
|
-
if expiration is not None:
|
|
364
|
-
return expiration
|
|
365
|
-
return None
|
|
366
|
-
|
|
367
|
-
|
|
368
356
|
def _utcnow() -> dt.datetime:
|
|
369
357
|
return dt.datetime.now(tz=dt.UTC)
|
|
370
358
|
|
|
@@ -391,23 +379,9 @@ def _workspace_user(workspace_client: WorkspaceClientLike) -> str | None:
|
|
|
391
379
|
)
|
|
392
380
|
|
|
393
381
|
|
|
394
|
-
def _get(workspace_client: WorkspaceClientLike, path: str) -> dict[str, Any]:
|
|
395
|
-
value = workspace_client.api_client.do("GET", path, headers={"Accept": "application/json"})
|
|
396
|
-
if not isinstance(value, dict):
|
|
397
|
-
raise TypeError(f"WorkspaceClient returned a non-object response for {path}")
|
|
398
|
-
return value
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def _list(workspace_client: WorkspaceClientLike, path: str, key: str) -> list[dict[str, Any]]:
|
|
402
|
-
values = _get(workspace_client, path).get(key, [])
|
|
403
|
-
if not isinstance(values, list):
|
|
404
|
-
raise TypeError(f"WorkspaceClient returned a non-list {key} response for {path}")
|
|
405
|
-
return [value for value in values if isinstance(value, dict)]
|
|
406
|
-
|
|
407
|
-
|
|
408
382
|
def _pick_project(workspace_client: WorkspaceClientLike) -> str:
|
|
409
|
-
projects =
|
|
410
|
-
candidates = [_resource_part(project.
|
|
383
|
+
projects = workspace_client.postgres.list_projects()
|
|
384
|
+
candidates = [_resource_part(project.name, "project") for project in projects]
|
|
411
385
|
candidates = [candidate for candidate in candidates if candidate]
|
|
412
386
|
if len(candidates) != 1:
|
|
413
387
|
raise ValueError(f"Expected one Lakebase project, found: {', '.join(candidates) or 'none'}")
|
|
@@ -415,20 +389,16 @@ def _pick_project(workspace_client: WorkspaceClientLike) -> str:
|
|
|
415
389
|
|
|
416
390
|
|
|
417
391
|
def _pick_branch(workspace_client: WorkspaceClientLike, project: str) -> str:
|
|
418
|
-
branches =
|
|
419
|
-
workspace_client,
|
|
420
|
-
f"{_API_BASE}/projects/{project}/branches",
|
|
421
|
-
"branches",
|
|
422
|
-
)
|
|
392
|
+
branches = list(workspace_client.postgres.list_branches(f"projects/{project}"))
|
|
423
393
|
selected = next(
|
|
424
394
|
(
|
|
425
395
|
branch
|
|
426
396
|
for branch in branches
|
|
427
|
-
if
|
|
397
|
+
if branch.status is not None and branch.status.default is True
|
|
428
398
|
),
|
|
429
399
|
branches[0] if len(branches) == 1 else None,
|
|
430
400
|
)
|
|
431
|
-
branch = _resource_part(selected.
|
|
401
|
+
branch = _resource_part(selected.name if selected else None, "branch")
|
|
432
402
|
if not branch:
|
|
433
403
|
raise ValueError(f"Could not choose a Lakebase branch for project {project}")
|
|
434
404
|
return branch
|
|
@@ -438,18 +408,16 @@ def _pick_endpoint(
|
|
|
438
408
|
workspace_client: WorkspaceClientLike,
|
|
439
409
|
project: str,
|
|
440
410
|
branch: str,
|
|
441
|
-
) ->
|
|
442
|
-
endpoints =
|
|
443
|
-
workspace_client
|
|
444
|
-
f"{_API_BASE}/projects/{project}/branches/{branch}/endpoints",
|
|
445
|
-
"endpoints",
|
|
411
|
+
) -> Endpoint:
|
|
412
|
+
endpoints = list(
|
|
413
|
+
workspace_client.postgres.list_endpoints(f"projects/{project}/branches/{branch}")
|
|
446
414
|
)
|
|
447
415
|
selected = next(
|
|
448
416
|
(
|
|
449
417
|
endpoint
|
|
450
418
|
for endpoint in endpoints
|
|
451
|
-
if
|
|
452
|
-
and endpoint
|
|
419
|
+
if endpoint.status is not None
|
|
420
|
+
and endpoint.status.endpoint_type is EndpointType.ENDPOINT_TYPE_READ_WRITE
|
|
453
421
|
),
|
|
454
422
|
endpoints[0] if len(endpoints) == 1 else None,
|
|
455
423
|
)
|
|
@@ -464,21 +432,19 @@ def _pick_database(
|
|
|
464
432
|
branch: str,
|
|
465
433
|
resource_id: str | None,
|
|
466
434
|
) -> str:
|
|
467
|
-
databases =
|
|
468
|
-
workspace_client
|
|
469
|
-
f"{_API_BASE}/projects/{project}/branches/{branch}/databases",
|
|
470
|
-
"databases",
|
|
435
|
+
databases = list(
|
|
436
|
+
workspace_client.postgres.list_databases(f"projects/{project}/branches/{branch}")
|
|
471
437
|
)
|
|
472
438
|
if resource_id:
|
|
473
439
|
databases = [
|
|
474
440
|
database
|
|
475
441
|
for database in databases
|
|
476
|
-
if _resource_part(database.
|
|
442
|
+
if _resource_part(database.name, "database") == resource_id
|
|
477
443
|
]
|
|
478
444
|
names = [
|
|
479
|
-
_string(database.
|
|
445
|
+
_string(database.status.postgres_database)
|
|
480
446
|
for database in databases
|
|
481
|
-
if
|
|
447
|
+
if database.status is not None
|
|
482
448
|
]
|
|
483
449
|
names = [name for name in names if name]
|
|
484
450
|
if _DEFAULT_DATABASE in names:
|
|
@@ -492,32 +458,25 @@ def _find_endpoint_by_host(
|
|
|
492
458
|
workspace_client: WorkspaceClientLike,
|
|
493
459
|
host: str,
|
|
494
460
|
) -> tuple[str, str, str] | None:
|
|
495
|
-
for project_record in
|
|
496
|
-
project = _resource_part(project_record.
|
|
461
|
+
for project_record in workspace_client.postgres.list_projects():
|
|
462
|
+
project = _resource_part(project_record.name, "project")
|
|
497
463
|
if not project:
|
|
498
464
|
continue
|
|
499
|
-
for branch_record in
|
|
500
|
-
|
|
501
|
-
f"{_API_BASE}/projects/{project}/branches",
|
|
502
|
-
"branches",
|
|
503
|
-
):
|
|
504
|
-
branch = _resource_part(branch_record.get("name"), "branch")
|
|
465
|
+
for branch_record in workspace_client.postgres.list_branches(f"projects/{project}"):
|
|
466
|
+
branch = _resource_part(branch_record.name, "branch")
|
|
505
467
|
if not branch:
|
|
506
468
|
continue
|
|
507
|
-
for endpoint in
|
|
508
|
-
|
|
509
|
-
f"{_API_BASE}/projects/{project}/branches/{branch}/endpoints",
|
|
510
|
-
"endpoints",
|
|
469
|
+
for endpoint in workspace_client.postgres.list_endpoints(
|
|
470
|
+
f"projects/{project}/branches/{branch}"
|
|
511
471
|
):
|
|
512
|
-
if _endpoint_host(endpoint) == host and (name := _string(endpoint.
|
|
472
|
+
if _endpoint_host(endpoint) == host and (name := _string(endpoint.name)):
|
|
513
473
|
return project, branch, name
|
|
514
474
|
return None
|
|
515
475
|
|
|
516
476
|
|
|
517
|
-
def _endpoint_host(endpoint:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return _string(hosts.get("host")) if isinstance(hosts, dict) else None
|
|
477
|
+
def _endpoint_host(endpoint: Endpoint) -> str | None:
|
|
478
|
+
hosts = endpoint.status.hosts if endpoint.status is not None else None
|
|
479
|
+
return _string(hosts.host) if hosts is not None else None
|
|
521
480
|
|
|
522
481
|
|
|
523
482
|
def _resource_part(value: object, kind: str) -> str | None:
|
|
@@ -545,10 +504,14 @@ def _parse_port(value: object) -> int:
|
|
|
545
504
|
def _parse_ssl_mode(value: object) -> SslMode:
|
|
546
505
|
if value is None or value == "":
|
|
547
506
|
return _DEFAULT_SSL_MODE
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
507
|
+
if isinstance(value, NativeSslMode):
|
|
508
|
+
return value
|
|
509
|
+
normalized = str(value).strip().upper()
|
|
510
|
+
try:
|
|
511
|
+
return NativeSslMode[normalized]
|
|
512
|
+
except KeyError:
|
|
513
|
+
accepted = ", ".join(SSL_MODES)
|
|
514
|
+
raise ValueError(f"PGSSLMODE must be one of {accepted}, got {value!r}") from None
|
|
552
515
|
|
|
553
516
|
|
|
554
517
|
def _first(*values: Any) -> Any:
|
|
File without changes
|
{dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/__init__.py
RENAMED
|
File without changes
|
{dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/advisory_lock.py
RENAMED
|
File without changes
|
{dbx_tools_postgres-0.6.222 → dbx_tools_postgres-0.6.224}/src/dbx_tools/postgres/topic_bus.py
RENAMED
|
File without changes
|