encore-dev 2.0.0a7__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.
Files changed (44) hide show
  1. encore/__init__.py +43 -0
  2. encore/cron/__init__.py +50 -0
  3. encore/internal/__init__.py +1 -0
  4. encore/internal/_loader.py +79 -0
  5. encore/internal/_native.pyi +734 -0
  6. encore/internal/lazy.py +35 -0
  7. encore/internal/runtime.py +133 -0
  8. encore/internal/startup.py +24 -0
  9. encore/logs/__init__.py +19 -0
  10. encore/logs/_console.py +155 -0
  11. encore/logs/_runtime.py +56 -0
  12. encore/meta/__init__.py +228 -0
  13. encore/metrics/__init__.py +390 -0
  14. encore/pubsub/__init__.py +425 -0
  15. encore/py.typed +0 -0
  16. encore/secrets/__init__.py +139 -0
  17. encore/shutdown/__init__.py +164 -0
  18. encore/storage/__init__.py +5 -0
  19. encore/storage/objects/__init__.py +396 -0
  20. encore/storage/postgres/__init__.py +737 -0
  21. encore/storage/redis/__init__.py +200 -0
  22. encore/storage/redis/_base.py +42 -0
  23. encore/storage/redis/_responses.py +150 -0
  24. encore/storage/redis/_values.py +189 -0
  25. encore/storage/redis/bitmap.py +142 -0
  26. encore/storage/redis/generic.py +367 -0
  27. encore/storage/redis/geo.py +508 -0
  28. encore/storage/redis/hash.py +504 -0
  29. encore/storage/redis/json.py +614 -0
  30. encore/storage/redis/list.py +511 -0
  31. encore/storage/redis/pubsub.py +276 -0
  32. encore/storage/redis/set.py +570 -0
  33. encore/storage/redis/sorted_set.py +753 -0
  34. encore/storage/redis/stream.py +773 -0
  35. encore/storage/redis/string.py +654 -0
  36. encore/tracing/__init__.py +19 -0
  37. encore/tracing/_context.py +27 -0
  38. encore/tracing/_encode.py +351 -0
  39. encore/tracing/_provider.py +62 -0
  40. encore/tracing/_span.py +226 -0
  41. encore/tracing/_tracer.py +93 -0
  42. encore_dev-2.0.0a7.dist-info/METADATA +791 -0
  43. encore_dev-2.0.0a7.dist-info/RECORD +44 -0
  44. encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
