kiba-core 0.5.3.dev86__tar.gz → 0.5.3.dev87__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.dev86 → kiba_core-0.5.3.dev87}/CHANGELOG.md +1 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/PKG-INFO +2 -1
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/json_route.py +2 -12
- kiba_core-0.5.3.dev87/core/api/request_body.py +31 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/streaming_json_route.py +2 -11
- kiba_core-0.5.3.dev87/core/api/uploaded_file.py +7 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/PKG-INFO +2 -1
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/SOURCES.txt +2 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/requires.txt +1 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/pyproject.toml +2 -1
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_json_route.py +44 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/uv.lock +11 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/publish.yml +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.gitignore +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/README.md +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/database_connection_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/database.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/entity_repository.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/makefile +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_database.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_entity_repository.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_logging.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/web3/test_eth_client_registry.py +0 -0
- {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/web3/test_rest_eth_client.py +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
|
+
- [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`
|
|
11
12
|
- [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)
|
|
12
13
|
- [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
|
|
13
14
|
- [MINOR] Updated `MessageQueueProcessor` to shut down gracefully: `run`/`run_batches` handle SIGTERM and SIGINT (a second signal falls back to the default handler), as does `request_shutdown()`, by finishing the message being processed, releasing claimed messages not yet started and stopping instead of sleeping. A processed message whose task is cancelled is released rather than left until its lease expires
|
|
@@ -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.dev87
|
|
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
|
|
@@ -14,6 +14,7 @@ Requires-Dist: uvicorn[standard]>=0.54.0; extra == "api"
|
|
|
14
14
|
Provides-Extra: core-api
|
|
15
15
|
Requires-Dist: starlette>=1.7.0; extra == "core-api"
|
|
16
16
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "core-api"
|
|
17
|
+
Requires-Dist: python-multipart>=0.0.29; extra == "core-api"
|
|
17
18
|
Provides-Extra: storage
|
|
18
19
|
Requires-Dist: aiobotocore>=3.9.2; extra == "storage"
|
|
19
20
|
Provides-Extra: queues
|
|
@@ -7,10 +7,9 @@ from starlette.requests import Request
|
|
|
7
7
|
|
|
8
8
|
from core.api.api_request import KibaApiRequest
|
|
9
9
|
from core.api.api_response import KibaJSONResponse
|
|
10
|
+
from core.api.request_body import read_request_body
|
|
10
11
|
from core.exceptions import BadRequestException
|
|
11
12
|
from core.exceptions import InternalServerErrorException
|
|
12
|
-
from core.util import json_util
|
|
13
|
-
from core.util.typing_util import JsonObject
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def json_route[ApiRequest: BaseModel, ApiResponse: BaseModel](
|
|
@@ -22,16 +21,7 @@ def json_route[ApiRequest: BaseModel, ApiResponse: BaseModel](
|
|
|
22
21
|
async def async_wrapper(receivedRequest: Request) -> KibaJSONResponse:
|
|
23
22
|
pathParams = receivedRequest.path_params
|
|
24
23
|
queryParams = receivedRequest.query_params
|
|
25
|
-
|
|
26
|
-
if len(bodyBytes) == 0:
|
|
27
|
-
body: JsonObject = {}
|
|
28
|
-
else:
|
|
29
|
-
try:
|
|
30
|
-
body = typing.cast(JsonObject, json_util.loads(bodyBytes.decode()))
|
|
31
|
-
except json_util.JsonDecodeException as exception:
|
|
32
|
-
raise BadRequestException(f'Invalid JSON body: {exception}')
|
|
33
|
-
if not isinstance(body, dict):
|
|
34
|
-
raise BadRequestException('Invalid JSON body: expected an object')
|
|
24
|
+
body = await read_request_body(request=receivedRequest)
|
|
35
25
|
allParams = {**pathParams, **body, **queryParams}
|
|
36
26
|
try:
|
|
37
27
|
requestParams = requestType(**allParams)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
|
|
4
|
+
from starlette.datastructures import UploadFile
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
|
|
7
|
+
from core.api.uploaded_file import UploadedFile
|
|
8
|
+
from core.exceptions import BadRequestException
|
|
9
|
+
from core.util import json_util
|
|
10
|
+
from core.util.typing_util import JsonObject
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def read_request_body(request: Request) -> Mapping[str, object]:
|
|
14
|
+
if request.headers.get('Content-Type', '').startswith('multipart/form-data'):
|
|
15
|
+
form = await request.form()
|
|
16
|
+
body: dict[str, object] = {}
|
|
17
|
+
for key, value in form.multi_items():
|
|
18
|
+
if key in body:
|
|
19
|
+
raise BadRequestException(f'Invalid form body: {key} is given more than once')
|
|
20
|
+
body[key] = UploadedFile(filename=value.filename, contentType=value.content_type, content=await value.read()) if isinstance(value, UploadFile) else value
|
|
21
|
+
return body
|
|
22
|
+
bodyBytes = await request.body()
|
|
23
|
+
if len(bodyBytes) == 0:
|
|
24
|
+
return {}
|
|
25
|
+
try:
|
|
26
|
+
jsonBody = typing.cast(JsonObject, json_util.loads(bodyBytes.decode()))
|
|
27
|
+
except json_util.JsonDecodeException as exception:
|
|
28
|
+
raise BadRequestException(f'Invalid JSON body: {exception}')
|
|
29
|
+
if not isinstance(jsonBody, dict):
|
|
30
|
+
raise BadRequestException('Invalid JSON body: expected an object')
|
|
31
|
+
return jsonBody
|
|
@@ -9,10 +9,10 @@ from starlette.requests import Request
|
|
|
9
9
|
from starlette.responses import StreamingResponse
|
|
10
10
|
|
|
11
11
|
from core.api.api_request import KibaApiRequest
|
|
12
|
+
from core.api.request_body import read_request_body
|
|
12
13
|
from core.exceptions import BadRequestException
|
|
13
14
|
from core.exceptions import InternalServerErrorException
|
|
14
15
|
from core.util import json_util
|
|
15
|
-
from core.util.typing_util import JsonObject
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
async def _convert_to_json_generator[T: BaseModel](response_iterator: AsyncIterator[T], expectedType: typing.Type[T]) -> AsyncIterator[bytes]:
|
|
@@ -31,16 +31,7 @@ def streaming_json_route[ApiRequest: BaseModel, ApiResponse: BaseModel](
|
|
|
31
31
|
async def async_wrapper(receivedRequest: Request) -> StreamingResponse:
|
|
32
32
|
pathParams = receivedRequest.path_params
|
|
33
33
|
queryParams = receivedRequest.query_params
|
|
34
|
-
|
|
35
|
-
if len(bodyBytes) == 0:
|
|
36
|
-
body: JsonObject = {}
|
|
37
|
-
else:
|
|
38
|
-
try:
|
|
39
|
-
body = typing.cast(JsonObject, json_util.loads(bodyBytes.decode()))
|
|
40
|
-
except json_util.JsonDecodeException as exception:
|
|
41
|
-
raise BadRequestException(f'Invalid JSON body: {exception}')
|
|
42
|
-
if not isinstance(body, dict):
|
|
43
|
-
raise BadRequestException('Invalid JSON body: expected an object')
|
|
34
|
+
body = await read_request_body(request=receivedRequest)
|
|
44
35
|
allParams = {**pathParams, **body, **queryParams}
|
|
45
36
|
try:
|
|
46
37
|
requestParams = requestType(**allParams)
|
|
@@ -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.dev87
|
|
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
|
|
@@ -14,6 +14,7 @@ Requires-Dist: uvicorn[standard]>=0.54.0; extra == "api"
|
|
|
14
14
|
Provides-Extra: core-api
|
|
15
15
|
Requires-Dist: starlette>=1.7.0; extra == "core-api"
|
|
16
16
|
Requires-Dist: uvicorn[standard]>=0.54.0; extra == "core-api"
|
|
17
|
+
Requires-Dist: python-multipart>=0.0.29; extra == "core-api"
|
|
17
18
|
Provides-Extra: storage
|
|
18
19
|
Requires-Dist: aiobotocore>=3.9.2; extra == "storage"
|
|
19
20
|
Provides-Extra: queues
|
|
@@ -26,11 +26,13 @@ core/api/health.py
|
|
|
26
26
|
core/api/json_route.py
|
|
27
27
|
core/api/openapi.py
|
|
28
28
|
core/api/rate_limit.py
|
|
29
|
+
core/api/request_body.py
|
|
29
30
|
core/api/request_context.py
|
|
30
31
|
core/api/route.py
|
|
31
32
|
core/api/route_auth.py
|
|
32
33
|
core/api/route_metadata.py
|
|
33
34
|
core/api/streaming_json_route.py
|
|
35
|
+
core/api/uploaded_file.py
|
|
34
36
|
core/api/middleware/__init__.py
|
|
35
37
|
core/api/middleware/database_connection_middleware.py
|
|
36
38
|
core/api/middleware/exception_handling_middleware.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kiba-core"
|
|
3
|
-
version = "0.5.3.
|
|
3
|
+
version = "0.5.3.dev87"
|
|
4
4
|
description = "Kiba Labs' python utilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12, <4"
|
|
@@ -18,6 +18,7 @@ api = [
|
|
|
18
18
|
core-api = [
|
|
19
19
|
"starlette>=1.7.0",
|
|
20
20
|
"uvicorn[standard]>=0.54.0",
|
|
21
|
+
"python-multipart>=0.0.29",
|
|
21
22
|
]
|
|
22
23
|
storage = [
|
|
23
24
|
"aiobotocore>=3.9.2",
|
|
@@ -6,6 +6,7 @@ from pydantic import BaseModel
|
|
|
6
6
|
|
|
7
7
|
from core.api.json_route import json_route
|
|
8
8
|
from core.api.api_request import KibaApiRequest
|
|
9
|
+
from core.api.uploaded_file import UploadedFile
|
|
9
10
|
from core.api.middleware.exception_handling_middleware import ExceptionHandlingMiddleware
|
|
10
11
|
from core.api.request_context import RequestContext
|
|
11
12
|
from core.api.request_context import RequestContextHolder
|
|
@@ -30,6 +31,18 @@ class ExampleResponse(BaseModel):
|
|
|
30
31
|
path_echo: str | None = None
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
class UploadRequest(BaseModel):
|
|
35
|
+
file: UploadedFile
|
|
36
|
+
age: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UploadResponse(BaseModel):
|
|
40
|
+
filename: str | None
|
|
41
|
+
content_type: str | None
|
|
42
|
+
content: str
|
|
43
|
+
age: int
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
@pytest.fixture
|
|
34
47
|
def client():
|
|
35
48
|
@json_route(requestType=ExampleRequest, responseType=ExampleResponse)
|
|
@@ -52,9 +65,20 @@ def client():
|
|
|
52
65
|
)
|
|
53
66
|
|
|
54
67
|
|
|
68
|
+
@json_route(requestType=UploadRequest, responseType=UploadResponse)
|
|
69
|
+
async def test_upload_endpoint(request: KibaApiRequest[UploadRequest]) -> UploadResponse:
|
|
70
|
+
return UploadResponse(
|
|
71
|
+
filename=request.data.file.filename,
|
|
72
|
+
content_type=request.data.file.contentType,
|
|
73
|
+
content=request.data.file.content.decode(),
|
|
74
|
+
age=request.data.age,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
55
78
|
app = Starlette(routes=[
|
|
56
79
|
Route("/test", test_endpoint, methods=["POST"]),
|
|
57
80
|
Route("/test/{path_param}", test_path_endpoint, methods=["POST"]),
|
|
81
|
+
Route("/upload", test_upload_endpoint, methods=["POST"]),
|
|
58
82
|
])
|
|
59
83
|
app.add_middleware(ExceptionHandlingMiddleware)
|
|
60
84
|
app.add_middleware(RequestContextMiddleware, requestContextHolder=requestContextHolder, requestContextFactory=create_request_context)
|
|
@@ -145,3 +169,23 @@ def test_json_route_with_non_object_json(client, content):
|
|
|
145
169
|
)
|
|
146
170
|
assert response.status_code == 400
|
|
147
171
|
assert "expected an object" in response.json().get("message", "")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_json_route_with_multipart_body_passes_files_and_fields(client):
|
|
175
|
+
response = client.post(
|
|
176
|
+
"/upload",
|
|
177
|
+
files={"file": ("scan.dcm", b"file-content", "application/dicom")},
|
|
178
|
+
data={"age": "41"},
|
|
179
|
+
)
|
|
180
|
+
assert response.status_code == 200
|
|
181
|
+
assert response.json() == {"filename": "scan.dcm", "content_type": "application/dicom", "content": "file-content", "age": 41}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_json_route_with_multipart_body_rejects_repeated_fields(client):
|
|
185
|
+
response = client.post(
|
|
186
|
+
"/upload",
|
|
187
|
+
files=[("file", ("first.dcm", b"first", "application/dicom")), ("file", ("second.dcm", b"second", "application/dicom"))],
|
|
188
|
+
data={"age": "41"},
|
|
189
|
+
)
|
|
190
|
+
assert response.status_code == 400
|
|
191
|
+
assert "file is given more than once" in response.json().get("message", "")
|
|
@@ -1697,6 +1697,7 @@ api = [
|
|
|
1697
1697
|
{ name = "uvicorn", extra = ["standard"] },
|
|
1698
1698
|
]
|
|
1699
1699
|
core-api = [
|
|
1700
|
+
{ name = "python-multipart" },
|
|
1700
1701
|
{ name = "starlette" },
|
|
1701
1702
|
{ name = "uvicorn", extra = ["standard"] },
|
|
1702
1703
|
]
|
|
@@ -1757,6 +1758,7 @@ requires-dist = [
|
|
|
1757
1758
|
{ name = "httpx2", marker = "extra == 'requester'", specifier = ">=2.13.1" },
|
|
1758
1759
|
{ name = "orjson", specifier = ">=3.13.0" },
|
|
1759
1760
|
{ name = "pydantic", specifier = ">=2.14.1" },
|
|
1761
|
+
{ name = "python-multipart", marker = "extra == 'core-api'", specifier = ">=0.0.29" },
|
|
1760
1762
|
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database-psql'", specifier = ">=2.1.4" },
|
|
1761
1763
|
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database-sqlite'", specifier = ">=2.1.4" },
|
|
1762
1764
|
{ name = "starlette", marker = "extra == 'core-api'", specifier = ">=1.7.0" },
|
|
@@ -2635,6 +2637,15 @@ wheels = [
|
|
|
2635
2637
|
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
|
|
2636
2638
|
]
|
|
2637
2639
|
|
|
2640
|
+
[[package]]
|
|
2641
|
+
name = "python-multipart"
|
|
2642
|
+
version = "0.0.32"
|
|
2643
|
+
source = { registry = "https://pypi.org/simple" }
|
|
2644
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
|
|
2645
|
+
wheels = [
|
|
2646
|
+
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
|
|
2647
|
+
]
|
|
2648
|
+
|
|
2638
2649
|
[[package]]
|
|
2639
2650
|
name = "pyunormalize"
|
|
2640
2651
|
version = "18.0.0"
|
|
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.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/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
|
{kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/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
|
{kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/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
|