kiba-core 0.5.3.dev87__tar.gz → 0.5.3.dev89__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.dev87 → kiba_core-0.5.3.dev89}/CHANGELOG.md +3 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/PKG-INFO +7 -7
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/database_connection_middleware.py +11 -1
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/request_body.py +6 -5
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/store/database.py +11 -2
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/kiba_core.egg-info/PKG-INFO +7 -7
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/kiba_core.egg-info/SOURCES.txt +1 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/kiba_core.egg-info/requires.txt +6 -6
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/pyproject.toml +8 -8
- kiba_core-0.5.3.dev89/tests/api/test_database_connection_middleware.py +81 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_json_route.py +19 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/test_database.py +55 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/uv.lock +374 -365
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.github/workflows/publish.yml +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/.gitignore +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/README.md +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/api/uploaded_file.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/store/entity_repository.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/web3/eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/makefile +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/test_message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/test_entity_repository.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/test_logging.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/web3/test_eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev87 → kiba_core-0.5.3.dev89}/tests/web3/test_rest_eth_client.py +0 -0
|
@@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- [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
|
+
- [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
|
+
- [PATCH] Fixed `json_route` and `streaming_json_route` leaving the temporary files of multipart uploads open
|
|
11
14
|
- [MINOR] Updated `json_route` and `streaming_json_route` (and so `route`) to accept `multipart/form-data` bodies: form fields are used like JSON body fields and uploaded files are passed as `core.api.uploaded_file.UploadedFile` (`filename`, `contentType`, `content`), so request models can declare `file: UploadedFile`. A field given more than once raises `BadRequestException`. The `core-api` extra now installs `python-multipart`
|
|
12
15
|
- [MAJOR] Added abstract `MessageQueue.release_message` to hand a claimed message back to be claimed again straight away without using an attempt (`SqlMessageQueue` and `CosmosMessageQueue` keep `postCount`; `SqsMessageQueue` and `AqsMessageQueue` make the message visible again, though SQS still counts the next receive towards its redrive policy)
|
|
13
16
|
- [MAJOR] Added abstract `MessageQueue.delete_expired_messages`: `SqlMessageQueue` deletes `succeeded` and `deduplicated` messages older than `retentionSeconds` (the other queues remove messages on completion and return 0). `SqlMessageQueue.get_message_count` no longer deletes them and its `shouldSkipRemovingNonRetained` parameter is removed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiba-core
|
|
3
|
-
Version: 0.5.3.
|
|
3
|
+
Version: 0.5.3.dev89
|
|
4
4
|
Summary: Kiba Labs' python utilities
|
|
5
5
|
Project-URL: repository, https://github.com/kibalabs/core-py
|
|
6
6
|
Requires-Python: <4,>=3.12
|
|
@@ -9,12 +9,12 @@ Requires-Dist: aiofiles>=25.1.0
|
|
|
9
9
|
Requires-Dist: pydantic>=2.14.1
|
|
10
10
|
Requires-Dist: orjson>=3.13.0
|
|
11
11
|
Provides-Extra: api
|
|
12
|
-
Requires-Dist: fastapi>=0.
|
|
12
|
+
Requires-Dist: fastapi>=0.146.0; extra == "api"
|
|
13
13
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "api"
|
|
14
14
|
Provides-Extra: core-api
|
|
15
|
-
Requires-Dist: starlette>=1.
|
|
15
|
+
Requires-Dist: starlette>=1.9.0; extra == "core-api"
|
|
16
16
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "core-api"
|
|
17
|
-
Requires-Dist: python-multipart>=0.0.
|
|
17
|
+
Requires-Dist: python-multipart>=0.0.32; extra == "core-api"
|
|
18
18
|
Provides-Extra: storage
|
|
19
19
|
Requires-Dist: aiobotocore>=3.9.2; extra == "storage"
|
|
20
20
|
Provides-Extra: queues
|
|
@@ -26,13 +26,13 @@ Requires-Dist: azure-storage-queue[aio]>=12.18.0; extra == "queue-aqs"
|
|
|
26
26
|
Provides-Extra: queue-cosmos
|
|
27
27
|
Requires-Dist: azure-cosmos>=4.17.1; extra == "queue-cosmos"
|
|
28
28
|
Provides-Extra: database-psql
|
|
29
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.1.
|
|
29
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.1.5; extra == "database-psql"
|
|
30
30
|
Requires-Dist: asyncpg>=0.32.0; extra == "database-psql"
|
|
31
31
|
Provides-Extra: database-sqlite
|
|
32
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.1.
|
|
32
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.1.5; extra == "database-sqlite"
|
|
33
33
|
Requires-Dist: aiosqlite>=0.22.1; extra == "database-sqlite"
|
|
34
34
|
Provides-Extra: requester
|
|
35
|
-
Requires-Dist: httpx2>=2.
|
|
35
|
+
Requires-Dist: httpx2>=2.14.0; extra == "requester"
|
|
36
36
|
Provides-Extra: web3
|
|
37
37
|
Requires-Dist: web3<8.0.0,>=7.16.0; extra == "web3"
|
|
38
38
|
Provides-Extra: cron
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from starlette.types import ASGIApp
|
|
2
|
+
from starlette.types import Message
|
|
2
3
|
from starlette.types import Receive
|
|
3
4
|
from starlette.types import Scope
|
|
4
5
|
from starlette.types import Send
|
|
@@ -18,4 +19,13 @@ class DatabaseConnectionMiddleware:
|
|
|
18
19
|
await self.app(scope, receive, send)
|
|
19
20
|
return
|
|
20
21
|
async with self.database.create_context_connection():
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
# NOTE(krishan711): commit before the response starts, otherwise a client can get the response and make its
|
|
24
|
+
# next request before this request's writes are visible. Anything after this (e.g. while streaming) is
|
|
25
|
+
# committed when the context connection ends
|
|
26
|
+
async def send_after_commit(message: Message) -> None:
|
|
27
|
+
if message['type'] == 'http.response.start':
|
|
28
|
+
await self.database.commit_context_connection()
|
|
29
|
+
await send(message)
|
|
30
|
+
|
|
31
|
+
await self.app(scope, receive, send_after_commit)
|
|
@@ -12,12 +12,13 @@ from core.util.typing_util import JsonObject
|
|
|
12
12
|
|
|
13
13
|
async def read_request_body(request: Request) -> Mapping[str, object]:
|
|
14
14
|
if request.headers.get('Content-Type', '').startswith('multipart/form-data'):
|
|
15
|
-
form = await request.form()
|
|
16
15
|
body: dict[str, object] = {}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
# NOTE(krishan711): the form is closed on exit so uploaded files' temporary files are closed (their content has been read by then)
|
|
17
|
+
async with request.form() as form:
|
|
18
|
+
for key, value in form.multi_items():
|
|
19
|
+
if key in body:
|
|
20
|
+
raise BadRequestException(f'Invalid form body: {key} is given more than once')
|
|
21
|
+
body[key] = UploadedFile(filename=value.filename, contentType=value.content_type, content=await value.read()) if isinstance(value, UploadFile) else value
|
|
21
22
|
return body
|
|
22
23
|
bodyBytes = await request.body()
|
|
23
24
|
if len(bodyBytes) == 0:
|
|
@@ -107,12 +107,12 @@ class Database:
|
|
|
107
107
|
raise InternalServerErrorException(message='Engine has not been established. Please called collect() first.')
|
|
108
108
|
if self._get_context_connection() is not None:
|
|
109
109
|
raise InternalServerErrorException(message='Connection has already been established in this context.')
|
|
110
|
-
connection = None
|
|
111
110
|
try:
|
|
112
|
-
async with self._engine.
|
|
111
|
+
async with self._engine.connect() as connection:
|
|
113
112
|
self._connectionContext.set(connection)
|
|
114
113
|
try:
|
|
115
114
|
yield connection
|
|
115
|
+
await self.commit_context_connection()
|
|
116
116
|
finally:
|
|
117
117
|
self._connectionContext.set(None)
|
|
118
118
|
except sqlalchemy.exc.InterfaceError as exception:
|
|
@@ -122,6 +122,15 @@ class Database:
|
|
|
122
122
|
await self.disconnect()
|
|
123
123
|
await self.connect()
|
|
124
124
|
|
|
125
|
+
# NOTE(krishan711): commits what the context connection has done so far. Anything run on it afterwards is in a new
|
|
126
|
+
# transaction, which is committed when the context connection ends (or rolled back if it ends with an exception).
|
|
127
|
+
async def commit_context_connection(self) -> None:
|
|
128
|
+
connection = self._get_context_connection()
|
|
129
|
+
if not connection:
|
|
130
|
+
raise InternalServerErrorException(message='No connection found. Please call create_context_connection() for the context.')
|
|
131
|
+
await connection.commit()
|
|
132
|
+
self._connectionsWithWrites.discard(connection)
|
|
133
|
+
|
|
125
134
|
# NOTE(krishan711): unlike create_context_connection this may be used inside an existing context connection.
|
|
126
135
|
# Everything in the block runs on a new transaction that commits when the block exits, independent of the
|
|
127
136
|
# outer transaction, which becomes the context connection again afterwards. It cannot see the outer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiba-core
|
|
3
|
-
Version: 0.5.3.
|
|
3
|
+
Version: 0.5.3.dev89
|
|
4
4
|
Summary: Kiba Labs' python utilities
|
|
5
5
|
Project-URL: repository, https://github.com/kibalabs/core-py
|
|
6
6
|
Requires-Python: <4,>=3.12
|
|
@@ -9,12 +9,12 @@ Requires-Dist: aiofiles>=25.1.0
|
|
|
9
9
|
Requires-Dist: pydantic>=2.14.1
|
|
10
10
|
Requires-Dist: orjson>=3.13.0
|
|
11
11
|
Provides-Extra: api
|
|
12
|
-
Requires-Dist: fastapi>=0.
|
|
12
|
+
Requires-Dist: fastapi>=0.146.0; extra == "api"
|
|
13
13
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "api"
|
|
14
14
|
Provides-Extra: core-api
|
|
15
|
-
Requires-Dist: starlette>=1.
|
|
15
|
+
Requires-Dist: starlette>=1.9.0; extra == "core-api"
|
|
16
16
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "core-api"
|
|
17
|
-
Requires-Dist: python-multipart>=0.0.
|
|
17
|
+
Requires-Dist: python-multipart>=0.0.32; extra == "core-api"
|
|
18
18
|
Provides-Extra: storage
|
|
19
19
|
Requires-Dist: aiobotocore>=3.9.2; extra == "storage"
|
|
20
20
|
Provides-Extra: queues
|
|
@@ -26,13 +26,13 @@ Requires-Dist: azure-storage-queue[aio]>=12.18.0; extra == "queue-aqs"
|
|
|
26
26
|
Provides-Extra: queue-cosmos
|
|
27
27
|
Requires-Dist: azure-cosmos>=4.17.1; extra == "queue-cosmos"
|
|
28
28
|
Provides-Extra: database-psql
|
|
29
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.1.
|
|
29
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.1.5; extra == "database-psql"
|
|
30
30
|
Requires-Dist: asyncpg>=0.32.0; extra == "database-psql"
|
|
31
31
|
Provides-Extra: database-sqlite
|
|
32
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.1.
|
|
32
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.1.5; extra == "database-sqlite"
|
|
33
33
|
Requires-Dist: aiosqlite>=0.22.1; extra == "database-sqlite"
|
|
34
34
|
Provides-Extra: requester
|
|
35
|
-
Requires-Dist: httpx2>=2.
|
|
35
|
+
Requires-Dist: httpx2>=2.14.0; extra == "requester"
|
|
36
36
|
Provides-Extra: web3
|
|
37
37
|
Requires-Dist: web3<8.0.0,>=7.16.0; extra == "web3"
|
|
38
38
|
Provides-Extra: cron
|
|
@@ -106,6 +106,7 @@ tests/test_exceptions.py
|
|
|
106
106
|
tests/test_logging.py
|
|
107
107
|
tests/api/test_api_response.py
|
|
108
108
|
tests/api/test_authorizer.py
|
|
109
|
+
tests/api/test_database_connection_middleware.py
|
|
109
110
|
tests/api/test_exception_handling_middleware.py
|
|
110
111
|
tests/api/test_json_route.py
|
|
111
112
|
tests/api/test_openapi.py
|
|
@@ -3,23 +3,23 @@ pydantic>=2.14.1
|
|
|
3
3
|
orjson>=3.13.0
|
|
4
4
|
|
|
5
5
|
[api]
|
|
6
|
-
fastapi>=0.
|
|
6
|
+
fastapi>=0.146.0
|
|
7
7
|
uvicorn[standard]>=0.54.0
|
|
8
8
|
|
|
9
9
|
[core-api]
|
|
10
|
-
starlette>=1.
|
|
10
|
+
starlette>=1.9.0
|
|
11
11
|
uvicorn[standard]>=0.54.0
|
|
12
|
-
python-multipart>=0.0.
|
|
12
|
+
python-multipart>=0.0.32
|
|
13
13
|
|
|
14
14
|
[cron]
|
|
15
15
|
apscheduler<4,>=3.11.3
|
|
16
16
|
|
|
17
17
|
[database-psql]
|
|
18
|
-
sqlalchemy[asyncio]>=2.1.
|
|
18
|
+
sqlalchemy[asyncio]>=2.1.5
|
|
19
19
|
asyncpg>=0.32.0
|
|
20
20
|
|
|
21
21
|
[database-sqlite]
|
|
22
|
-
sqlalchemy[asyncio]>=2.1.
|
|
22
|
+
sqlalchemy[asyncio]>=2.1.5
|
|
23
23
|
aiosqlite>=0.22.1
|
|
24
24
|
|
|
25
25
|
[queue-aqs]
|
|
@@ -35,7 +35,7 @@ aiobotocore>=3.9.2
|
|
|
35
35
|
aiobotocore>=3.9.2
|
|
36
36
|
|
|
37
37
|
[requester]
|
|
38
|
-
httpx2>=2.
|
|
38
|
+
httpx2>=2.14.0
|
|
39
39
|
|
|
40
40
|
[storage]
|
|
41
41
|
aiobotocore>=3.9.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kiba-core"
|
|
3
|
-
version = "0.5.3.
|
|
3
|
+
version = "0.5.3.dev89"
|
|
4
4
|
description = "Kiba Labs' python utilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12, <4"
|
|
@@ -12,13 +12,13 @@ dependencies = [
|
|
|
12
12
|
|
|
13
13
|
[project.optional-dependencies]
|
|
14
14
|
api = [
|
|
15
|
-
"fastapi>=0.
|
|
15
|
+
"fastapi>=0.146.0",
|
|
16
16
|
"uvicorn[standard]>=0.54.0",
|
|
17
17
|
]
|
|
18
18
|
core-api = [
|
|
19
|
-
"starlette>=1.
|
|
19
|
+
"starlette>=1.9.0",
|
|
20
20
|
"uvicorn[standard]>=0.54.0",
|
|
21
|
-
"python-multipart>=0.0.
|
|
21
|
+
"python-multipart>=0.0.32",
|
|
22
22
|
]
|
|
23
23
|
storage = [
|
|
24
24
|
"aiobotocore>=3.9.2",
|
|
@@ -37,15 +37,15 @@ queue-cosmos = [
|
|
|
37
37
|
"azure-cosmos>=4.17.1",
|
|
38
38
|
]
|
|
39
39
|
database-psql = [
|
|
40
|
-
"sqlalchemy[asyncio]>=2.1.
|
|
40
|
+
"sqlalchemy[asyncio]>=2.1.5",
|
|
41
41
|
"asyncpg>=0.32.0",
|
|
42
42
|
]
|
|
43
43
|
database-sqlite = [
|
|
44
|
-
"sqlalchemy[asyncio]>=2.1.
|
|
44
|
+
"sqlalchemy[asyncio]>=2.1.5",
|
|
45
45
|
"aiosqlite>=0.22.1",
|
|
46
46
|
]
|
|
47
47
|
requester = [
|
|
48
|
-
"httpx2>=2.
|
|
48
|
+
"httpx2>=2.14.0",
|
|
49
49
|
]
|
|
50
50
|
web3 = [
|
|
51
51
|
"web3>=7.16.0,<8.0.0",
|
|
@@ -60,7 +60,7 @@ types = [
|
|
|
60
60
|
|
|
61
61
|
[dependency-groups]
|
|
62
62
|
dev = [
|
|
63
|
-
"kiba-build==0.1.11.
|
|
63
|
+
"kiba-build==0.1.11.dev20",
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
[project.urls]
|
|
@@ -0,0 +1,81 @@
|
|
|
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) == []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from starlette.applications import Starlette
|
|
3
3
|
from starlette.routing import Route
|
|
4
|
+
from starlette.datastructures import UploadFile
|
|
4
5
|
from starlette.testclient import TestClient
|
|
5
6
|
from pydantic import BaseModel
|
|
6
7
|
|
|
@@ -189,3 +190,21 @@ def test_json_route_with_multipart_body_rejects_repeated_fields(client):
|
|
|
189
190
|
)
|
|
190
191
|
assert response.status_code == 400
|
|
191
192
|
assert "file is given more than once" in response.json().get("message", "")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_json_route_with_multipart_body_closes_uploaded_files(client, monkeypatch):
|
|
196
|
+
closedFilenames = []
|
|
197
|
+
originalClose = UploadFile.close
|
|
198
|
+
|
|
199
|
+
async def close(self):
|
|
200
|
+
closedFilenames.append(self.filename)
|
|
201
|
+
await originalClose(self)
|
|
202
|
+
|
|
203
|
+
monkeypatch.setattr(UploadFile, "close", close)
|
|
204
|
+
response = client.post(
|
|
205
|
+
"/upload",
|
|
206
|
+
files={"file": ("scan.dcm", b"file-content", "application/dicom")},
|
|
207
|
+
data={"age": "41"},
|
|
208
|
+
)
|
|
209
|
+
assert response.status_code == 200
|
|
210
|
+
assert closedFilenames == ["scan.dcm"]
|
|
@@ -229,3 +229,58 @@ async def test_isolated_context_connection_raises_on_uncommitted_outer_writes_wh
|
|
|
229
229
|
async with database.create_isolated_context_connection():
|
|
230
230
|
pass
|
|
231
231
|
await database.disconnect()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.asyncio
|
|
235
|
+
async def test_commit_context_connection_makes_writes_visible_before_the_context_ends(database):
|
|
236
|
+
metadata = MetaData()
|
|
237
|
+
records = Table('records', metadata, Column('name', sqlalchemy.Text, primary_key=True))
|
|
238
|
+
await database.connect(poolSize=2)
|
|
239
|
+
async with database.create_transaction() as connection:
|
|
240
|
+
await connection.run_sync(metadata.create_all)
|
|
241
|
+
|
|
242
|
+
with pytest.raises(ValueError, match='rollback'):
|
|
243
|
+
async with database.create_context_connection():
|
|
244
|
+
await database.execute(insert(records).values(name='committed'))
|
|
245
|
+
await database.commit_context_connection()
|
|
246
|
+
async with database.create_transaction() as connection:
|
|
247
|
+
result = await database.execute(select(records.c.name), connection=connection)
|
|
248
|
+
assert result.scalars().all() == ['committed']
|
|
249
|
+
await database.execute(insert(records).values(name='rolledback'))
|
|
250
|
+
raise ValueError('rollback')
|
|
251
|
+
|
|
252
|
+
async with database.create_transaction() as connection:
|
|
253
|
+
result = await database.execute(select(records.c.name), connection=connection)
|
|
254
|
+
assert result.scalars().all() == ['committed']
|
|
255
|
+
await database.disconnect()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_writes_after_commit_context_connection_are_committed_when_the_context_ends(database, caplog):
|
|
260
|
+
metadata = MetaData()
|
|
261
|
+
records = Table('records', metadata, Column('name', sqlalchemy.Text, primary_key=True))
|
|
262
|
+
await database.connect(poolSize=2)
|
|
263
|
+
async with database.create_transaction() as connection:
|
|
264
|
+
await connection.run_sync(metadata.create_all)
|
|
265
|
+
|
|
266
|
+
with caplog.at_level(logging.ERROR):
|
|
267
|
+
async with database.create_context_connection():
|
|
268
|
+
await database.execute(insert(records).values(name='first'))
|
|
269
|
+
await database.commit_context_connection()
|
|
270
|
+
async with database.create_isolated_context_connection():
|
|
271
|
+
pass
|
|
272
|
+
await database.execute(insert(records).values(name='second'))
|
|
273
|
+
assert 'ISOLATED_CONNECTION_AFTER_UNCOMMITTED_WRITES' not in caplog.text
|
|
274
|
+
|
|
275
|
+
async with database.create_transaction() as connection:
|
|
276
|
+
result = await database.execute(select(records.c.name).order_by(records.c.name), connection=connection)
|
|
277
|
+
assert result.scalars().all() == ['first', 'second']
|
|
278
|
+
await database.disconnect()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@pytest.mark.asyncio
|
|
282
|
+
async def test_commit_context_connection_requires_a_context_connection(database):
|
|
283
|
+
await database.connect(poolSize=1)
|
|
284
|
+
with pytest.raises(InternalServerErrorException, match='No connection found'):
|
|
285
|
+
await database.commit_context_connection()
|
|
286
|
+
await database.disconnect()
|