macss-modular-api-sqlserver 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.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: macss-modular-api-sqlserver
3
+ Version: 0.4.7
4
+ Summary: Official MACSS SQL Server 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_sqlserver
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_sqlserver#readme
11
+ Keywords: macss,sqlserver,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-sqlserver
26
+
27
+ Official MACSS SQL Server integration package for Python.
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ from modular_api_sqlserver 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 SQL Server 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_sqlserver/__init__.py,sha256=oc7QvcB8Ed6B8jVwzetvRrZBA3hROJs1WrC37bHv5fE,1090
2
+ modular_api_sqlserver/db_client.py,sha256=jVTiw9urV2AGqpg7sQzW-ID3PfUdWxSrMxG-I83pZ_c,13045
3
+ macss_modular_api_sqlserver-0.4.7.dist-info/METADATA,sha256=DWSUnJrI8AFMOPf2969GRZYNuI5_mHE_zYF2nKu6q_M,2208
4
+ macss_modular_api_sqlserver-0.4.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ macss_modular_api_sqlserver-0.4.7.dist-info/top_level.txt,sha256=n0TFMe-mbowNietmYzSthEnQ-gmZLPXaLJ6h0HhuX84,22
6
+ macss_modular_api_sqlserver-0.4.7.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ modular_api_sqlserver
@@ -0,0 +1,53 @@
1
+ """Public package exports for modular_api_sqlserver."""
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,428 @@
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 Any, 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
+ driver: 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_SQLSERVER_PORT", "")
57
+
58
+ try:
59
+ port = int(raw_port)
60
+ except ValueError:
61
+ port = 14333
62
+
63
+ return cls(
64
+ host=values.get("MODULAR_API_SQLSERVER_HOST", "127.0.0.1"),
65
+ port=port,
66
+ database=values.get("MODULAR_API_SQLSERVER_DATABASE", "modular_api_graphql_v1"),
67
+ username=values.get("MODULAR_API_SQLSERVER_USERNAME", "sa"),
68
+ password=values.get("MODULAR_API_SQLSERVER_PASSWORD", "ModularApi_dev_StrongPass1"),
69
+ driver=values.get(
70
+ "MODULAR_API_SQLSERVER_DRIVER",
71
+ "ODBC Driver 17 for SQL Server",
72
+ ),
73
+ )
74
+
75
+ @property
76
+ def engine_id(self) -> str:
77
+ return "sqlserver"
78
+
79
+ @property
80
+ def redacted_summary(self) -> str:
81
+ return (
82
+ f"{self.engine_id}://{self.username}@{self.host}:{self.port}/"
83
+ f"{self.database}?driver={self.driver}"
84
+ )
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class DbCommand:
89
+ kind: DbCommandKind
90
+ text: str
91
+ parameters: tuple[object, ...] = field(default_factory=tuple)
92
+ label: str | None = None
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class DbExecutionMetadata:
97
+ duration: int
98
+ command_label: str | None = None
99
+ row_count: int | None = None
100
+ affected_count: int | None = None
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class DbRowSet:
105
+ rows: list[Mapping[str, object]]
106
+ metadata: DbExecutionMetadata
107
+
108
+
109
+ @dataclass(frozen=True, slots=True)
110
+ class DbExecutionSummary:
111
+ affected_count: int
112
+ metadata: DbExecutionMetadata
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class DbScalar(Generic[T]):
117
+ value: T
118
+ metadata: DbExecutionMetadata
119
+
120
+
121
+ @dataclass(frozen=True, slots=True)
122
+ class DbFailure:
123
+ kind: DbFailureKind
124
+ code: str
125
+ message: str
126
+ retryable: bool
127
+ transient: bool
128
+ details: object | None = None
129
+ cause_summary: str | None = None
130
+
131
+
132
+ class DbResult(Generic[T]):
133
+ def __init__(self, value: object = _MISSING, failure: DbFailure | None = None) -> None:
134
+ self._value = value
135
+ self._failure = failure
136
+
137
+ @classmethod
138
+ def success(cls, value: T) -> DbResult[T]:
139
+ return cls(value=value)
140
+
141
+ @classmethod
142
+ def from_failure(cls, failure: DbFailure) -> DbResult[T]:
143
+ return cls(failure=failure)
144
+
145
+ @property
146
+ def is_success(self) -> bool:
147
+ return self._failure is None
148
+
149
+ @property
150
+ def is_failure(self) -> bool:
151
+ return self._failure is not None
152
+
153
+ @property
154
+ def value(self) -> T:
155
+ if self._failure is not None or self._value is _MISSING:
156
+ raise RuntimeError("DbResult does not contain a success value.")
157
+ return cast(T, self._value)
158
+
159
+ @property
160
+ def failure(self) -> DbFailure:
161
+ if self._failure is None:
162
+ raise RuntimeError("DbResult does not contain a failure value.")
163
+ return self._failure
164
+
165
+ def map(self, transform: Callable[[T], R]) -> DbResult[R]:
166
+ if self.is_failure:
167
+ return DbResult.from_failure(self.failure)
168
+ return DbResult.success(transform(self.value))
169
+
170
+ def flat_map(self, transform: Callable[[T], DbResult[R]]) -> DbResult[R]:
171
+ if self.is_failure:
172
+ return DbResult.from_failure(self.failure)
173
+ return transform(self.value)
174
+
175
+ def map_failure(self, transform: Callable[[DbFailure], DbFailure]) -> DbResult[T]:
176
+ if self.is_success:
177
+ return DbResult.success(self.value)
178
+ return DbResult.from_failure(transform(self.failure))
179
+
180
+ def get_or_throw(self, message: str | None = None) -> T:
181
+ if self.is_failure:
182
+ raise RuntimeError(message or self.failure.message)
183
+ return self.value
184
+
185
+
186
+ @dataclass(frozen=True, slots=True)
187
+ class DbProviderDescription:
188
+ engine_id: str
189
+ database: str
190
+ redacted_summary: str
191
+ owns_resources: bool
192
+
193
+
194
+ class DbSessionLease(Generic[S]):
195
+ def __init__(
196
+ self,
197
+ *,
198
+ session: S,
199
+ owned_by_package: bool,
200
+ releaser: Callable[[], DbResult[None]],
201
+ ) -> None:
202
+ self.session = session
203
+ self.owned_by_package = owned_by_package
204
+ self._releaser = releaser
205
+
206
+ def release(self) -> DbResult[None]:
207
+ if not self.owned_by_package:
208
+ return DbResult.success(None)
209
+ return self._releaser()
210
+
211
+
212
+ class DbSessionProvider(Protocol[S]):
213
+ def acquire(self) -> DbResult[DbSessionLease[S]]: ...
214
+
215
+ def close(self) -> DbResult[None]: ...
216
+
217
+ def describe(self) -> DbProviderDescription: ...
218
+
219
+
220
+ class DbCommandExecutor(Protocol[S]):
221
+ def query(self, session: S, command: DbCommand) -> DbResult[DbRowSet]: ...
222
+
223
+ def execute(self, session: S, command: DbCommand) -> DbResult[DbExecutionSummary]: ...
224
+
225
+ def scalar(self, session: S, command: DbCommand) -> DbResult[DbScalar[object]]: ...
226
+
227
+
228
+ @dataclass(frozen=True, slots=True)
229
+ class DbTransactionContext(Generic[S]):
230
+ settings: DbConnectionSettings
231
+ session: S
232
+ command_executor: DbCommandExecutor[S]
233
+
234
+ def query(self, command: DbCommand) -> DbResult[DbRowSet]:
235
+ return self.command_executor.query(self.session, command)
236
+
237
+ def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
238
+ return self.command_executor.execute(self.session, command)
239
+
240
+ def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
241
+ return cast(DbResult[DbScalar[T]], self.command_executor.scalar(self.session, command))
242
+
243
+
244
+ class DbTransactionRunner(Protocol[S]):
245
+ def run(
246
+ self,
247
+ context: DbTransactionContext[S],
248
+ body: Callable[[DbTransactionContext[S]], DbResult[T]],
249
+ ) -> DbResult[T]: ...
250
+
251
+
252
+ @dataclass(frozen=True, slots=True)
253
+ class DbRepositoryContext(Generic[S]):
254
+ settings: DbConnectionSettings
255
+ session_provider: DbSessionProvider[S]
256
+ command_executor: DbCommandExecutor[S]
257
+ transaction_runner: DbTransactionRunner[S]
258
+
259
+
260
+ class DbRepository(Generic[S]):
261
+ def __init__(self, context: DbRepositoryContext[S]) -> None:
262
+ self.context = context
263
+
264
+ def query(self, command: DbCommand) -> DbResult[DbRowSet]:
265
+ return _with_lease(
266
+ self.context.session_provider,
267
+ lambda lease: self.context.command_executor.query(lease.session, command),
268
+ )
269
+
270
+ def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
271
+ return _with_lease(
272
+ self.context.session_provider,
273
+ lambda lease: self.context.command_executor.execute(lease.session, command),
274
+ )
275
+
276
+ def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
277
+ return cast(
278
+ DbResult[DbScalar[T]],
279
+ _with_lease(
280
+ self.context.session_provider,
281
+ lambda lease: self.context.command_executor.scalar(lease.session, command),
282
+ ),
283
+ )
284
+
285
+ def transaction(self, body: Callable[[DbTransactionContext[S]], DbResult[T]]) -> DbResult[T]:
286
+ client = DbClient(
287
+ settings=self.context.settings,
288
+ session_provider=self.context.session_provider,
289
+ command_executor=self.context.command_executor,
290
+ transaction_runner=self.context.transaction_runner,
291
+ )
292
+ return client.transaction(body)
293
+
294
+
295
+ class DbClient(Generic[S]):
296
+ def __init__(
297
+ self,
298
+ *,
299
+ settings: DbConnectionSettings,
300
+ session_provider: DbSessionProvider[S],
301
+ command_executor: DbCommandExecutor[S],
302
+ transaction_runner: DbTransactionRunner[S],
303
+ ) -> None:
304
+ self.settings = settings
305
+ self.session_provider = session_provider
306
+ self.command_executor = command_executor
307
+ self.transaction_runner = transaction_runner
308
+
309
+ def query(self, command: DbCommand) -> DbResult[DbRowSet]:
310
+ return _with_lease(
311
+ self.session_provider,
312
+ lambda lease: self.command_executor.query(lease.session, command),
313
+ )
314
+
315
+ def execute(self, command: DbCommand) -> DbResult[DbExecutionSummary]:
316
+ return _with_lease(
317
+ self.session_provider,
318
+ lambda lease: self.command_executor.execute(lease.session, command),
319
+ )
320
+
321
+ def scalar(self, command: DbCommand) -> DbResult[DbScalar[T]]:
322
+ return cast(
323
+ DbResult[DbScalar[T]],
324
+ _with_lease(
325
+ self.session_provider,
326
+ lambda lease: self.command_executor.scalar(lease.session, command),
327
+ ),
328
+ )
329
+
330
+ def transaction(self, body: Callable[[DbTransactionContext[S]], DbResult[T]]) -> DbResult[T]:
331
+ lease_result = self.session_provider.acquire()
332
+ if lease_result.is_failure:
333
+ return DbResult.from_failure(lease_result.failure)
334
+
335
+ lease = lease_result.value
336
+ context = DbTransactionContext(
337
+ settings=self.settings,
338
+ session=lease.session,
339
+ command_executor=self.command_executor,
340
+ )
341
+ result = self.transaction_runner.run(context, body)
342
+ release_result = lease.release()
343
+
344
+ if result.is_failure:
345
+ return DbResult.from_failure(result.failure)
346
+ if release_result.is_failure:
347
+ return DbResult.from_failure(release_result.failure)
348
+ return result
349
+
350
+ def repository_context(self) -> DbRepositoryContext[S]:
351
+ return DbRepositoryContext(
352
+ settings=self.settings,
353
+ session_provider=self.session_provider,
354
+ command_executor=self.command_executor,
355
+ transaction_runner=self.transaction_runner,
356
+ )
357
+
358
+ def close(self) -> DbResult[None]:
359
+ return self.session_provider.close()
360
+
361
+ def describe(self) -> DbProviderDescription:
362
+ return self.session_provider.describe()
363
+
364
+
365
+ @dataclass(frozen=True, slots=True)
366
+ class DbHealthReport:
367
+ status: DbHealthStatus
368
+ response_time: int
369
+ redacted_summary: str
370
+ details: str | None = None
371
+
372
+
373
+ class DbHealthContributor(Generic[S]):
374
+ def __init__(self, *, client: DbClient[S], probe_command: DbCommand | None = None) -> None:
375
+ self.client = client
376
+ self.probe_command = probe_command or DbCommand(
377
+ kind=DbCommandKind.SCALAR,
378
+ text="SELECT 1",
379
+ label="db.health",
380
+ )
381
+
382
+ def probe(self) -> DbHealthReport:
383
+ started_at = time.monotonic()
384
+ result = self.client.scalar(self.probe_command)
385
+ response_time = int((time.monotonic() - started_at) * 1000)
386
+
387
+ if result.is_success:
388
+ return DbHealthReport(
389
+ status=DbHealthStatus.HEALTHY,
390
+ response_time=response_time,
391
+ redacted_summary=self.client.describe().redacted_summary,
392
+ )
393
+
394
+ return DbHealthReport(
395
+ status=DbHealthStatus.UNHEALTHY,
396
+ response_time=response_time,
397
+ redacted_summary=self.client.describe().redacted_summary,
398
+ details=result.failure.code,
399
+ )
400
+
401
+
402
+ @dataclass(frozen=True, slots=True)
403
+ class DbGraphqlSupport(Generic[S]):
404
+ catalog_provider: object
405
+ read_executor: object
406
+ health_contributor: DbHealthContributor[S]
407
+ source_digest_factory: object | None = None
408
+ artifact_loader: object | None = None
409
+ capability_registration: object | None = None
410
+
411
+
412
+ def _with_lease(
413
+ session_provider: DbSessionProvider[S],
414
+ operation: Callable[[DbSessionLease[S]], DbResult[T]],
415
+ ) -> DbResult[T]:
416
+ lease_result = session_provider.acquire()
417
+ if lease_result.is_failure:
418
+ return DbResult.from_failure(lease_result.failure)
419
+
420
+ lease = lease_result.value
421
+ operation_result = operation(lease)
422
+ release_result = lease.release()
423
+
424
+ if operation_result.is_failure:
425
+ return DbResult.from_failure(operation_result.failure)
426
+ if release_result.is_failure:
427
+ return DbResult.from_failure(release_result.failure)
428
+ return operation_result