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.
- {python_neva-2.2.0 → python_neva-2.3.0}/PKG-INFO +2 -1
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/connection.py +7 -4
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/manager.py +16 -5
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/transaction.py +37 -0
- python_neva-2.3.0/neva/obs/instrumentation/__init__.py +1 -0
- python_neva-2.3.0/neva/obs/instrumentation/sqlalchemy.py +15 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/test_case.py +1 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/pyproject.toml +2 -1
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_sqlalchemy_integration.py +135 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/uv.lock +145 -1
- {python_neva-2.2.0 → python_neva-2.3.0}/.envrc +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/.gitignore +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/.pre-commit-config.yaml +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/.python-version +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/README.md +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/app.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/application.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/config.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/facade.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/faststream.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/arch/service_provider.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/base_providers.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/loader.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/config/repository.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/config.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/database/provider.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/dispatcher.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/event.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/event_registry.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/listener.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/events/provider.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/py.typed +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/config.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/provider.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/accessors.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/app.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/config.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/db.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/event.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/hash.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/log.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/results.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/strategy.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/strconv.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/support/time.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/fakes.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/fixtures.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/neva/testing/http.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/ruff.toml +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/arch/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/arch/test_scope.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/test_loader.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/config/test_repository.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/conftest.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/conftest.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction_context.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/conftest.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_deferred.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_dispatch.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_event.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_event_registry.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/events/test_immediate.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/test_encrypter.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/security/test_hash_manager.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/__init__.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_fixtures.py +0 -0
- {python_neva-2.2.0 → python_neva-2.3.0}/tests/testing/test_refresh_database.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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)
|
|
@@ -7,7 +7,7 @@ packages = ["neva"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "python-neva"
|
|
10
|
-
version = "2.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|