macss-modular-api-postgres 0.4.7__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.
- macss_modular_api_postgres-0.4.7.dist-info/METADATA +65 -0
- macss_modular_api_postgres-0.4.7.dist-info/RECORD +6 -0
- macss_modular_api_postgres-0.4.7.dist-info/WHEEL +5 -0
- macss_modular_api_postgres-0.4.7.dist-info/top_level.txt +1 -0
- modular_api_postgres/__init__.py +53 -0
- modular_api_postgres/db_client.py +425 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: macss-modular-api-postgres
|
|
3
|
+
Version: 0.4.7
|
|
4
|
+
Summary: Official MACSS Postgres integration package for Python.
|
|
5
|
+
Author: ccisne.dev
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/macss-dev/modular_api
|
|
8
|
+
Project-URL: Repository, https://github.com/macss-dev/modular_api/tree/main/code/py/modular_api_postgres
|
|
9
|
+
Project-URL: Issues, https://github.com/macss-dev/modular_api/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/macss-dev/modular_api/tree/main/code/py/modular_api_postgres#readme
|
|
11
|
+
Keywords: macss,postgres,database
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# macss-modular-api-postgres
|
|
26
|
+
|
|
27
|
+
Official MACSS Postgres integration package for Python.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from modular_api_postgres import DbClient, DbCommand, DbCommandKind, DbConnectionSettings
|
|
33
|
+
|
|
34
|
+
settings = DbConnectionSettings.from_environment()
|
|
35
|
+
|
|
36
|
+
client = DbClient(
|
|
37
|
+
settings=settings,
|
|
38
|
+
session_provider=my_session_provider,
|
|
39
|
+
command_executor=my_command_executor,
|
|
40
|
+
transaction_runner=my_transaction_runner,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = client.scalar(
|
|
44
|
+
DbCommand(
|
|
45
|
+
kind=DbCommandKind.SCALAR,
|
|
46
|
+
text="select count(*) from users",
|
|
47
|
+
label="users.count",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if result.is_success:
|
|
52
|
+
print(result.value.value)
|
|
53
|
+
else:
|
|
54
|
+
print(result.failure.message)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [example/example.py](example/example.py) for a complete in-memory wiring sample.
|
|
58
|
+
|
|
59
|
+
## Current slice
|
|
60
|
+
|
|
61
|
+
- normalized Postgres connection defaults and redacted summaries
|
|
62
|
+
- engine-agnostic `DbClient`, `DbRepository`, and transaction contracts
|
|
63
|
+
- explicit lease ownership semantics for package-owned and application-owned sessions
|
|
64
|
+
- health contributor and GraphQL support bundle for higher-level integrations
|
|
65
|
+
- real driver bindings intentionally remain outside this first slice
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
modular_api_postgres/__init__.py,sha256=URrLgEvTQBn48LkyY7aJsz5vzwC38lEo_AMgF1JY0a4,1089
|
|
2
|
+
modular_api_postgres/db_client.py,sha256=ixskWFwjikbIV_OsHCncYTjbKZTR4ZQdtbLD1ErmN0A,12959
|
|
3
|
+
macss_modular_api_postgres-0.4.7.dist-info/METADATA,sha256=j4IyD8lHfFMi_0Gynd5fU_soWWWfxyV_mNN7ylC7DZU,2196
|
|
4
|
+
macss_modular_api_postgres-0.4.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
macss_modular_api_postgres-0.4.7.dist-info/top_level.txt,sha256=256SRpi7HI1fM37vDEnZVT6An9ivmo9YcA7TJN1Dmuk,21
|
|
6
|
+
macss_modular_api_postgres-0.4.7.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
modular_api_postgres
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Public package exports for modular_api_postgres."""
|
|
2
|
+
|
|
3
|
+
from .db_client import (
|
|
4
|
+
DbClient,
|
|
5
|
+
DbCommand,
|
|
6
|
+
DbCommandExecutor,
|
|
7
|
+
DbCommandKind,
|
|
8
|
+
DbConnectionSettings,
|
|
9
|
+
DbExecutionMetadata,
|
|
10
|
+
DbExecutionSummary,
|
|
11
|
+
DbFailure,
|
|
12
|
+
DbFailureKind,
|
|
13
|
+
DbGraphqlSupport,
|
|
14
|
+
DbHealthContributor,
|
|
15
|
+
DbHealthReport,
|
|
16
|
+
DbHealthStatus,
|
|
17
|
+
DbProviderDescription,
|
|
18
|
+
DbRepository,
|
|
19
|
+
DbRepositoryContext,
|
|
20
|
+
DbResult,
|
|
21
|
+
DbRowSet,
|
|
22
|
+
DbScalar,
|
|
23
|
+
DbSessionLease,
|
|
24
|
+
DbSessionProvider,
|
|
25
|
+
DbTransactionContext,
|
|
26
|
+
DbTransactionRunner,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DbClient",
|
|
31
|
+
"DbCommand",
|
|
32
|
+
"DbCommandExecutor",
|
|
33
|
+
"DbCommandKind",
|
|
34
|
+
"DbConnectionSettings",
|
|
35
|
+
"DbExecutionMetadata",
|
|
36
|
+
"DbExecutionSummary",
|
|
37
|
+
"DbFailure",
|
|
38
|
+
"DbFailureKind",
|
|
39
|
+
"DbGraphqlSupport",
|
|
40
|
+
"DbHealthContributor",
|
|
41
|
+
"DbHealthReport",
|
|
42
|
+
"DbHealthStatus",
|
|
43
|
+
"DbProviderDescription",
|
|
44
|
+
"DbRepository",
|
|
45
|
+
"DbRepositoryContext",
|
|
46
|
+
"DbResult",
|
|
47
|
+
"DbRowSet",
|
|
48
|
+
"DbScalar",
|
|
49
|
+
"DbSessionLease",
|
|
50
|
+
"DbSessionProvider",
|
|
51
|
+
"DbTransactionContext",
|
|
52
|
+
"DbTransactionRunner",
|
|
53
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Callable, Generic, Mapping, Protocol, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
S = TypeVar("S")
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
_MISSING = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DbCommandKind(str, Enum):
|
|
16
|
+
QUERY = "query"
|
|
17
|
+
EXECUTE = "execute"
|
|
18
|
+
BATCH = "batch"
|
|
19
|
+
SCALAR = "scalar"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DbFailureKind(str, Enum):
|
|
23
|
+
CONNECTIVITY = "connectivity"
|
|
24
|
+
TIMEOUT = "timeout"
|
|
25
|
+
AUTHENTICATION = "authentication"
|
|
26
|
+
AUTHORIZATION = "authorization"
|
|
27
|
+
CONSTRAINT = "constraint"
|
|
28
|
+
CONFLICT = "conflict"
|
|
29
|
+
NOT_FOUND = "not_found"
|
|
30
|
+
SERIALIZATION = "serialization"
|
|
31
|
+
CANCELLED = "cancelled"
|
|
32
|
+
UNKNOWN = "unknown"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DbHealthStatus(str, Enum):
|
|
36
|
+
HEALTHY = "healthy"
|
|
37
|
+
UNHEALTHY = "unhealthy"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class DbConnectionSettings:
|
|
42
|
+
host: str
|
|
43
|
+
port: int
|
|
44
|
+
database: str
|
|
45
|
+
username: str
|
|
46
|
+
password: str
|
|
47
|
+
ssl_mode: str
|
|
48
|
+
options: Mapping[str, object] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_environment(
|
|
52
|
+
cls,
|
|
53
|
+
environment: Mapping[str, str] | None = None,
|
|
54
|
+
) -> DbConnectionSettings:
|
|
55
|
+
values = os.environ if environment is None else environment
|
|
56
|
+
raw_port = values.get("MODULAR_API_POSTGRES_PORT", "")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
port = int(raw_port)
|
|
60
|
+
except ValueError:
|
|
61
|
+
port = 5432
|
|
62
|
+
|
|
63
|
+
return cls(
|
|
64
|
+
host=values.get("MODULAR_API_POSTGRES_HOST", "127.0.0.1"),
|
|
65
|
+
port=port,
|
|
66
|
+
database=values.get("MODULAR_API_POSTGRES_DATABASE", "modular_api_graphql_v1"),
|
|
67
|
+
username=values.get("MODULAR_API_POSTGRES_USERNAME", "postgres"),
|
|
68
|
+
password=values.get("MODULAR_API_POSTGRES_PASSWORD", "postgres"),
|
|
69
|
+
ssl_mode=values.get("MODULAR_API_POSTGRES_SSLMODE", "disable"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def engine_id(self) -> str:
|
|
74
|
+
return "postgres"
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def redacted_summary(self) -> str:
|
|
78
|
+
return (
|
|
79
|
+
f"{self.engine_id}://{self.username}@{self.host}:{self.port}/"
|
|
80
|
+
f"{self.database}?sslmode={self.ssl_mode}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class DbCommand:
|
|
86
|
+
kind: DbCommandKind
|
|
87
|
+
text: str
|
|
88
|
+
parameters: tuple[object, ...] = field(default_factory=tuple)
|
|
89
|
+
label: str | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class DbExecutionMetadata:
|
|
94
|
+
duration: int
|
|
95
|
+
command_label: str | None = None
|
|
96
|
+
row_count: int | None = None
|
|
97
|
+
affected_count: int | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class DbRowSet:
|
|
102
|
+
rows: list[Mapping[str, object]]
|
|
103
|
+
metadata: DbExecutionMetadata
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class DbExecutionSummary:
|
|
108
|
+
affected_count: int
|
|
109
|
+
metadata: DbExecutionMetadata
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True, slots=True)
|
|
113
|
+
class DbScalar(Generic[T]):
|
|
114
|
+
value: T
|
|
115
|
+
metadata: DbExecutionMetadata
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class DbFailure:
|
|
120
|
+
kind: DbFailureKind
|
|
121
|
+
code: str
|
|
122
|
+
message: str
|
|
123
|
+
retryable: bool
|
|
124
|
+
transient: bool
|
|
125
|
+
details: object | None = None
|
|
126
|
+
cause_summary: str | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DbResult(Generic[T]):
|
|
130
|
+
def __init__(self, value: object = _MISSING, failure: DbFailure | None = None) -> None:
|
|
131
|
+
self._value = value
|
|
132
|
+
self._failure = failure
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def success(cls, value: T) -> DbResult[T]:
|
|
136
|
+
return cls(value=value)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_failure(cls, failure: DbFailure) -> DbResult[T]:
|
|
140
|
+
return cls(failure=failure)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def is_success(self) -> bool:
|
|
144
|
+
return self._failure is None
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def is_failure(self) -> bool:
|
|
148
|
+
return self._failure is not None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def value(self) -> T:
|
|
152
|
+
if self._failure is not None or self._value is _MISSING:
|
|
153
|
+
raise RuntimeError("DbResult does not contain a success value.")
|
|
154
|
+
return cast(T, self._value)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def failure(self) -> DbFailure:
|
|
158
|
+
if self._failure is None:
|
|
159
|
+
raise RuntimeError("DbResult does not contain a failure value.")
|
|
160
|
+
return self._failure
|
|
161
|
+
|
|
162
|
+
def map(self, transform: Callable[[T], R]) -> DbResult[R]:
|
|
163
|
+
if self.is_failure:
|
|
164
|
+
return DbResult.from_failure(self.failure)
|
|
165
|
+
return DbResult.success(transform(self.value))
|
|
166
|
+
|
|
167
|
+
def flat_map(self, transform: Callable[[T], DbResult[R]]) -> DbResult[R]:
|
|
168
|
+
if self.is_failure:
|
|
169
|
+
return DbResult.from_failure(self.failure)
|
|
170
|
+
return transform(self.value)
|
|
171
|
+
|
|
172
|
+
def map_failure(self, transform: Callable[[DbFailure], DbFailure]) -> DbResult[T]:
|
|
173
|
+
if self.is_success:
|
|
174
|
+
return DbResult.success(self.value)
|
|
175
|
+
return DbResult.from_failure(transform(self.failure))
|
|
176
|
+
|
|
177
|
+
def get_or_throw(self, message: str | None = None) -> T:
|
|
178
|
+
if self.is_failure:
|
|
179
|
+
raise RuntimeError(message or self.failure.message)
|
|
180
|
+
return self.value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True, slots=True)
|
|
184
|
+
class DbProviderDescription:
|
|
185
|
+
engine_id: str
|
|
186
|
+
database: str
|
|
187
|
+
redacted_summary: str
|
|
188
|
+
owns_resources: bool
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class DbSessionLease(Generic[S]):
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
session: S,
|
|
196
|
+
owned_by_package: bool,
|
|
197
|
+
releaser: Callable[[], DbResult[None]],
|
|
198
|
+
) -> None:
|
|
199
|
+
self.session = session
|
|
200
|
+
self.owned_by_package = owned_by_package
|
|
201
|
+
self._releaser = releaser
|
|
202
|
+
|
|
203
|
+
def release(self) -> DbResult[None]:
|
|
204
|
+
if not self.owned_by_package:
|
|
205
|
+
return DbResult.success(None)
|
|
206
|
+
return self._releaser()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DbSessionProvider(Protocol[S]):
|
|
210
|
+
def acquire(self) -> DbResult[DbSessionLease[S]]: ...
|
|
211
|
+
|
|
212
|
+
def close(self) -> DbResult[None]: ...
|
|
213
|
+
|
|
214
|
+
def describe(self) -> DbProviderDescription: ...
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class DbCommandExecutor(Protocol[S]):
|
|
218
|
+
def query(self, session: S, command: DbCommand) -> DbResult[DbRowSet]: ...
|
|
219
|
+
|
|
220
|
+
def execute(self, session: S, command: DbCommand) -> DbResult[DbExecutionSummary]: ...
|
|
221
|
+
|
|
222
|
+
def scalar(self, session: S, command: DbCommand) -> DbResult[DbScalar[object]]: ...
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True)
|
|
226
|
+
class DbTransactionContext(Generic[S]):
|
|
227
|
+
settings: DbConnectionSettings
|
|
228
|
+
session: S
|
|
229
|
+
command_executor: DbCommandExecutor[S]
|
|
230
|
+
|
|
231
|
+
def query(self, command: DbCommand) -> DbResult[DbRowSet]:
|
|
232
|
+
return self.command_executor.query(self.session, command)
|
|
233
|
+
|
|
234
|
+
def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
|
|
235
|
+
return self.command_executor.execute(self.session, command)
|
|
236
|
+
|
|
237
|
+
def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
|
|
238
|
+
return cast(DbResult[DbScalar[T]], self.command_executor.scalar(self.session, command))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class DbTransactionRunner(Protocol[S]):
|
|
242
|
+
def run(
|
|
243
|
+
self,
|
|
244
|
+
context: DbTransactionContext[S],
|
|
245
|
+
body: Callable[[DbTransactionContext[S]], DbResult[T]],
|
|
246
|
+
) -> DbResult[T]: ...
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True, slots=True)
|
|
250
|
+
class DbRepositoryContext(Generic[S]):
|
|
251
|
+
settings: DbConnectionSettings
|
|
252
|
+
session_provider: DbSessionProvider[S]
|
|
253
|
+
command_executor: DbCommandExecutor[S]
|
|
254
|
+
transaction_runner: DbTransactionRunner[S]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class DbRepository(Generic[S]):
|
|
258
|
+
def __init__(self, context: DbRepositoryContext[S]) -> None:
|
|
259
|
+
self.context = context
|
|
260
|
+
|
|
261
|
+
def query(self, command: DbCommand) -> DbResult[DbRowSet]:
|
|
262
|
+
return _with_lease(
|
|
263
|
+
self.context.session_provider,
|
|
264
|
+
lambda lease: self.context.command_executor.query(lease.session, command),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
|
|
268
|
+
return _with_lease(
|
|
269
|
+
self.context.session_provider,
|
|
270
|
+
lambda lease: self.context.command_executor.execute(lease.session, command),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
|
|
274
|
+
return cast(
|
|
275
|
+
DbResult[DbScalar[T]],
|
|
276
|
+
_with_lease(
|
|
277
|
+
self.context.session_provider,
|
|
278
|
+
lambda lease: self.context.command_executor.scalar(lease.session, command),
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def transaction(self, body: Callable[[DbTransactionContext[S]], DbResult[T]]) -> DbResult[T]:
|
|
283
|
+
client = DbClient(
|
|
284
|
+
settings=self.context.settings,
|
|
285
|
+
session_provider=self.context.session_provider,
|
|
286
|
+
command_executor=self.context.command_executor,
|
|
287
|
+
transaction_runner=self.context.transaction_runner,
|
|
288
|
+
)
|
|
289
|
+
return client.transaction(body)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class DbClient(Generic[S]):
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
settings: DbConnectionSettings,
|
|
297
|
+
session_provider: DbSessionProvider[S],
|
|
298
|
+
command_executor: DbCommandExecutor[S],
|
|
299
|
+
transaction_runner: DbTransactionRunner[S],
|
|
300
|
+
) -> None:
|
|
301
|
+
self.settings = settings
|
|
302
|
+
self.session_provider = session_provider
|
|
303
|
+
self.command_executor = command_executor
|
|
304
|
+
self.transaction_runner = transaction_runner
|
|
305
|
+
|
|
306
|
+
def query(self, command: DbCommand) -> DbResult[DbRowSet]:
|
|
307
|
+
return _with_lease(
|
|
308
|
+
self.session_provider,
|
|
309
|
+
lambda lease: self.command_executor.query(lease.session, command),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
|
|
313
|
+
return _with_lease(
|
|
314
|
+
self.session_provider,
|
|
315
|
+
lambda lease: self.command_executor.execute(lease.session, command),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
|
|
319
|
+
return cast(
|
|
320
|
+
DbResult[DbScalar[T]],
|
|
321
|
+
_with_lease(
|
|
322
|
+
self.session_provider,
|
|
323
|
+
lambda lease: self.command_executor.scalar(lease.session, command),
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def transaction(self, body: Callable[[DbTransactionContext[S]], DbResult[T]]) -> DbResult[T]:
|
|
328
|
+
lease_result = self.session_provider.acquire()
|
|
329
|
+
if lease_result.is_failure:
|
|
330
|
+
return DbResult.from_failure(lease_result.failure)
|
|
331
|
+
|
|
332
|
+
lease = lease_result.value
|
|
333
|
+
context = DbTransactionContext(
|
|
334
|
+
settings=self.settings,
|
|
335
|
+
session=lease.session,
|
|
336
|
+
command_executor=self.command_executor,
|
|
337
|
+
)
|
|
338
|
+
result = self.transaction_runner.run(context, body)
|
|
339
|
+
release_result = lease.release()
|
|
340
|
+
|
|
341
|
+
if result.is_failure:
|
|
342
|
+
return DbResult.from_failure(result.failure)
|
|
343
|
+
if release_result.is_failure:
|
|
344
|
+
return DbResult.from_failure(release_result.failure)
|
|
345
|
+
return result
|
|
346
|
+
|
|
347
|
+
def repository_context(self) -> DbRepositoryContext[S]:
|
|
348
|
+
return DbRepositoryContext(
|
|
349
|
+
settings=self.settings,
|
|
350
|
+
session_provider=self.session_provider,
|
|
351
|
+
command_executor=self.command_executor,
|
|
352
|
+
transaction_runner=self.transaction_runner,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def close(self) -> DbResult[None]:
|
|
356
|
+
return self.session_provider.close()
|
|
357
|
+
|
|
358
|
+
def describe(self) -> DbProviderDescription:
|
|
359
|
+
return self.session_provider.describe()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass(frozen=True, slots=True)
|
|
363
|
+
class DbHealthReport:
|
|
364
|
+
status: DbHealthStatus
|
|
365
|
+
response_time: int
|
|
366
|
+
redacted_summary: str
|
|
367
|
+
details: str | None = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class DbHealthContributor(Generic[S]):
|
|
371
|
+
def __init__(self, *, client: DbClient[S], probe_command: DbCommand | None = None) -> None:
|
|
372
|
+
self.client = client
|
|
373
|
+
self.probe_command = probe_command or DbCommand(
|
|
374
|
+
kind=DbCommandKind.SCALAR,
|
|
375
|
+
text="SELECT 1",
|
|
376
|
+
label="db.health",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def probe(self) -> DbHealthReport:
|
|
380
|
+
started_at = time.monotonic()
|
|
381
|
+
result = self.client.scalar(self.probe_command)
|
|
382
|
+
response_time = int((time.monotonic() - started_at) * 1000)
|
|
383
|
+
|
|
384
|
+
if result.is_success:
|
|
385
|
+
return DbHealthReport(
|
|
386
|
+
status=DbHealthStatus.HEALTHY,
|
|
387
|
+
response_time=response_time,
|
|
388
|
+
redacted_summary=self.client.describe().redacted_summary,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return DbHealthReport(
|
|
392
|
+
status=DbHealthStatus.UNHEALTHY,
|
|
393
|
+
response_time=response_time,
|
|
394
|
+
redacted_summary=self.client.describe().redacted_summary,
|
|
395
|
+
details=result.failure.code,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@dataclass(frozen=True, slots=True)
|
|
400
|
+
class DbGraphqlSupport(Generic[S]):
|
|
401
|
+
catalog_provider: object
|
|
402
|
+
read_executor: object
|
|
403
|
+
health_contributor: DbHealthContributor[S]
|
|
404
|
+
source_digest_factory: object | None = None
|
|
405
|
+
artifact_loader: object | None = None
|
|
406
|
+
capability_registration: object | None = None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _with_lease(
|
|
410
|
+
session_provider: DbSessionProvider[S],
|
|
411
|
+
operation: Callable[[DbSessionLease[S]], DbResult[T]],
|
|
412
|
+
) -> DbResult[T]:
|
|
413
|
+
lease_result = session_provider.acquire()
|
|
414
|
+
if lease_result.is_failure:
|
|
415
|
+
return DbResult.from_failure(lease_result.failure)
|
|
416
|
+
|
|
417
|
+
lease = lease_result.value
|
|
418
|
+
operation_result = operation(lease)
|
|
419
|
+
release_result = lease.release()
|
|
420
|
+
|
|
421
|
+
if operation_result.is_failure:
|
|
422
|
+
return DbResult.from_failure(operation_result.failure)
|
|
423
|
+
if release_result.is_failure:
|
|
424
|
+
return DbResult.from_failure(release_result.failure)
|
|
425
|
+
return operation_result
|