dbx-tools-postgres 0.6.78__py3-none-any.whl

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.
@@ -0,0 +1,109 @@
1
+ from .address import (
2
+ SSL_MODES,
3
+ LakebaseConnectionInputs,
4
+ ParsedAddress,
5
+ SslMode,
6
+ parse_address,
7
+ parse_resource_path,
8
+ parseAddress,
9
+ parseResourcePath,
10
+ )
11
+ from .advisory_lock import (
12
+ ExplicitAdvisoryLockId,
13
+ acquire_advisory_lock,
14
+ acquire_advisory_lock_async,
15
+ advisory_lock,
16
+ advisory_lock_async,
17
+ advisory_lock_id,
18
+ advisory_transaction_lock,
19
+ advisory_transaction_lock_async,
20
+ explicit_advisory_lock_id,
21
+ release_advisory_lock,
22
+ release_advisory_lock_async,
23
+ try_advisory_lock,
24
+ try_advisory_lock_async,
25
+ try_advisory_transaction_lock,
26
+ try_advisory_transaction_lock_async,
27
+ )
28
+ from .engine import (
29
+ CredentialProvider,
30
+ PostgresEngineConfig,
31
+ ResolvedPostgresConnection,
32
+ WorkspaceClientLike,
33
+ autoscaling_credential_provider,
34
+ autoscalingCredentialProvider,
35
+ create_async_engine,
36
+ create_engine,
37
+ createAsyncEngine,
38
+ createEngine,
39
+ install_credential_injection,
40
+ installCredentialInjection,
41
+ resolve_postgres_connection,
42
+ resolvePostgresConnection,
43
+ workspace_credential_provider,
44
+ workspaceCredentialProvider,
45
+ )
46
+ from .topic_bus import (
47
+ PostgresTopicBus,
48
+ PostgresTopicBusOptions,
49
+ SerializableValue,
50
+ TopicListener,
51
+ TopicMessage,
52
+ TopicMetadata,
53
+ TopicMetadataProvider,
54
+ TopicPublishInput,
55
+ channel_name,
56
+ channelName,
57
+ )
58
+
59
+ __all__ = [
60
+ "SSL_MODES",
61
+ "CredentialProvider",
62
+ "ExplicitAdvisoryLockId",
63
+ "LakebaseConnectionInputs",
64
+ "ParsedAddress",
65
+ "PostgresEngineConfig",
66
+ "PostgresTopicBus",
67
+ "PostgresTopicBusOptions",
68
+ "ResolvedPostgresConnection",
69
+ "SerializableValue",
70
+ "SslMode",
71
+ "TopicListener",
72
+ "TopicMessage",
73
+ "TopicMetadata",
74
+ "TopicMetadataProvider",
75
+ "TopicPublishInput",
76
+ "WorkspaceClientLike",
77
+ "acquire_advisory_lock",
78
+ "acquire_advisory_lock_async",
79
+ "advisory_lock",
80
+ "advisory_lock_async",
81
+ "advisory_lock_id",
82
+ "advisory_transaction_lock",
83
+ "advisory_transaction_lock_async",
84
+ "autoscalingCredentialProvider",
85
+ "autoscaling_credential_provider",
86
+ "channelName",
87
+ "channel_name",
88
+ "createAsyncEngine",
89
+ "createEngine",
90
+ "create_async_engine",
91
+ "create_engine",
92
+ "explicit_advisory_lock_id",
93
+ "installCredentialInjection",
94
+ "install_credential_injection",
95
+ "parseAddress",
96
+ "parseResourcePath",
97
+ "parse_address",
98
+ "parse_resource_path",
99
+ "release_advisory_lock",
100
+ "release_advisory_lock_async",
101
+ "resolvePostgresConnection",
102
+ "resolve_postgres_connection",
103
+ "try_advisory_lock",
104
+ "try_advisory_lock_async",
105
+ "try_advisory_transaction_lock",
106
+ "try_advisory_transaction_lock_async",
107
+ "workspaceCredentialProvider",
108
+ "workspace_credential_provider",
109
+ ]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import asdict, dataclass
5
+ from typing import Literal
6
+ from urllib.parse import parse_qs, unquote, urlparse
7
+
8
+ SslMode = Literal["require", "disable", "prefer"]
9
+ SSL_MODES: tuple[SslMode, ...] = ("require", "disable", "prefer")
10
+
11
+ _URL_SCHEME_RE = re.compile(r"^(postgres|postgresql)://", re.IGNORECASE)
12
+ _PROJECT_ID_RE = re.compile(r"^[a-z][a-z0-9-]{0,61}[a-z0-9]$|^[a-z]$")
13
+ _HOSTNAME_HINT_RE = re.compile(r"^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$", re.IGNORECASE)
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class LakebaseConnectionInputs:
18
+ project: str | None = None
19
+ branch: str | None = None
20
+ endpoint: str | None = None
21
+ database: str | None = None
22
+ host: str | None = None
23
+ port: int | None = None
24
+ ssl_mode: SslMode | None = None
25
+
26
+ def as_dict(self) -> dict[str, object]:
27
+ return {key: value for key, value in asdict(self).items() if value is not None}
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ParsedAddress(LakebaseConnectionInputs):
32
+ endpoint_id: str | None = None
33
+ database_resource_id: str | None = None
34
+ user: str | None = None
35
+
36
+
37
+ def parse_address(value: str | None) -> ParsedAddress:
38
+ if not value or not (address := value.strip()):
39
+ return ParsedAddress()
40
+ if _URL_SCHEME_RE.match(address):
41
+ return _parse_uri(address)
42
+ if address.startswith("projects/"):
43
+ return _parse_resource_path_segments(address)
44
+ if "." in address and _HOSTNAME_HINT_RE.fullmatch(address):
45
+ return ParsedAddress(host=address)
46
+ if _PROJECT_ID_RE.fullmatch(address):
47
+ return ParsedAddress(project=address)
48
+ return ParsedAddress()
49
+
50
+
51
+ def parse_resource_path(value: str | None) -> ParsedAddress:
52
+ if not value or not (address := value.strip()).startswith("projects/"):
53
+ return ParsedAddress()
54
+ return _parse_resource_path_segments(address)
55
+
56
+
57
+ def _parse_uri(address: str) -> ParsedAddress:
58
+ try:
59
+ parsed = urlparse(address)
60
+ port = parsed.port
61
+ except ValueError:
62
+ return ParsedAddress()
63
+ if parsed.scheme.lower() not in {"postgres", "postgresql"}:
64
+ return ParsedAddress()
65
+ ssl_mode_value = parse_qs(parsed.query).get("sslmode") or parse_qs(parsed.query).get("sslMode")
66
+ ssl_mode = ssl_mode_value[0].lower() if ssl_mode_value else None
67
+ return ParsedAddress(
68
+ host=parsed.hostname or None,
69
+ port=port,
70
+ user=unquote(parsed.username) if parsed.username else None,
71
+ database=unquote(parsed.path.removeprefix("/")) or None,
72
+ ssl_mode=ssl_mode if ssl_mode in SSL_MODES else None,
73
+ )
74
+
75
+
76
+ def _parse_resource_path_segments(address: str) -> ParsedAddress:
77
+ parts = address.split("/")
78
+ if len(parts) < 2 or parts[0] != "projects" or not parts[1]:
79
+ return ParsedAddress()
80
+ project = parts[1]
81
+ if len(parts) == 2:
82
+ return ParsedAddress(project=project)
83
+ if len(parts) == 4 and parts[2] == "branches" and parts[3]:
84
+ return ParsedAddress(project=project, branch=parts[3])
85
+ if (
86
+ len(parts) == 6
87
+ and parts[2] == "branches"
88
+ and parts[3]
89
+ and parts[4] == "endpoints"
90
+ and parts[5]
91
+ ):
92
+ return ParsedAddress(
93
+ project=project,
94
+ branch=parts[3],
95
+ endpoint=address,
96
+ endpoint_id=parts[5],
97
+ )
98
+ if (
99
+ len(parts) == 6
100
+ and parts[2] == "branches"
101
+ and parts[3]
102
+ and parts[4] == "databases"
103
+ and parts[5]
104
+ ):
105
+ return ParsedAddress(
106
+ project=project,
107
+ branch=parts[3],
108
+ database_resource_id=parts[5],
109
+ )
110
+ return ParsedAddress()
111
+
112
+
113
+ parseAddress = parse_address
114
+ parseResourcePath = parse_resource_path
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from collections.abc import AsyncIterator, Iterator
5
+ from contextlib import asynccontextmanager, contextmanager
6
+ from dataclasses import dataclass
7
+ from typing import Protocol
8
+
9
+ from dbx_tools.core import to_stable_key
10
+ from sqlalchemy import text
11
+ from sqlalchemy.engine import Connection, Engine
12
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ExplicitAdvisoryLockId:
17
+ value: int
18
+
19
+
20
+ class SyncQueryable(Protocol):
21
+ def execute(self, statement: object, parameters: dict[str, object] | None = None) -> object: ...
22
+
23
+
24
+ class AsyncQueryable(Protocol):
25
+ async def execute(
26
+ self, statement: object, parameters: dict[str, object] | None = None
27
+ ) -> object: ...
28
+
29
+
30
+ def advisory_lock_id(key: object) -> int:
31
+ if isinstance(key, ExplicitAdvisoryLockId):
32
+ return _signed_64(key.value)
33
+ parts = key if isinstance(key, (list, tuple)) else [key]
34
+ canonical = "\0".join(to_stable_key(part) for part in parts)
35
+ return int.from_bytes(hashlib.sha256(canonical.encode()).digest()[:8], "big", signed=True)
36
+
37
+
38
+ def explicit_advisory_lock_id(value: int) -> ExplicitAdvisoryLockId:
39
+ return ExplicitAdvisoryLockId(value)
40
+
41
+
42
+ def acquire_advisory_lock(
43
+ connection: SyncQueryable,
44
+ key: object,
45
+ *,
46
+ transaction: bool = False,
47
+ wait: bool = True,
48
+ ) -> bool:
49
+ function = _function_name(transaction=transaction, wait=wait)
50
+ result = connection.execute(
51
+ text(f"SELECT {function}(:lock_id)"), {"lock_id": advisory_lock_id(key)}
52
+ )
53
+ return True if wait else bool(result.scalar_one())
54
+
55
+
56
+ async def acquire_advisory_lock_async(
57
+ connection: AsyncQueryable,
58
+ key: object,
59
+ *,
60
+ transaction: bool = False,
61
+ wait: bool = True,
62
+ ) -> bool:
63
+ function = _function_name(transaction=transaction, wait=wait)
64
+ result = await connection.execute(
65
+ text(f"SELECT {function}(:lock_id)"), {"lock_id": advisory_lock_id(key)}
66
+ )
67
+ return True if wait else bool(result.scalar_one())
68
+
69
+
70
+ def release_advisory_lock(connection: SyncQueryable, key: object) -> None:
71
+ lock_id = advisory_lock_id(key)
72
+ result = connection.execute(text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": lock_id})
73
+ if result.scalar_one() is not True:
74
+ raise RuntimeError(f"Postgres advisory lock {lock_id} was not held by this connection")
75
+
76
+
77
+ async def release_advisory_lock_async(connection: AsyncQueryable, key: object) -> None:
78
+ lock_id = advisory_lock_id(key)
79
+ result = await connection.execute(
80
+ text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": lock_id}
81
+ )
82
+ if result.scalar_one() is not True:
83
+ raise RuntimeError(f"Postgres advisory lock {lock_id} was not held by this connection")
84
+
85
+
86
+ @contextmanager
87
+ def advisory_lock(engine: Engine, key: object) -> Iterator[Connection]:
88
+ with engine.connect() as connection:
89
+ acquire_advisory_lock(connection, key)
90
+ failure: Exception | None = None
91
+ try:
92
+ yield connection
93
+ except Exception as error: # noqa: BLE001
94
+ failure = error
95
+ unlock_failure = _capture_release(connection, key)
96
+ if failure is not None:
97
+ if unlock_failure is not None:
98
+ failure.add_note(f"Advisory lock release also failed: {unlock_failure}")
99
+ raise failure
100
+ if unlock_failure is not None:
101
+ raise unlock_failure
102
+
103
+
104
+ @contextmanager
105
+ def try_advisory_lock(engine: Engine, key: object) -> Iterator[Connection | None]:
106
+ with engine.connect() as connection:
107
+ if not acquire_advisory_lock(connection, key, wait=False):
108
+ yield None
109
+ return
110
+ failure: Exception | None = None
111
+ try:
112
+ yield connection
113
+ except Exception as error: # noqa: BLE001
114
+ failure = error
115
+ unlock_failure = _capture_release(connection, key)
116
+ if failure is not None:
117
+ if unlock_failure is not None:
118
+ failure.add_note(f"Advisory lock release also failed: {unlock_failure}")
119
+ raise failure
120
+ if unlock_failure is not None:
121
+ raise unlock_failure
122
+
123
+
124
+ @contextmanager
125
+ def advisory_transaction_lock(engine: Engine, key: object) -> Iterator[Connection]:
126
+ with engine.begin() as connection:
127
+ acquire_advisory_lock(connection, key, transaction=True)
128
+ yield connection
129
+
130
+
131
+ @contextmanager
132
+ def try_advisory_transaction_lock(engine: Engine, key: object) -> Iterator[Connection | None]:
133
+ with engine.begin() as connection:
134
+ if not acquire_advisory_lock(connection, key, transaction=True, wait=False):
135
+ yield None
136
+ return
137
+ yield connection
138
+
139
+
140
+ @asynccontextmanager
141
+ async def advisory_lock_async(engine: AsyncEngine, key: object) -> AsyncIterator[AsyncConnection]:
142
+ async with engine.connect() as connection:
143
+ await acquire_advisory_lock_async(connection, key)
144
+ failure: Exception | None = None
145
+ try:
146
+ yield connection
147
+ except Exception as error: # noqa: BLE001
148
+ failure = error
149
+ unlock_failure = await _capture_release_async(connection, key)
150
+ if failure is not None:
151
+ if unlock_failure is not None:
152
+ failure.add_note(f"Advisory lock release also failed: {unlock_failure}")
153
+ raise failure
154
+ if unlock_failure is not None:
155
+ raise unlock_failure
156
+
157
+
158
+ @asynccontextmanager
159
+ async def try_advisory_lock_async(
160
+ engine: AsyncEngine, key: object
161
+ ) -> AsyncIterator[AsyncConnection | None]:
162
+ async with engine.connect() as connection:
163
+ if not await acquire_advisory_lock_async(connection, key, wait=False):
164
+ yield None
165
+ return
166
+ failure: Exception | None = None
167
+ try:
168
+ yield connection
169
+ except Exception as error: # noqa: BLE001
170
+ failure = error
171
+ unlock_failure = await _capture_release_async(connection, key)
172
+ if failure is not None:
173
+ if unlock_failure is not None:
174
+ failure.add_note(f"Advisory lock release also failed: {unlock_failure}")
175
+ raise failure
176
+ if unlock_failure is not None:
177
+ raise unlock_failure
178
+
179
+
180
+ @asynccontextmanager
181
+ async def advisory_transaction_lock_async(
182
+ engine: AsyncEngine, key: object
183
+ ) -> AsyncIterator[AsyncConnection]:
184
+ async with engine.begin() as connection:
185
+ await acquire_advisory_lock_async(connection, key, transaction=True)
186
+ yield connection
187
+
188
+
189
+ @asynccontextmanager
190
+ async def try_advisory_transaction_lock_async(
191
+ engine: AsyncEngine, key: object
192
+ ) -> AsyncIterator[AsyncConnection | None]:
193
+ async with engine.begin() as connection:
194
+ if not await acquire_advisory_lock_async(connection, key, transaction=True, wait=False):
195
+ yield None
196
+ return
197
+ yield connection
198
+
199
+
200
+ def _function_name(*, transaction: bool, wait: bool) -> str:
201
+ if transaction:
202
+ return "pg_advisory_xact_lock" if wait else "pg_try_advisory_xact_lock"
203
+ return "pg_advisory_lock" if wait else "pg_try_advisory_lock"
204
+
205
+
206
+ def _signed_64(value: int) -> int:
207
+ return ((value + 2**63) % 2**64) - 2**63
208
+
209
+
210
+ def _capture_release(connection: SyncQueryable, key: object) -> Exception | None:
211
+ try:
212
+ release_advisory_lock(connection, key)
213
+ except Exception as error: # noqa: BLE001
214
+ return error
215
+ return None
216
+
217
+
218
+ async def _capture_release_async(connection: AsyncQueryable, key: object) -> Exception | None:
219
+ try:
220
+ await release_advisory_lock_async(connection, key)
221
+ except Exception as error: # noqa: BLE001
222
+ return error
223
+ return None