@@ -0,0 +1,737 @@
1
+ """Postgres declarations, explicit access handles, and streaming queries.
2
+
3
+ Declare databases at module scope and use their query methods to execute SQL.
4
+ Use begin() for transactions and acquire() for dedicated pooled connections.
5
+
6
+ Typical usage example:
7
+
8
+ from encore.storage import postgres
9
+
10
+ db = postgres.Database("orders", migrations="./migrations")
11
+
12
+ async def get_user(user_id: int):
13
+ return await db.query("SELECT * FROM users WHERE id = $1", user_id).one()
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from collections.abc import AsyncGenerator, Callable, Generator
20
+ from dataclasses import dataclass
21
+ from types import TracebackType
22
+ from typing import Any, Literal
23
+
24
+ from ...internal.lazy import Lazy
25
+ from ...internal.runtime import EncoreError, _native, runtime
26
+ from ...tracing._context import active_ctx_bytes
27
+
28
+ __all__ = [
29
+ "Database",
30
+ "DatabaseRef",
31
+ "Queryable",
32
+ "Cursor",
33
+ "Connection",
34
+ "Transaction",
35
+ "ExecResult",
36
+ "Row",
37
+ ]
38
+
39
+ logger = logging.getLogger(__name__)
40
+ Row = dict[str, Any]
41
+ _Access = Literal["reader", "writer", "admin"]
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ExecResult:
46
+ """An immutable result of a completed SQL statement.
47
+
48
+ Attributes:
49
+ rows_affected: The number of rows affected, as reported by Postgres.
50
+ """
51
+
52
+ rows_affected: int
53
+
54
+
55
+ class Cursor:
56
+ """A SQL cursor consumed once by iteration, one(), or all().
57
+
58
+ Execution starts when iteration advances or a helper is awaited. Starting
59
+ one consumption mode prevents any other consumption of this query, including
60
+ after an error or partial iteration. Obtain cursors from query() on a
61
+ database, reference, connection, or transaction.
62
+
63
+ Exhaust a stream or call aclose() to release its connection. For early exits,
64
+ use the query as an async context manager. Entering the context does not
65
+ execute or consume the query. Leaving it closes the stream. The one() and
66
+ all() helpers close their cursors automatically.
67
+ """
68
+
69
+ def __init__(self, rows: AsyncGenerator[Row, None]) -> None:
70
+ """Initialize a cursor with a deferred row stream.
71
+
72
+ Args:
73
+ rows: The row stream that executes the query when advanced.
74
+ """
75
+ self._rows = rows
76
+ self._consumed = False
77
+
78
+ def _consume(self) -> AsyncGenerator[Row, None]:
79
+ if self._consumed:
80
+ raise EncoreError("query has already been consumed")
81
+ self._consumed = True
82
+ return self._rows
83
+
84
+ def __aiter__(self) -> AsyncGenerator[Row, None]:
85
+ """Return the row iterator and reserve this query for streaming.
86
+
87
+ Returns:
88
+ An async iterator yielding dictionaries of column names to values.
89
+ """
90
+ return self._consume()
91
+
92
+ async def one(self) -> Row | None:
93
+ """Return zero or one row and close the cursor.
94
+
95
+ Returns:
96
+ A dictionary of column names to values, or None if no rows match.
97
+
98
+ Raises:
99
+ EncoreError: The query fails or returns more than one row.
100
+ """
101
+ rows = self._consume()
102
+ try:
103
+ first = None
104
+ async for row in rows:
105
+ if first is not None:
106
+ raise EncoreError("query returned more than one row")
107
+ first = row
108
+ return first
109
+ finally:
110
+ await rows.aclose()
111
+
112
+ async def all(self) -> list[Row]:
113
+ """Collect all rows in memory and close the cursor.
114
+
115
+ Returns:
116
+ The result rows as dictionaries of column names to values.
117
+ An empty list if no rows match.
118
+
119
+ Raises:
120
+ EncoreError: The query fails.
121
+ """
122
+ rows = self._consume()
123
+ try:
124
+ return [row async for row in rows]
125
+ finally:
126
+ await rows.aclose()
127
+
128
+ async def aclose(self) -> None:
129
+ """Close the row stream and prevent further consumption of this query.
130
+
131
+ Closing an unstarted query does not execute SQL. Repeated closes have
132
+ no effect. Wait for any active read or helper to finish before closing.
133
+ """
134
+ self._consumed = True
135
+ await self._rows.aclose()
136
+
137
+ async def __aenter__(self) -> Cursor:
138
+ """Return this cursor without executing or consuming it."""
139
+ return self
140
+
141
+ async def __aexit__(
142
+ self,
143
+ exc_type: type[BaseException] | None,
144
+ exc: BaseException | None,
145
+ tb: TracebackType | None,
146
+ ) -> None:
147
+ """Close the query without suppressing an exception from the block.
148
+
149
+ Args:
150
+ exc_type: The exception type, or None on a clean exit.
151
+ exc: The exception instance, or None on a clean exit.
152
+ tb: The traceback, or None on a clean exit.
153
+ """
154
+ await self.aclose()
155
+
156
+
157
+ class _Queries:
158
+ """A query executor shared by the public Postgres wrappers.
159
+
160
+ Attributes:
161
+ get_handle: A callable that resolves the native execution handle on
162
+ demand.
163
+ """
164
+
165
+ def __init__(self, get_handle: Callable[[], _native.PostgresHandle]) -> None:
166
+ """Initialize the executor without resolving its native handle.
167
+
168
+ Args:
169
+ get_handle: A callable that returns the native execution handle.
170
+ """
171
+ self.get_handle = get_handle
172
+
173
+ async def query(self, sql: str, *params: Any) -> AsyncGenerator[Row, None]:
174
+ """Yield rows and close the native cursor when iteration ends.
175
+
176
+ Args:
177
+ sql: A SQL statement with positional placeholders such as $1 and $2.
178
+ *params: Values bound to the placeholders in order.
179
+
180
+ Yields:
181
+ Dictionaries mapping column names to decoded Python values.
182
+
183
+ Raises:
184
+ EncoreError: Postgres cannot execute the query or retrieve a row.
185
+ """
186
+ cursor = await self.get_handle().query(
187
+ sql, list(params), source=active_ctx_bytes()
188
+ )
189
+ try:
190
+ while (row := await cursor.next()) is not None:
191
+ yield row
192
+ finally:
193
+ await cursor.close()
194
+
195
+ async def exec(self, sql: str, *params: Any) -> ExecResult:
196
+ """Execute a statement and wait for its affected-row count.
197
+
198
+ Args:
199
+ sql: A SQL statement with positional placeholders such as $1 and $2.
200
+ *params: Values bound to the placeholders in order.
201
+
202
+ Returns:
203
+ An ExecResult containing the affected-row count reported by
204
+ Postgres. Any rows returned by the statement are discarded.
205
+
206
+ Raises:
207
+ EncoreError: Postgres cannot complete the statement.
208
+ """
209
+ count = await self.get_handle().exec(
210
+ sql, list(params), source=active_ctx_bytes()
211
+ )
212
+ return ExecResult(rows_affected=count)
213
+
214
+
215
+ class Queryable:
216
+ """A query interface for database references, connections, and transactions.
217
+
218
+ SQL parameters use $1, $2, and subsequent positional placeholders.
219
+ Connections and transactions must be open and have no other active operation
220
+ or row stream when starting a query.
221
+ """
222
+
223
+ def __init__(self, queries: _Queries) -> None:
224
+ """Initialize the interface with its query executor.
225
+
226
+ Args:
227
+ queries: The executor used by this handle's query methods.
228
+ """
229
+ self._queries = queries
230
+
231
+ def query(self, sql: str, *params: Any) -> Cursor:
232
+ """Create a query for streaming or fetching rows.
233
+
234
+ Consume the query once with async iteration, one(), or all(). No SQL runs
235
+ until iteration advances or a helper is awaited. Exhaust the stream or
236
+ close it before reusing its connection or transaction. Use
237
+ the query as an async context manager when leaving iteration early.
238
+
239
+ Args:
240
+ sql: A SQL statement with positional placeholders such as $1 and $2.
241
+ *params: Values bound to the placeholders in order.
242
+
243
+ Returns:
244
+ A Cursor whose rows map column names to decoded Python values.
245
+
246
+ Raises:
247
+ EncoreError: During iteration, acquiring a connection, executing the
248
+ query, or retrieving a row fails.
249
+ """
250
+ return Cursor(self._queries.query(sql, *params))
251
+
252
+ async def exec(self, sql: str, *params: Any) -> ExecResult:
253
+ """Execute a statement and discard any returned rows.
254
+
255
+ Args:
256
+ sql: A SQL statement with positional placeholders such as $1 and $2.
257
+ *params: Values bound to the placeholders in order.
258
+
259
+ Returns:
260
+ An ExecResult containing the affected-row count reported by
261
+ Postgres.
262
+
263
+ Raises:
264
+ EncoreError: Acquiring a connection or executing the statement fails.
265
+ """
266
+ return await self._queries.exec(sql, *params)
267
+
268
+
269
+ class DatabaseRef(Queryable):
270
+ """A database handle that can be passed to other functions.
271
+
272
+ Obtain a reference from Database.read(), Database.write(), or
273
+ Database.admin() so Encore can infer the service's required permissions.
274
+ References share the database's pool and connection privileges.
275
+ """
276
+
277
+ def __init__(
278
+ self,
279
+ get_handle: Callable[[], _native.PostgresHandle],
280
+ *,
281
+ access: _Access,
282
+ primary: bool = True,
283
+ ) -> None:
284
+ """Initialize a reference without resolving its native handle.
285
+
286
+ Application code should use Database.read(), Database.write(), or
287
+ Database.admin() so Encore can discover the required permissions.
288
+
289
+ Args:
290
+ get_handle: A callable that returns the database's native handle.
291
+ access: The selected access level: "reader", "writer", or "admin".
292
+ primary: The stored primary-server preference. Defaults to True.
293
+ Does not affect connection selection.
294
+ """
295
+ super().__init__(_Queries(get_handle))
296
+ self._access = access
297
+ self._primary = primary
298
+
299
+ @property
300
+ def connection_string(self) -> str:
301
+ """The proxy connection string for connecting with another client.
302
+
303
+ All references return the same URL, regardless of their
304
+ selected access level or primary-server preference.
305
+ """
306
+ return self._queries.get_handle().conn_string()
307
+
308
+ def begin(self) -> _BeginTransaction:
309
+ """Create an awaitable that begins a pooled transaction.
310
+
311
+ Returns:
312
+ An awaitable that produces a Transaction, also usable as an async
313
+ context manager. No transaction starts until the result is awaited
314
+ or entered. A clean context exit commits. An exceptional exit rolls
315
+ back. When awaiting directly, commit or roll back the transaction
316
+ explicitly.
317
+
318
+ Raises:
319
+ EncoreError: When awaited or entered, the runtime cannot acquire a
320
+ connection or begin the transaction.
321
+ """
322
+ return _BeginTransaction(self._queries.get_handle())
323
+
324
+ def acquire(self) -> _AcquireConnection:
325
+ """Create an awaitable that acquires a pooled connection.
326
+
327
+ Returns:
328
+ An awaitable that produces a Connection, also usable as an async
329
+ context manager. No connection is acquired until the result is
330
+ awaited or entered. Leaving the context closes the connection. When
331
+ awaiting directly, close the connection explicitly after use.
332
+
333
+ Raises:
334
+ EncoreError: When awaited or entered, the runtime cannot acquire a
335
+ connection.
336
+ """
337
+ return _AcquireConnection(self._queries.get_handle())
338
+
339
+
340
+ class Database:
341
+ """A module-level Postgres database declaration.
342
+
343
+ Encore infers read-write permissions from direct database operations.
344
+ Use read(), write(), or admin() to obtain a reference that can be passed to
345
+ other functions.
346
+ """
347
+
348
+ def __init__(self, name: str, /, *, migrations: str | None = None) -> None:
349
+ """Declare a new Postgres database.
350
+
351
+ Encore discovers this declaration at module scope and provisions the
352
+ database. Constructing it does not acquire a connection.
353
+
354
+ Args:
355
+ name: The logical database name, supplied as a positional string
356
+ literal so Encore can resolve it statically.
357
+ migrations: A slash-separated path to a subdirectory relative to the
358
+ declaring file, such as "./migrations". Must be a string literal
359
+ or None. Defaults to None, which declares no Encore migration
360
+ directory.
361
+ """
362
+ self._name = name
363
+ self._migrations = migrations
364
+ self._handle = Lazy(lambda: runtime().sql_database(name))
365
+ self._writer = DatabaseRef(self._handle.get, access="writer")
366
+
367
+ @classmethod
368
+ def named(cls, name: str, /) -> Database:
369
+ """Reference an existing database without declaring it again.
370
+
371
+ Use this at module scope. The database is configured where it is
372
+ declared.
373
+
374
+ Args:
375
+ name: The existing database's logical name, supplied as a positional
376
+ string literal so Encore can resolve it statically.
377
+
378
+ Returns:
379
+ A Database that refers to the existing resource.
380
+ """
381
+ return cls(name)
382
+
383
+ @property
384
+ def name(self) -> str:
385
+ """The logical name of the database."""
386
+ return self._name
387
+
388
+ def admin(self) -> DatabaseRef:
389
+ """Create a handle that records admin access for this service.
390
+
391
+ Returns:
392
+ A reference sharing this database's pool and connection privileges.
393
+ """
394
+ return DatabaseRef(self._handle.get, access="admin")
395
+
396
+ def write(self) -> DatabaseRef:
397
+ """Return a handle that records read-write access for this service.
398
+
399
+ Returns:
400
+ A reference sharing this database's pool and connection privileges.
401
+ """
402
+ return self._writer
403
+
404
+ def read(self, primary: bool = False) -> DatabaseRef:
405
+ """Create a handle that records reader access for this service.
406
+
407
+ Args:
408
+ primary: The stored primary-server preference. Defaults to False.
409
+ Does not affect connection selection.
410
+
411
+ Returns:
412
+ A reference sharing this database's pool and connection privileges.
413
+ """
414
+ return DatabaseRef(self._handle.get, access="reader", primary=primary)
415
+
416
+ def query(self, sql: str, *params: Any) -> Cursor:
417
+ """Create a query for streaming or fetching rows.
418
+
419
+ Consume the query once with async iteration, one(), or all(). No SQL runs
420
+ until iteration advances or a helper is awaited. See Queryable.query
421
+ for stream cleanup requirements.
422
+
423
+ Args:
424
+ sql: A SQL statement with positional placeholders such as $1 and $2.
425
+ *params: Values bound to the placeholders in order.
426
+
427
+ Returns:
428
+ A Cursor whose rows map column names to decoded Python values.
429
+
430
+ Raises:
431
+ EncoreError: During iteration, the runtime cannot acquire a
432
+ connection or Postgres cannot execute the query or retrieve a
433
+ row.
434
+ """
435
+ return self._writer.query(sql, *params)
436
+
437
+ async def exec(self, sql: str, *params: Any) -> ExecResult:
438
+ """Execute a statement and discard any returned rows.
439
+
440
+ Args:
441
+ sql: A SQL statement with positional placeholders such as $1 and $2.
442
+ *params: Values bound to the placeholders in order.
443
+
444
+ Returns:
445
+ An ExecResult containing the affected-row count reported by
446
+ Postgres. Any rows returned by the statement are discarded.
447
+
448
+ Raises:
449
+ EncoreError: The runtime cannot acquire a connection or the
450
+ statement fails.
451
+ """
452
+ return await self._writer.exec(sql, *params)
453
+
454
+ @property
455
+ def connection_string(self) -> str:
456
+ """The proxy connection string for connecting with another client."""
457
+ return self._writer.connection_string
458
+
459
+ def begin(self) -> _BeginTransaction:
460
+ """Create an awaitable that begins a pooled transaction.
461
+
462
+ Returns:
463
+ An awaitable that produces a Transaction, also usable as an async
464
+ context manager. No transaction starts until the result is awaited
465
+ or entered. A clean context exit commits. An exceptional exit rolls
466
+ back. When awaiting directly, commit or roll back the transaction
467
+ explicitly.
468
+
469
+ Raises:
470
+ EncoreError: When awaited or entered, the runtime cannot acquire a
471
+ connection or begin the transaction.
472
+ """
473
+ return self._writer.begin()
474
+
475
+ def acquire(self) -> _AcquireConnection:
476
+ """Create an awaitable that acquires a pooled connection.
477
+
478
+ Returns:
479
+ An awaitable that produces a Connection, also usable as an async
480
+ context manager. No connection is acquired until the result is
481
+ awaited or entered. Leaving the context closes the connection. When
482
+ awaiting directly, close the connection explicitly after use.
483
+
484
+ Raises:
485
+ EncoreError: When awaited or entered, the runtime cannot acquire a
486
+ connection.
487
+ """
488
+ return self._writer.acquire()
489
+
490
+
491
+ class Connection(Queryable):
492
+ """An acquired connection returned to its pool when closed or destroyed.
493
+
494
+ Obtain a connection with Database.acquire() or DatabaseRef.acquire(). Close
495
+ it explicitly or use it as an async context manager. Operations require an
496
+ open connection with no other active operation, transaction, or row stream.
497
+ Finish the transaction or close the stream before reusing the connection.
498
+ """
499
+
500
+ def __init__(self, impl: _native.PostgresHandle) -> None:
501
+ """Wrap an acquired native connection.
502
+
503
+ Args:
504
+ impl: An acquired native connection handle. Application code should
505
+ obtain a Connection by awaiting or entering acquire().
506
+ """
507
+ self._impl = impl
508
+ super().__init__(_Queries(lambda: self._impl))
509
+
510
+ def begin(self) -> _BeginTransaction:
511
+ """Create an awaitable that begins a transaction on this connection.
512
+
513
+ Returns:
514
+ An awaitable that produces a Transaction, also usable as an async
515
+ context manager. The transaction exclusively borrows this connection
516
+ until it finishes. A clean context exit commits. An exceptional exit
517
+ rolls back. The connection stays acquired afterwards.
518
+
519
+ Raises:
520
+ EncoreError: When awaited or entered, Postgres cannot begin the
521
+ transaction.
522
+ """
523
+ return _BeginTransaction(self._impl)
524
+
525
+ async def close(self) -> None:
526
+ """Return this connection to its pool.
527
+
528
+ Repeated calls have no effect after the connection is closed. Close any
529
+ active cursor and finish any transaction before closing the connection.
530
+
531
+ """
532
+ await self._impl.close()
533
+
534
+ async def __aenter__(self) -> Connection:
535
+ """Return this open connection for use in an async context."""
536
+ if self._impl.is_closed():
537
+ raise EncoreError("database handle is closed")
538
+ return self
539
+
540
+ async def __aexit__(
541
+ self,
542
+ exc_type: type[BaseException] | None,
543
+ exc: BaseException | None,
544
+ tb: TracebackType | None,
545
+ ) -> None:
546
+ """Close the connection without suppressing an exception from the block.
547
+
548
+ Args:
549
+ exc_type: The exception type, or None on a clean exit.
550
+ exc: The exception instance, or None on a clean exit.
551
+ tb: The traceback, or None on a clean exit.
552
+
553
+ """
554
+ await self.close()
555
+
556
+
557
+ class Transaction(Queryable):
558
+ """An open transaction that commits or rolls back on context exit.
559
+
560
+ Obtain a transaction by awaiting or entering begin(). A clean context exit
561
+ commits. An exceptional exit rolls back. Without a context manager, commit
562
+ or roll back explicitly. Abandoned transactions are rolled back when their
563
+ native handles are destroyed.
564
+
565
+ Once commit or rollback starts, it consumes the handle even if Postgres
566
+ reports an error. Operations require an open transaction with no other
567
+ active operation or row stream. Close row streams before committing or
568
+ rolling back.
569
+ """
570
+
571
+ def __init__(self, impl: _native.PostgresHandle) -> None:
572
+ """Wrap an open native transaction.
573
+
574
+ Args:
575
+ impl: An open native transaction handle. Application code should
576
+ obtain a Transaction by awaiting or entering begin().
577
+ """
578
+ self._impl = impl
579
+ super().__init__(_Queries(lambda: self._impl))
580
+
581
+ async def commit(self) -> None:
582
+ """Commit the transaction and close its handle.
583
+
584
+ Close active row streams before committing. Once completion starts, it
585
+ continues if the awaiting task is cancelled, and a borrowed connection
586
+ remains unavailable until completion finishes. A Postgres error still
587
+ consumes the transaction handle.
588
+
589
+ Raises:
590
+ EncoreError: Postgres cannot commit the transaction.
591
+ """
592
+ await self._impl.commit()
593
+
594
+ async def rollback(self) -> None:
595
+ """Roll back the transaction and close its handle.
596
+
597
+ Close active row streams before rolling back. Once completion starts, it
598
+ continues if the awaiting task is cancelled, and a borrowed connection
599
+ remains unavailable until completion finishes. A Postgres error still
600
+ consumes the transaction handle.
601
+
602
+ Raises:
603
+ EncoreError: Postgres cannot roll back the transaction.
604
+ """
605
+ await self._impl.rollback()
606
+
607
+ async def __aenter__(self) -> Transaction:
608
+ """Return this open transaction for use in an async context."""
609
+ if self._impl.is_closed():
610
+ raise EncoreError("database handle is closed")
611
+ return self
612
+
613
+ async def __aexit__(
614
+ self,
615
+ exc_type: type[BaseException] | None,
616
+ exc: BaseException | None,
617
+ tb: TracebackType | None,
618
+ ) -> None:
619
+ """Commit on a clean exit or roll back while an exception unwinds.
620
+
621
+ An already completed transaction needs no further action. If rollback
622
+ fails while handling an exception, the failure is logged and the
623
+ original exception propagates.
624
+
625
+ Args:
626
+ exc_type: The exception type, or None on a clean exit.
627
+ exc: The exception instance, or None on a clean exit.
628
+ tb: The traceback, or None on a clean exit.
629
+
630
+ Raises:
631
+ EncoreError: Committing on a clean exit fails.
632
+ """
633
+ if self._impl.is_closed():
634
+ return
635
+ if exc_type is None:
636
+ await self.commit()
637
+ return
638
+ try:
639
+ await self.rollback()
640
+ except Exception:
641
+ logger.warning(
642
+ "rolling back the transaction failed while unwinding an error",
643
+ exc_info=True,
644
+ )
645
+
646
+
647
+ class _BeginTransaction:
648
+ """An awaitable and async context manager that starts a transaction.
649
+
650
+ No transaction starts until the result is awaited or entered.
651
+ """
652
+
653
+ def __init__(self, impl: _native.PostgresHandle) -> None:
654
+ """Initialize the deferred transaction operation.
655
+
656
+ Args:
657
+ impl: A native database or acquired connection handle.
658
+ """
659
+ self._impl = impl
660
+ self._tx: Transaction | None = None
661
+
662
+ async def _begin(self) -> Transaction:
663
+ """Return a transaction after beginning it on the native handle."""
664
+ return Transaction(await self._impl.begin(source=active_ctx_bytes()))
665
+
666
+ def __await__(self) -> Generator[Any, Any, Transaction]:
667
+ """Return the iterator that drives transaction creation when awaited."""
668
+ return self._begin().__await__()
669
+
670
+ async def __aenter__(self) -> Transaction:
671
+ """Return a new transaction for the async context."""
672
+ self._tx = await self._begin()
673
+ return self._tx
674
+
675
+ async def __aexit__(
676
+ self,
677
+ exc_type: type[BaseException] | None,
678
+ exc: BaseException | None,
679
+ tb: TracebackType | None,
680
+ ) -> None:
681
+ """Commit on a clean exit or roll back on an exceptional exit.
682
+
683
+ Do nothing if the transaction has already finished. A rollback failure
684
+ is logged without suppressing the exception from the block.
685
+
686
+ Args:
687
+ exc_type: The exception type, or None on a clean exit.
688
+ exc: The exception instance, or None on a clean exit.
689
+ tb: The traceback, or None on a clean exit.
690
+ """
691
+ if self._tx is not None:
692
+ await self._tx.__aexit__(exc_type, exc, tb)
693
+
694
+
695
+ class _AcquireConnection:
696
+ """An awaitable and async context manager that acquires a connection.
697
+
698
+ No connection is acquired until the result is awaited or entered.
699
+ """
700
+
701
+ def __init__(self, impl: _native.PostgresHandle) -> None:
702
+ """Initialize the deferred connection acquisition.
703
+
704
+ Args:
705
+ impl: A native database handle whose pool supplies the connection.
706
+ """
707
+ self._impl = impl
708
+ self._conn: Connection | None = None
709
+
710
+ async def _acquire(self) -> Connection:
711
+ """Return a connection after acquiring it from the native pool."""
712
+ return Connection(await self._impl.acquire(source=active_ctx_bytes()))
713
+
714
+ def __await__(self) -> Generator[Any, Any, Connection]:
715
+ """Return the iterator that acquires the connection when awaited."""
716
+ return self._acquire().__await__()
717
+
718
+ async def __aenter__(self) -> Connection:
719
+ """Return a newly acquired connection for the async context."""
720
+ self._conn = await self._acquire()
721
+ return self._conn
722
+
723
+ async def __aexit__(
724
+ self,
725
+ exc_type: type[BaseException] | None,
726
+ exc: BaseException | None,
727
+ tb: TracebackType | None,
728
+ ) -> None:
729
+ """Close the connection when the context exits.
730
+
731
+ Args:
732
+ exc_type: The exception type, or None on a clean exit.
733
+ exc: The exception instance, or None on a clean exit.
734
+ tb: The traceback, or None on a clean exit.
735
+ """
736
+ if self._conn is not None:
737
+ await self._conn.__aexit__(exc_type, exc, tb)