python-neva 2.2.0__tar.gz → 2.3.0__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.
Files changed (122) hide show
  1. {python_neva-2.2.0 → python_neva-2.3.0}/PKG-INFO +2 -1
  2. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/connection.py +7 -4
  3. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/manager.py +16 -5
  4. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/transaction.py +37 -0
  5. python_neva-2.3.0/neva/obs/instrumentation/__init__.py +1 -0
  6. python_neva-2.3.0/neva/obs/instrumentation/sqlalchemy.py +15 -0
  7. {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/test_case.py +1 -0
  8. {python_neva-2.2.0 → python_neva-2.3.0}/pyproject.toml +2 -1
  9. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_sqlalchemy_integration.py +135 -0
  10. {python_neva-2.2.0 → python_neva-2.3.0}/uv.lock +145 -1
  11. {python_neva-2.2.0 → python_neva-2.3.0}/.envrc +0 -0
  12. {python_neva-2.2.0 → python_neva-2.3.0}/.gitignore +0 -0
  13. {python_neva-2.2.0 → python_neva-2.3.0}/.pre-commit-config.yaml +0 -0
  14. {python_neva-2.2.0 → python_neva-2.3.0}/.python-version +0 -0
  15. {python_neva-2.2.0 → python_neva-2.3.0}/README.md +0 -0
  16. {python_neva-2.2.0 → python_neva-2.3.0}/neva/__init__.py +0 -0
  17. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/__init__.py +0 -0
  18. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/app.py +0 -0
  19. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/application.py +0 -0
  20. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/config.py +0 -0
  21. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/facade.py +0 -0
  22. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/faststream.py +0 -0
  23. {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/service_provider.py +0 -0
  24. {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/__init__.py +0 -0
  25. {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/base_providers.py +0 -0
  26. {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/loader.py +0 -0
  27. {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/repository.py +0 -0
  28. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/__init__.py +0 -0
  29. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/config.py +0 -0
  30. {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/provider.py +0 -0
  31. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/__init__.py +0 -0
  32. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/dispatcher.py +0 -0
  33. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/event.py +0 -0
  34. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/event_registry.py +0 -0
  35. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/listener.py +0 -0
  36. {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/provider.py +0 -0
  37. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/__init__.py +0 -0
  38. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/__init__.py +0 -0
  39. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/manager.py +0 -0
  40. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/provider.py +0 -0
  41. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/__init__.py +0 -0
  42. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/correlation.py +0 -0
  43. {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/profiler.py +0 -0
  44. {python_neva-2.2.0 → python_neva-2.3.0}/neva/py.typed +0 -0
  45. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/__init__.py +0 -0
  46. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/__init__.py +0 -0
  47. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/encrypter.py +0 -0
  48. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/protocol.py +0 -0
  49. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/__init__.py +0 -0
  50. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/config.py +0 -0
  51. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hash_manager.py +0 -0
  52. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
  53. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
  54. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  55. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
  56. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/provider.py +0 -0
  57. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/__init__.py +0 -0
  58. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/generate_token.py +0 -0
  59. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/hash_token.py +0 -0
  60. {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/verify_token.py +0 -0
  61. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/__init__.py +0 -0
  62. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/accessors.py +0 -0
  63. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/__init__.py +0 -0
  64. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/app.py +0 -0
  65. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/app.pyi +0 -0
  66. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/config.py +0 -0
  67. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/config.pyi +0 -0
  68. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/crypt.py +0 -0
  69. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/crypt.pyi +0 -0
  70. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/db.py +0 -0
  71. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/db.pyi +0 -0
  72. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/event.py +0 -0
  73. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/event.pyi +0 -0
  74. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/hash.py +0 -0
  75. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/hash.pyi +0 -0
  76. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/log.py +0 -0
  77. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/log.pyi +0 -0
  78. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/results.py +0 -0
  79. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/strategy.py +0 -0
  80. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/strconv.py +0 -0
  81. {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/time.py +0 -0
  82. {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/__init__.py +0 -0
  83. {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/fakes.py +0 -0
  84. {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/fixtures.py +0 -0
  85. {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/http.py +0 -0
  86. {python_neva-2.2.0 → python_neva-2.3.0}/ruff.toml +0 -0
  87. {python_neva-2.2.0 → python_neva-2.3.0}/tests/__init__.py +0 -0
  88. {python_neva-2.2.0 → python_neva-2.3.0}/tests/arch/__init__.py +0 -0
  89. {python_neva-2.2.0 → python_neva-2.3.0}/tests/arch/test_scope.py +0 -0
  90. {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/__init__.py +0 -0
  91. {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/test_loader.py +0 -0
  92. {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/test_repository.py +0 -0
  93. {python_neva-2.2.0 → python_neva-2.3.0}/tests/conftest.py +0 -0
  94. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/__init__.py +0 -0
  95. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/conftest.py +0 -0
  96. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_connection_manager.py +0 -0
  97. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_database_manager.py +0 -0
  98. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_edge_cases.py +0 -0
  99. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_multi_connection.py +0 -0
  100. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction.py +0 -0
  101. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction_context.py +0 -0
  102. {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction_registry.py +0 -0
  103. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/__init__.py +0 -0
  104. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/conftest.py +0 -0
  105. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_deferred.py +0 -0
  106. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_dispatch.py +0 -0
  107. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_event.py +0 -0
  108. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_event_registry.py +0 -0
  109. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_function_listener.py +0 -0
  110. {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_immediate.py +0 -0
  111. {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/__init__.py +0 -0
  112. {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/test_correlation.py +0 -0
  113. {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/test_profiler.py +0 -0
  114. {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/__init__.py +0 -0
  115. {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/test_encrypter.py +0 -0
  116. {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/test_hash_manager.py +0 -0
  117. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/__init__.py +0 -0
  118. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_event_fake.py +0 -0
  119. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_facade_restore.py +0 -0
  120. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_fixtures.py +0 -0
  121. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_refresh_database.py +0 -0
  122. {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_test_case.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -9,6 +9,7 @@ Requires-Dist: cryptography>=46.0.3
9
9
  Requires-Dist: dishka>=1.7.2
10
10
  Requires-Dist: fastapi[all]>=0.129.0
11
11
  Requires-Dist: faststream>=0.6.6
12
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
12
13
  Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
13
14
  Requires-Dist: pyinstrument>=5.1.1
14
15
  Requires-Dist: sqlalchemy[asyncio]>=2.0.0
@@ -152,11 +152,11 @@ class ConnectionManager:
152
152
  For nested calls on the same connection, reuses the parent session
153
153
  and begins a savepoint instead of a full transaction.
154
154
 
155
- Raises:
156
- RuntimeError: If no engine has been registered for this connection.
157
-
158
155
  Yields:
159
156
  BoundTransaction: A transaction with a guaranteed non-null session.
157
+
158
+ Raises:
159
+ RuntimeError: If no engine has been registered for this connection.
160
160
  """
161
161
  if self.session_factory is None:
162
162
  raise RuntimeError(
@@ -167,7 +167,10 @@ class ConnectionManager:
167
167
  parent = self.tx_context.current(self.name)
168
168
 
169
169
  match parent:
170
- case Some(parent_tx) if isinstance(parent_tx, BoundTransaction):
170
+ case Some(parent_tx) if (
171
+ isinstance(parent_tx, BoundTransaction)
172
+ and parent_tx.is_accessible_from_current_task
173
+ ):
171
174
  tx = Transaction(self.name)
172
175
  tx.parent = parent
173
176
  bound = tx.begin(parent_tx.session)
@@ -10,6 +10,7 @@ from neva import Nothing, Option, Some
10
10
  from neva.database.connection import ConnectionManager, TransactionContext
11
11
  from neva.database.transaction import BoundTransaction, Transaction
12
12
  from neva.obs import LogManager
13
+ from neva.obs.instrumentation.sqlalchemy import instrument
13
14
 
14
15
 
15
16
  @final
@@ -30,6 +31,7 @@ class DatabaseManager:
30
31
  name: The connection name.
31
32
  engine: The async engine.
32
33
  """
34
+ instrument(engine)
33
35
  self._engines[name] = engine
34
36
  self._session_factories[name] = async_sessionmaker(
35
37
  bind=engine,
@@ -60,15 +62,24 @@ class DatabaseManager:
60
62
  def session(self, connection: str | None = None) -> Option[AsyncSession]:
61
63
  """Returns the current session for a connection.
62
64
 
65
+ Only returns a session if the active transaction was opened by the
66
+ current asyncio task. Transactions inherited from a parent task
67
+ (e.g. via context propagation in Strawberry GraphQL resolvers) are
68
+ not accessible this way — each task must call ``begin()`` to obtain
69
+ its own session.
70
+
63
71
  Args:
64
72
  connection: The connection name. Defaults to "default".
65
73
 
66
74
  Returns:
67
75
  The current session, if any. Returns Nothing if there is no active
68
- bound transaction on the given connection.
76
+ bound transaction on the given connection, or if the active
77
+ transaction was opened by a different asyncio task.
69
78
  """
70
79
  match self.current(connection):
71
- case Some(tx) if isinstance(tx, BoundTransaction):
80
+ case Some(tx) if (
81
+ isinstance(tx, BoundTransaction) and tx.is_accessible_from_current_task
82
+ ):
72
83
  return Some(tx.session)
73
84
  case _:
74
85
  return Nothing()
@@ -93,11 +104,11 @@ class DatabaseManager:
93
104
  Args:
94
105
  name: The connection name.
95
106
 
96
- Raises:
97
- RuntimeError: If no engine has been registered for the connection.
98
-
99
107
  Yields:
100
108
  BoundTransaction: A transaction with a guaranteed non-null session.
109
+
110
+ Raises:
111
+ RuntimeError: If no engine has been registered for the connection.
101
112
  """ # noqa: DOC502 explicit documentation of the possible exception raised by 'begin'
102
113
  async with self.connection(name).begin() as tx:
103
114
  yield tx
@@ -1,5 +1,6 @@
1
1
  """Transaction management systems."""
2
2
 
3
+ import asyncio
3
4
  from collections.abc import Awaitable
4
5
  from dataclasses import dataclass, field
5
6
  from enum import Enum, auto
@@ -128,3 +129,39 @@ class BoundTransaction(Transaction):
128
129
  """Transaction bound to an SQLAlchemy session."""
129
130
 
130
131
  session: AsyncSession
132
+ _owning_task: asyncio.Task[object] | None = field(default=None, init=False)
133
+ _shared: bool = field(default=False, init=False)
134
+
135
+ def __post_init__(self) -> None:
136
+ """Capture the current asyncio task as the owner of this transaction."""
137
+ self._owning_task = asyncio.current_task()
138
+
139
+ def share(self) -> None:
140
+ """Allow any asyncio task to access this transaction's session.
141
+
142
+ By default, a transaction is only accessible from the task that
143
+ created it. Calling this method lifts that restriction, allowing
144
+ other tasks that inherited the transaction context to use the same
145
+ session.
146
+
147
+ This is intended for controlled sequential-access patterns such as
148
+ test isolation wrappers (e.g. ``RefreshDatabase``) where a fixture
149
+ opens a transaction and the test body runs inside it. It must
150
+ **not** be used when multiple tasks may access the session
151
+ concurrently, as ``AsyncSession`` is not concurrent-safe.
152
+ """
153
+ self._shared = True
154
+
155
+ @property
156
+ def is_accessible_from_current_task(self) -> bool:
157
+ """Return True if the current asyncio task may use this transaction's session.
158
+
159
+ Returns:
160
+ True if the transaction was created by the current task, or if
161
+ ``share()`` has been called. False if called from a different
162
+ task without sharing, or from outside any async task.
163
+ """
164
+ if self._shared:
165
+ return True
166
+ current = asyncio.current_task()
167
+ return current is not None and current is self._owning_task
@@ -0,0 +1 @@
1
+ """Various OTel instrumentation."""
@@ -0,0 +1,15 @@
1
+ """SQLAlchemy instrumentation."""
2
+
3
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
4
+ from sqlalchemy import Engine
5
+ from sqlalchemy.ext.asyncio import AsyncEngine
6
+
7
+
8
+ def instrument(engine: AsyncEngine | Engine) -> None:
9
+ """Instrument SQLAlchemy engine."""
10
+ match engine:
11
+ case AsyncEngine():
12
+ to_instrument = engine.sync_engine
13
+ case Engine():
14
+ to_instrument = engine
15
+ SQLAlchemyInstrumentor().instrument(engine=to_instrument)
@@ -97,5 +97,6 @@ class RefreshDatabase:
97
97
  """Wrap the test in a DB transaction and roll it back on completion."""
98
98
  db = _test_case_application.make(DatabaseManager).unwrap()
99
99
  async with db.begin() as tx:
100
+ tx.share()
100
101
  yield
101
102
  await tx.rollback()
@@ -7,7 +7,7 @@ packages = ["neva"]
7
7
 
8
8
  [project]
9
9
  name = "python-neva"
10
- version = "2.2.0"
10
+ version = "2.3.0"
11
11
  description = "Add your description here"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "asyncpg>=0.30.0",
24
24
  "aiosqlite>=0.20.0",
25
25
  "typer>=0.21.1",
26
+ "opentelemetry-instrumentation-sqlalchemy>=0.62b0",
26
27
  ]
27
28
 
28
29
  [project.optional-dependencies]
@@ -1,5 +1,6 @@
1
1
  """SQLAlchemy integration tests with real in-memory SQLite."""
2
2
 
3
+ import asyncio
3
4
  from collections.abc import AsyncIterator
4
5
  from typing import final
5
6
 
@@ -177,3 +178,137 @@ class TestDatabaseManagerClose:
177
178
 
178
179
  manager_after = db.connection("default")
179
180
  assert manager_before is not manager_after
181
+
182
+
183
+ class TestConcurrentTaskIsolation:
184
+ async def test_session_not_visible_from_different_task(
185
+ self, db: DatabaseManager
186
+ ) -> None:
187
+ result: list[bool] = []
188
+
189
+ async with db.begin():
190
+
191
+ async def child() -> None:
192
+ result.append(db.session().is_some)
193
+
194
+ await asyncio.create_task(child())
195
+
196
+ assert result == [False]
197
+
198
+ async def test_session_visible_from_same_task(self, db: DatabaseManager) -> None:
199
+ async with db.begin() as tx:
200
+ assert db.session().is_some
201
+ assert db.session().unwrap() is tx.session
202
+
203
+ async def test_begin_from_different_task_creates_new_session(
204
+ self, db: DatabaseManager
205
+ ) -> None:
206
+ outer_session_id: list[int] = []
207
+ inner_session_id: list[int] = []
208
+
209
+ async with db.begin() as outer:
210
+ outer_session_id.append(id(outer.session))
211
+
212
+ async def child() -> None:
213
+ async with db.begin() as inner:
214
+ inner_session_id.append(id(inner.session))
215
+
216
+ await asyncio.create_task(child())
217
+
218
+ assert outer_session_id[0] != inner_session_id[0]
219
+
220
+ async def test_begin_from_different_task_has_no_parent(
221
+ self, db: DatabaseManager
222
+ ) -> None:
223
+ parent_set: list[bool] = []
224
+
225
+ async with db.begin():
226
+
227
+ async def child() -> None:
228
+ async with db.begin() as tx:
229
+ parent_set.append(tx.parent.is_some)
230
+
231
+ await asyncio.create_task(child())
232
+
233
+ assert parent_set == [False]
234
+
235
+ async def test_concurrent_tasks_get_distinct_sessions(
236
+ self, db: DatabaseManager
237
+ ) -> None:
238
+ session_ids: list[int] = []
239
+
240
+ async with db.begin():
241
+
242
+ async def task_work() -> None:
243
+ async with db.begin() as tx:
244
+ session_ids.append(id(tx.session))
245
+
246
+ _ = await asyncio.gather(
247
+ asyncio.create_task(task_work()),
248
+ asyncio.create_task(task_work()),
249
+ asyncio.create_task(task_work()),
250
+ )
251
+
252
+ assert len(session_ids) == 3
253
+ assert len(set(session_ids)) == 3
254
+
255
+ async def test_concurrent_reads_do_not_raise(self, db: DatabaseManager) -> None:
256
+ errors: list[Exception] = []
257
+
258
+ async with db.begin():
259
+
260
+ async def resolver() -> None:
261
+ try:
262
+ async with db.begin() as tx:
263
+ _ = await tx.session.execute(select(User))
264
+ except Exception as exc:
265
+ errors.append(exc)
266
+
267
+ _ = await asyncio.gather(
268
+ asyncio.create_task(resolver()),
269
+ asyncio.create_task(resolver()),
270
+ asyncio.create_task(resolver()),
271
+ )
272
+
273
+ assert not errors, f"Unexpected errors: {errors}"
274
+
275
+ async def test_nested_begin_same_task_still_uses_savepoint(
276
+ self, db: DatabaseManager
277
+ ) -> None:
278
+ async with db.begin() as outer, db.connection("default").begin() as inner:
279
+ assert inner.session is outer.session
280
+ assert inner.parent.is_some
281
+
282
+ async def test_shared_transaction_is_visible_from_different_task(
283
+ self, db: DatabaseManager
284
+ ) -> None:
285
+ result: list[bool] = []
286
+
287
+ async with db.begin() as tx:
288
+ tx.share()
289
+
290
+ async def child() -> None:
291
+ result.append(db.session().is_some)
292
+
293
+ await asyncio.create_task(child())
294
+
295
+ assert result == [True]
296
+
297
+ async def test_shared_transaction_child_begin_creates_savepoint(
298
+ self, db: DatabaseManager
299
+ ) -> None:
300
+ inner_parent_set: list[bool] = []
301
+ same_session: list[bool] = []
302
+
303
+ async with db.begin() as outer:
304
+ outer.share()
305
+
306
+ async def child() -> None:
307
+ async with db.begin() as inner:
308
+ inner_parent_set.append(inner.parent.is_some)
309
+ same_session.append(inner.session is outer.session)
310
+
311
+ await asyncio.create_task(child())
312
+
313
+ assert inner_parent_set == [True]
314
+ assert same_session == [True]
@@ -795,6 +795,18 @@ wheels = [
795
795
  { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
796
796
  ]
797
797
 
798
+ [[package]]
799
+ name = "importlib-metadata"
800
+ version = "8.7.1"
801
+ source = { registry = "https://pypi.org/simple" }
802
+ dependencies = [
803
+ { name = "zipp" },
804
+ ]
805
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
806
+ wheels = [
807
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
808
+ ]
809
+
798
810
  [[package]]
799
811
  name = "iniconfig"
800
812
  version = "2.3.0"
@@ -1020,6 +1032,63 @@ wheels = [
1020
1032
  { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
1021
1033
  ]
1022
1034
 
1035
+ [[package]]
1036
+ name = "opentelemetry-api"
1037
+ version = "1.41.0"
1038
+ source = { registry = "https://pypi.org/simple" }
1039
+ dependencies = [
1040
+ { name = "importlib-metadata" },
1041
+ { name = "typing-extensions" },
1042
+ ]
1043
+ sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" }
1044
+ wheels = [
1045
+ { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" },
1046
+ ]
1047
+
1048
+ [[package]]
1049
+ name = "opentelemetry-instrumentation"
1050
+ version = "0.62b0"
1051
+ source = { registry = "https://pypi.org/simple" }
1052
+ dependencies = [
1053
+ { name = "opentelemetry-api" },
1054
+ { name = "opentelemetry-semantic-conventions" },
1055
+ { name = "packaging" },
1056
+ { name = "wrapt" },
1057
+ ]
1058
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/fd/b8e90bb340957f059084376f94cff336b0e871a42feba7d3f7342365e987/opentelemetry_instrumentation-0.62b0.tar.gz", hash = "sha256:aa1b0b9ab2e1722c2a8a5384fb016fc28d30bba51826676c8036074790d2861e", size = 34042, upload-time = "2026-04-09T14:40:22.843Z" }
1059
+ wheels = [
1060
+ { url = "https://files.pythonhosted.org/packages/00/b6/3356d2e335e3c449c5183e9b023f30f04f1b7073a6583c68745ea2e704b1/opentelemetry_instrumentation-0.62b0-py3-none-any.whl", hash = "sha256:30d4e76486eae64fb095264a70c2c809c4bed17b73373e53091470661f7d477c", size = 34158, upload-time = "2026-04-09T14:39:21.428Z" },
1061
+ ]
1062
+
1063
+ [[package]]
1064
+ name = "opentelemetry-instrumentation-sqlalchemy"
1065
+ version = "0.62b0"
1066
+ source = { registry = "https://pypi.org/simple" }
1067
+ dependencies = [
1068
+ { name = "opentelemetry-api" },
1069
+ { name = "opentelemetry-instrumentation" },
1070
+ { name = "opentelemetry-semantic-conventions" },
1071
+ { name = "packaging" },
1072
+ { name = "wrapt" },
1073
+ ]
1074
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/3d/40adc8c38e5be017ceb230a28ca57ca81981d4dc0c4b902cc930c77fd14f/opentelemetry_instrumentation_sqlalchemy-0.62b0.tar.gz", hash = "sha256:d02f85b83f349e9ef70a34cb3f4c3a3481fa15b11747f09209818663e161cac4", size = 18539, upload-time = "2026-04-09T14:40:50.251Z" }
1075
+ wheels = [
1076
+ { url = "https://files.pythonhosted.org/packages/e7/e0/77954ac593f34740dc32e28a15fe7170e90f6ba6398eaaa5c88b34c05ed1/opentelemetry_instrumentation_sqlalchemy-0.62b0-py3-none-any.whl", hash = "sha256:ec576e0660080d9d15ce4fa44d2a07fff8cb4b796a84344cb0f2c9e5d6e26f79", size = 15534, upload-time = "2026-04-09T14:40:03.957Z" },
1077
+ ]
1078
+
1079
+ [[package]]
1080
+ name = "opentelemetry-semantic-conventions"
1081
+ version = "0.62b0"
1082
+ source = { registry = "https://pypi.org/simple" }
1083
+ dependencies = [
1084
+ { name = "opentelemetry-api" },
1085
+ { name = "typing-extensions" },
1086
+ ]
1087
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" }
1088
+ wheels = [
1089
+ { url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" },
1090
+ ]
1091
+
1023
1092
  [[package]]
1024
1093
  name = "orjson"
1025
1094
  version = "3.11.5"
@@ -1438,7 +1507,7 @@ wheels = [
1438
1507
 
1439
1508
  [[package]]
1440
1509
  name = "python-neva"
1441
- version = "2.2.0"
1510
+ version = "2.3.0"
1442
1511
  source = { editable = "." }
1443
1512
  dependencies = [
1444
1513
  { name = "aiosqlite" },
@@ -1447,6 +1516,7 @@ dependencies = [
1447
1516
  { name = "dishka" },
1448
1517
  { name = "fastapi", extra = ["all"] },
1449
1518
  { name = "faststream" },
1519
+ { name = "opentelemetry-instrumentation-sqlalchemy" },
1450
1520
  { name = "pwdlib", extra = ["argon2", "bcrypt"] },
1451
1521
  { name = "pyinstrument" },
1452
1522
  { name = "sqlalchemy", extra = ["asyncio"] },
@@ -1482,6 +1552,7 @@ requires-dist = [
1482
1552
  { name = "dishka", specifier = ">=1.7.2" },
1483
1553
  { name = "fastapi", extras = ["all"], specifier = ">=0.129.0" },
1484
1554
  { name = "faststream", specifier = ">=0.6.6" },
1555
+ { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.62b0" },
1485
1556
  { name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
1486
1557
  { name = "pyinstrument", specifier = ">=5.1.1" },
1487
1558
  { name = "pytest", marker = "extra == 'testing'", specifier = ">=9.0.2" },
@@ -2062,3 +2133,76 @@ wheels = [
2062
2133
  { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
2063
2134
  { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
2064
2135
  ]
2136
+
2137
+ [[package]]
2138
+ name = "wrapt"
2139
+ version = "2.1.2"
2140
+ source = { registry = "https://pypi.org/simple" }
2141
+ sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
2142
+ wheels = [
2143
+ { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" },
2144
+ { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" },
2145
+ { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" },
2146
+ { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" },
2147
+ { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" },
2148
+ { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" },
2149
+ { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" },
2150
+ { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" },
2151
+ { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" },
2152
+ { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" },
2153
+ { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" },
2154
+ { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" },
2155
+ { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" },
2156
+ { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" },
2157
+ { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" },
2158
+ { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" },
2159
+ { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" },
2160
+ { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" },
2161
+ { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" },
2162
+ { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" },
2163
+ { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" },
2164
+ { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" },
2165
+ { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" },
2166
+ { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" },
2167
+ { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" },
2168
+ { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" },
2169
+ { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" },
2170
+ { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" },
2171
+ { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" },
2172
+ { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" },
2173
+ { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" },
2174
+ { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" },
2175
+ { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" },
2176
+ { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" },
2177
+ { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" },
2178
+ { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" },
2179
+ { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" },
2180
+ { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" },
2181
+ { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" },
2182
+ { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" },
2183
+ { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" },
2184
+ { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" },
2185
+ { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" },
2186
+ { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" },
2187
+ { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" },
2188
+ { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" },
2189
+ { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" },
2190
+ { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" },
2191
+ { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" },
2192
+ { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" },
2193
+ { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" },
2194
+ { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" },
2195
+ { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" },
2196
+ { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" },
2197
+ { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" },
2198
+ { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
2199
+ ]
2200
+
2201
+ [[package]]
2202
+ name = "zipp"
2203
+ version = "3.23.0"
2204
+ source = { registry = "https://pypi.org/simple" }
2205
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
2206
+ wheels = [
2207
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
2208
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes