kiba-core 0.5.3.dev89__tar.gz → 0.5.3.dev91__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.
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/CHANGELOG.md +1 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/store/database.py +19 -5
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/store/entity_repository.py +12 -2
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/pyproject.toml +1 -1
- kiba_core-0.5.3.dev91/tests/api/test_database_connection_middleware.py +168 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/test_entity_repository.py +43 -0
- kiba_core-0.5.3.dev89/tests/api/test_database_connection_middleware.py +0 -81
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.github/workflows/publish.yml +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/.gitignore +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/README.md +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/database_connection_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/request_body.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/uploaded_file.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/web3/eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/SOURCES.txt +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/requires.txt +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/makefile +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_json_route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/test_database.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/test_logging.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/web3/test_eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/web3/test_rest_eth_client.py +0 -0
- {kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/uv.lock +0 -0
|
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- [PATCH] Fixed `DatabaseConnectionMiddleware`'s commit when the response starts failing ("another operation is in progress") for streaming responses behind a `BaseHTTPMiddleware`: `Database.execute` and `Database.commit_context_connection` now wait for each other on the same connection
|
|
11
12
|
- [MINOR] Added `Database.commit_context_connection` to commit the context connection's writes so far; anything run on it afterwards is committed when the context connection ends
|
|
12
13
|
- [PATCH] Updated `DatabaseConnectionMiddleware` to commit before the response starts, so a client can't get the response and make its next request before the request's writes are visible (writes made while streaming are still committed when the request ends)
|
|
13
14
|
- [PATCH] Fixed `json_route` and `streaming_json_route` leaving the temporary files of multipart uploads open
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import contextlib
|
|
2
3
|
import contextvars
|
|
3
4
|
import datetime
|
|
@@ -47,6 +48,7 @@ class Database:
|
|
|
47
48
|
self._engine: AsyncEngine | None = None
|
|
48
49
|
self._connectionContext = contextvars.ContextVar[DatabaseConnection | None]('_connectionContext')
|
|
49
50
|
self._connectionsWithWrites = weakref.WeakSet[DatabaseConnection]()
|
|
51
|
+
self._connectionLocks = weakref.WeakKeyDictionary[DatabaseConnection, asyncio.Lock]()
|
|
50
52
|
|
|
51
53
|
async def connect(self, poolSize: int = 100) -> None:
|
|
52
54
|
if not self._engine:
|
|
@@ -86,6 +88,16 @@ class Database:
|
|
|
86
88
|
async with self._engine.begin() as connection:
|
|
87
89
|
yield connection
|
|
88
90
|
|
|
91
|
+
# NOTE(krishan711): a connection can only run one operation at a time, so execute and commit_context_connection wait
|
|
92
|
+
# for each other. e.g. DatabaseConnectionMiddleware commits when the response starts, which (behind a BaseHTTPMiddleware)
|
|
93
|
+
# can be while the route, in another task, is still running queries for a streaming response
|
|
94
|
+
def _get_connection_lock(self, connection: DatabaseConnection) -> asyncio.Lock:
|
|
95
|
+
lock = self._connectionLocks.get(connection)
|
|
96
|
+
if lock is None:
|
|
97
|
+
lock = asyncio.Lock()
|
|
98
|
+
self._connectionLocks[connection] = lock
|
|
99
|
+
return lock
|
|
100
|
+
|
|
89
101
|
def _get_context_connection(self) -> DatabaseConnection | None:
|
|
90
102
|
try:
|
|
91
103
|
connection = self._connectionContext.get()
|
|
@@ -128,8 +140,9 @@ class Database:
|
|
|
128
140
|
connection = self._get_context_connection()
|
|
129
141
|
if not connection:
|
|
130
142
|
raise InternalServerErrorException(message='No connection found. Please call create_context_connection() for the context.')
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
async with self._get_connection_lock(connection=connection):
|
|
144
|
+
await connection.commit()
|
|
145
|
+
self._connectionsWithWrites.discard(connection)
|
|
133
146
|
|
|
134
147
|
# NOTE(krishan711): unlike create_context_connection this may be used inside an existing context connection.
|
|
135
148
|
# Everything in the block runs on a new transaction that commits when the block exits, independent of the
|
|
@@ -164,6 +177,7 @@ class Database:
|
|
|
164
177
|
connection = self._get_context_connection()
|
|
165
178
|
if not connection:
|
|
166
179
|
raise InternalServerErrorException(message='No connection found. Please provide a connection or call create_context_connection() for the context.')
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
async with self._get_connection_lock(connection=connection):
|
|
181
|
+
if isinstance(query, UpdateBase):
|
|
182
|
+
self._connectionsWithWrites.add(connection)
|
|
183
|
+
return typing.cast(Result[*tuple[typing.Any, ...]], await connection.execute(statement=query)) # type: ignore[explicit-any]
|
|
@@ -9,6 +9,7 @@ from sqlalchemy.dialects import sqlite as sqlalchemy_sqlite
|
|
|
9
9
|
from sqlalchemy.engine import Result
|
|
10
10
|
from sqlalchemy.engine import RowMapping
|
|
11
11
|
from sqlalchemy.sql import Select
|
|
12
|
+
from sqlalchemy.sql.base import ReadOnlyColumnCollection
|
|
12
13
|
|
|
13
14
|
from core.exceptions import KibaException
|
|
14
15
|
from core.exceptions import NotFoundException
|
|
@@ -25,6 +26,7 @@ from core.store.retriever import uuid_from_value
|
|
|
25
26
|
from core.util import date_util
|
|
26
27
|
|
|
27
28
|
EntityType = typing.TypeVar('EntityType', bound=BaseModel)
|
|
29
|
+
UpsertUpdateWhere = typing.Callable[[ReadOnlyColumnCollection[str, typing.Any]], sqlalchemy.ColumnElement[bool]] # type: ignore[explicit-any]
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
|
|
@@ -135,7 +137,8 @@ class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
|
|
|
135
137
|
result = await database.execute(query=self.table.update().where(self.idColumn == idValue).values(updateValues).returning(*self.list_select_columns()), connection=connection)
|
|
136
138
|
return self.force_from_result(result=result)
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
# NOTE(krishan711): updateWhere gets the conflicting row's new values (`excluded`) and only updates when it is true; when it is false nothing is written and the stored row is returned
|
|
141
|
+
async def upsert(self, database: Database, constraintColumnNames: list[str], connection: DatabaseConnection | None = None, updateWhere: UpsertUpdateWhere | None = None, **kwargs) -> EntityType: # type: ignore[no-untyped-def] # noqa: ANN003
|
|
139
142
|
updateValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=False)
|
|
140
143
|
insertValues = self._create_values(kwargs=kwargs, shouldAddUpdatedDate=True, shouldAddCreatedDate=True)
|
|
141
144
|
self._add_generated_id(values=insertValues)
|
|
@@ -144,9 +147,16 @@ class EntityRepository(typing.Generic[EntityType]): # noqa: UP046
|
|
|
144
147
|
doUpdateStatement = insertStatement.on_conflict_do_update(
|
|
145
148
|
index_elements=constraintColumns,
|
|
146
149
|
set_=updateValues,
|
|
150
|
+
where=updateWhere(insertStatement.excluded) if updateWhere is not None else None,
|
|
147
151
|
)
|
|
148
152
|
result = await database.execute(query=doUpdateStatement.returning(*self.list_select_columns()), connection=connection)
|
|
149
|
-
|
|
153
|
+
row = result.mappings().first()
|
|
154
|
+
if row is None and updateWhere is not None:
|
|
155
|
+
storedQuery = self.build_select().where(*[column == insertValues[column] for column in constraintColumns])
|
|
156
|
+
row = (await database.execute(query=storedQuery, connection=connection)).mappings().first()
|
|
157
|
+
if row is None:
|
|
158
|
+
raise NotFoundException
|
|
159
|
+
return self.from_row(row=row)
|
|
150
160
|
|
|
151
161
|
async def upsert_many(self, database: Database, constraintColumnNames: list[str], rowDicts: list[dict[str, typing.Any]], connection: DatabaseConnection | None = None) -> list[EntityType]: # type: ignore[explicit-any]
|
|
152
162
|
if not rowDicts:
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import pytest_asyncio
|
|
8
|
+
import sqlalchemy
|
|
9
|
+
from sqlalchemy import Column
|
|
10
|
+
from sqlalchemy import MetaData
|
|
11
|
+
from sqlalchemy import Table
|
|
12
|
+
from sqlalchemy import insert
|
|
13
|
+
from sqlalchemy import select
|
|
14
|
+
from sqlalchemy import text
|
|
15
|
+
from starlette.applications import Starlette
|
|
16
|
+
from starlette.middleware import Middleware
|
|
17
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
18
|
+
from starlette.requests import Request
|
|
19
|
+
from starlette.responses import StreamingResponse
|
|
20
|
+
from starlette.routing import Route
|
|
21
|
+
|
|
22
|
+
from core.api.middleware.database_connection_middleware import DatabaseConnectionMiddleware
|
|
23
|
+
from core.store.database import Database
|
|
24
|
+
|
|
25
|
+
PSQL_CONNECTION_STRING = os.environ.get('CORE_TEST_PSQL_CONNECTION_STRING')
|
|
26
|
+
|
|
27
|
+
metadata = MetaData()
|
|
28
|
+
records = Table(f'records_{uuid.uuid4().hex}', metadata, Column('name', sqlalchemy.Text, primary_key=True))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest_asyncio.fixture(params=['sqlite', 'postgresql'] if PSQL_CONNECTION_STRING else ['sqlite'])
|
|
32
|
+
async def database(request, tmp_path):
|
|
33
|
+
connectionString = PSQL_CONNECTION_STRING if request.param == 'postgresql' else Database.create_sqlite_connection_string(str(tmp_path / 'database.sqlite'))
|
|
34
|
+
database = Database(connectionString=connectionString)
|
|
35
|
+
await database.connect(poolSize=2)
|
|
36
|
+
async with database.create_transaction() as connection:
|
|
37
|
+
await connection.run_sync(metadata.drop_all)
|
|
38
|
+
await connection.run_sync(metadata.create_all)
|
|
39
|
+
yield database
|
|
40
|
+
async with database.create_transaction() as connection:
|
|
41
|
+
await connection.run_sync(metadata.drop_all)
|
|
42
|
+
await database.disconnect()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def get_record_names(database: Database) -> list[str]:
|
|
46
|
+
async with database.create_transaction() as connection:
|
|
47
|
+
result = await database.execute(select(records.c.name).order_by(records.c.name), connection=connection)
|
|
48
|
+
return list(result.scalars().all())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def call_app(app, database: Database) -> list[tuple[str, list[str]]]:
|
|
52
|
+
sentMessages: list[tuple[str, list[str]]] = []
|
|
53
|
+
|
|
54
|
+
async def receive():
|
|
55
|
+
return {'type': 'http.request', 'body': b'', 'more_body': False}
|
|
56
|
+
|
|
57
|
+
async def send(message):
|
|
58
|
+
sentMessages.append((message['type'], await get_record_names(database=database)))
|
|
59
|
+
|
|
60
|
+
await DatabaseConnectionMiddleware(app=app, database=database)({'type': 'http', 'method': 'POST', 'path': '/'}, receive, send)
|
|
61
|
+
return sentMessages
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_writes_are_committed_before_the_response_starts(database):
|
|
66
|
+
async def app(scope, receive, send):
|
|
67
|
+
await database.execute(insert(records).values(name='request'))
|
|
68
|
+
await send({'type': 'http.response.start', 'status': 200, 'headers': []})
|
|
69
|
+
await send({'type': 'http.response.body', 'body': b'ok'})
|
|
70
|
+
|
|
71
|
+
sentMessages = await call_app(app=app, database=database)
|
|
72
|
+
|
|
73
|
+
assert sentMessages == [('http.response.start', ['request']), ('http.response.body', ['request'])]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_writes_while_streaming_are_committed_when_the_request_ends(database):
|
|
78
|
+
async def app(scope, receive, send):
|
|
79
|
+
await database.execute(insert(records).values(name='before'))
|
|
80
|
+
await send({'type': 'http.response.start', 'status': 200, 'headers': []})
|
|
81
|
+
await database.execute(insert(records).values(name='streaming'))
|
|
82
|
+
await send({'type': 'http.response.body', 'body': b'ok'})
|
|
83
|
+
|
|
84
|
+
sentMessages = await call_app(app=app, database=database)
|
|
85
|
+
|
|
86
|
+
assert sentMessages == [('http.response.start', ['before']), ('http.response.body', ['before'])]
|
|
87
|
+
assert await get_record_names(database=database) == ['before', 'streaming']
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_writes_are_rolled_back_when_the_request_fails_before_responding(database):
|
|
92
|
+
async def app(scope, receive, send):
|
|
93
|
+
await database.execute(insert(records).values(name='request'))
|
|
94
|
+
raise ValueError('failed')
|
|
95
|
+
|
|
96
|
+
with pytest.raises(ValueError, match='failed'):
|
|
97
|
+
await call_app(app=app, database=database)
|
|
98
|
+
|
|
99
|
+
assert await get_record_names(database=database) == []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def post_to_app(app) -> tuple[int, bytes]:
|
|
103
|
+
scope = {
|
|
104
|
+
'type': 'http', 'asgi': {'version': '3.0'}, 'http_version': '1.1', 'method': 'POST', 'scheme': 'http', 'path': '/', 'raw_path': b'/',
|
|
105
|
+
'root_path': '', 'query_string': b'', 'headers': [], 'client': ('127.0.0.1', 1), 'server': ('test', 80),
|
|
106
|
+
}
|
|
107
|
+
isRequestSent = False
|
|
108
|
+
responseFinished = asyncio.Event()
|
|
109
|
+
statusHolder: list[int] = []
|
|
110
|
+
bodyParts: list[bytes] = []
|
|
111
|
+
|
|
112
|
+
async def receive():
|
|
113
|
+
nonlocal isRequestSent
|
|
114
|
+
if not isRequestSent:
|
|
115
|
+
isRequestSent = True
|
|
116
|
+
return {'type': 'http.request', 'body': b'', 'more_body': False}
|
|
117
|
+
await responseFinished.wait()
|
|
118
|
+
return {'type': 'http.disconnect'}
|
|
119
|
+
|
|
120
|
+
async def send(message):
|
|
121
|
+
if message['type'] == 'http.response.start':
|
|
122
|
+
statusHolder.append(message['status'])
|
|
123
|
+
elif message['type'] == 'http.response.body':
|
|
124
|
+
bodyParts.append(message.get('body', b''))
|
|
125
|
+
if not message.get('more_body', False):
|
|
126
|
+
responseFinished.set()
|
|
127
|
+
|
|
128
|
+
await app(scope, receive, send)
|
|
129
|
+
responseFinished.set()
|
|
130
|
+
return (statusHolder or [0])[0], b''.join(bodyParts)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_streaming_through_base_http_middleware_does_not_query_the_connection_concurrently(database):
|
|
135
|
+
# NOTE(krishan711): BaseHTTPMiddleware runs the route in another task, so the stream is still running queries when
|
|
136
|
+
# the response start reaches DatabaseConnectionMiddleware and it commits
|
|
137
|
+
async def stream() -> AsyncIterator[bytes]:
|
|
138
|
+
for index in range(20):
|
|
139
|
+
if database.get_dialect_name() == 'postgresql':
|
|
140
|
+
await database.execute(text('SELECT pg_sleep(0.005)'))
|
|
141
|
+
await database.execute(insert(records).values(name=f'event-{index:02d}'))
|
|
142
|
+
yield b'event\n'
|
|
143
|
+
|
|
144
|
+
async def endpoint(request: Request) -> StreamingResponse:
|
|
145
|
+
await database.execute(insert(records).values(name='request'))
|
|
146
|
+
return StreamingResponse(stream())
|
|
147
|
+
|
|
148
|
+
async def passthrough(request, callNext):
|
|
149
|
+
return await callNext(request)
|
|
150
|
+
|
|
151
|
+
app = Starlette(
|
|
152
|
+
routes=[Route('/', endpoint=endpoint, methods=['POST'])],
|
|
153
|
+
middleware=[
|
|
154
|
+
Middleware(DatabaseConnectionMiddleware, database=database),
|
|
155
|
+
Middleware(BaseHTTPMiddleware, dispatch=passthrough),
|
|
156
|
+
],
|
|
157
|
+
)
|
|
158
|
+
for _ in range(5):
|
|
159
|
+
status, body = await post_to_app(app=app)
|
|
160
|
+
assert status == 200
|
|
161
|
+
assert body == b'event\n' * 20
|
|
162
|
+
assert len(await get_record_names(database=database)) == 21
|
|
163
|
+
async with database.create_transaction() as connection:
|
|
164
|
+
await connection.execute(records.delete())
|
|
165
|
+
|
|
166
|
+
async with database.create_transaction() as connection:
|
|
167
|
+
await connection.execute(insert(records).values(name='check'))
|
|
168
|
+
assert await get_record_names(database=database) == ['check']
|
|
@@ -172,6 +172,49 @@ async def test_upsert_inserts_then_updates(database: Database):
|
|
|
172
172
|
assert upsertedItem.count == 2
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
async def test_upsert_with_false_update_where_keeps_and_returns_the_stored_row(database: Database):
|
|
176
|
+
item = await _create_item(database=database, name='a', count=1)
|
|
177
|
+
async with database.create_transaction() as connection:
|
|
178
|
+
upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda excluded: excluded.count > ItemsTable.c.count, name='a', count=0, amount=5)
|
|
179
|
+
storedItem = await ItemsRepository.get(database=database, connection=connection, idValue=item.itemId)
|
|
180
|
+
assert upsertedItem == item
|
|
181
|
+
assert storedItem == item
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def test_upsert_with_false_update_where_finds_the_stored_row_by_converted_constraint_values(database: Database):
|
|
185
|
+
repository = UpperNameEntityRepository(table=ItemsTable, modelClass=Item)
|
|
186
|
+
async with database.create_transaction() as connection:
|
|
187
|
+
item = await repository.create(database=database, connection=connection, name='a', count=1, amount=1)
|
|
188
|
+
upsertedItem = await repository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda _excluded: sqlalchemy.false(), name='a', count=2, amount=1)
|
|
189
|
+
assert upsertedItem == item
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def test_upsert_with_true_update_where_updates_the_row(database: Database):
|
|
193
|
+
item = await _create_item(database=database, name='a', count=1)
|
|
194
|
+
async with database.create_transaction() as connection:
|
|
195
|
+
upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda excluded: excluded.count > ItemsTable.c.count, name='a', count=2, amount=5)
|
|
196
|
+
assert (upsertedItem.itemId, upsertedItem.createdDate, upsertedItem.count, upsertedItem.amount) == (item.itemId, item.createdDate, 2, 5)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def test_upsert_with_update_where_still_inserts_new_rows(database: Database):
|
|
200
|
+
async with database.create_transaction() as connection:
|
|
201
|
+
upsertedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=lambda _excluded: sqlalchemy.false(), name='a', count=1, amount=1)
|
|
202
|
+
storedItems = await ItemsRepository.list_many(database=database, connection=connection, fieldFilters=[])
|
|
203
|
+
assert storedItems == [upsertedItem]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def test_upsert_only_when_values_changed(database: Database):
|
|
207
|
+
def changed(excluded: typing.Any) -> sqlalchemy.ColumnElement[bool]: # type: ignore[explicit-any]
|
|
208
|
+
return sqlalchemy.or_(ItemsTable.c.count.is_distinct_from(excluded.count), ItemsTable.c.expiryDate.is_distinct_from(excluded.expiryDate))
|
|
209
|
+
|
|
210
|
+
async with database.create_transaction() as connection:
|
|
211
|
+
item = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=None)
|
|
212
|
+
unchangedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=None)
|
|
213
|
+
changedItem = await ItemsRepository.upsert(database=database, connection=connection, constraintColumnNames=['name'], updateWhere=changed, name='a', count=1, amount=1, expiryDate=datetime.datetime(2030, 1, 1, tzinfo=datetime.UTC))
|
|
214
|
+
assert unchangedItem == item
|
|
215
|
+
assert (changedItem.itemId, changedItem.expiryDate) == (item.itemId, datetime.datetime(2030, 1, 1, tzinfo=datetime.UTC))
|
|
216
|
+
|
|
217
|
+
|
|
175
218
|
async def test_upsert_many_inserts_and_updates(database: Database):
|
|
176
219
|
item = await _create_item(database=database, name='a')
|
|
177
220
|
async with database.create_transaction() as connection:
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import pytest_asyncio
|
|
3
|
-
import sqlalchemy
|
|
4
|
-
from sqlalchemy import Column
|
|
5
|
-
from sqlalchemy import MetaData
|
|
6
|
-
from sqlalchemy import Table
|
|
7
|
-
from sqlalchemy import insert
|
|
8
|
-
from sqlalchemy import select
|
|
9
|
-
|
|
10
|
-
from core.api.middleware.database_connection_middleware import DatabaseConnectionMiddleware
|
|
11
|
-
from core.store.database import Database
|
|
12
|
-
|
|
13
|
-
metadata = MetaData()
|
|
14
|
-
records = Table('records', metadata, Column('name', sqlalchemy.Text, primary_key=True))
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest_asyncio.fixture
|
|
18
|
-
async def database(tmp_path):
|
|
19
|
-
database = Database(connectionString=Database.create_sqlite_connection_string(str(tmp_path / 'database.sqlite')))
|
|
20
|
-
await database.connect(poolSize=2)
|
|
21
|
-
async with database.create_transaction() as connection:
|
|
22
|
-
await connection.run_sync(metadata.create_all)
|
|
23
|
-
yield database
|
|
24
|
-
await database.disconnect()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def get_record_names(database: Database) -> list[str]:
|
|
28
|
-
async with database.create_transaction() as connection:
|
|
29
|
-
result = await database.execute(select(records.c.name).order_by(records.c.name), connection=connection)
|
|
30
|
-
return list(result.scalars().all())
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
async def call_app(app, database: Database) -> list[tuple[str, list[str]]]:
|
|
34
|
-
sentMessages: list[tuple[str, list[str]]] = []
|
|
35
|
-
|
|
36
|
-
async def receive():
|
|
37
|
-
return {'type': 'http.request', 'body': b'', 'more_body': False}
|
|
38
|
-
|
|
39
|
-
async def send(message):
|
|
40
|
-
sentMessages.append((message['type'], await get_record_names(database=database)))
|
|
41
|
-
|
|
42
|
-
await DatabaseConnectionMiddleware(app=app, database=database)({'type': 'http', 'method': 'POST', 'path': '/'}, receive, send)
|
|
43
|
-
return sentMessages
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@pytest.mark.asyncio
|
|
47
|
-
async def test_writes_are_committed_before_the_response_starts(database):
|
|
48
|
-
async def app(scope, receive, send):
|
|
49
|
-
await database.execute(insert(records).values(name='request'))
|
|
50
|
-
await send({'type': 'http.response.start', 'status': 200, 'headers': []})
|
|
51
|
-
await send({'type': 'http.response.body', 'body': b'ok'})
|
|
52
|
-
|
|
53
|
-
sentMessages = await call_app(app=app, database=database)
|
|
54
|
-
|
|
55
|
-
assert sentMessages == [('http.response.start', ['request']), ('http.response.body', ['request'])]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@pytest.mark.asyncio
|
|
59
|
-
async def test_writes_while_streaming_are_committed_when_the_request_ends(database):
|
|
60
|
-
async def app(scope, receive, send):
|
|
61
|
-
await database.execute(insert(records).values(name='before'))
|
|
62
|
-
await send({'type': 'http.response.start', 'status': 200, 'headers': []})
|
|
63
|
-
await database.execute(insert(records).values(name='streaming'))
|
|
64
|
-
await send({'type': 'http.response.body', 'body': b'ok'})
|
|
65
|
-
|
|
66
|
-
sentMessages = await call_app(app=app, database=database)
|
|
67
|
-
|
|
68
|
-
assert sentMessages == [('http.response.start', ['before']), ('http.response.body', ['before'])]
|
|
69
|
-
assert await get_record_names(database=database) == ['before', 'streaming']
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@pytest.mark.asyncio
|
|
73
|
-
async def test_writes_are_rolled_back_when_the_request_fails_before_responding(database):
|
|
74
|
-
async def app(scope, receive, send):
|
|
75
|
-
await database.execute(insert(records).values(name='request'))
|
|
76
|
-
raise ValueError('failed')
|
|
77
|
-
|
|
78
|
-
with pytest.raises(ValueError, match='failed'):
|
|
79
|
-
await call_app(app=app, database=database)
|
|
80
|
-
|
|
81
|
-
assert await get_record_names(database=database) == []
|
|
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
|
{kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/core/api/middleware/server_headers_middleware.py
RENAMED
|
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
|
{kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/api/test_exception_handling_middleware.py
RENAMED
|
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
|
{kiba_core-0.5.3.dev89 → kiba_core-0.5.3.dev91}/tests/queues/test_message_queue_processor.py
RENAMED
|
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
|