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.
Files changed (145) hide show
  1. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/CHANGELOG.md +1 -0
  2. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/PKG-INFO +2 -1
  3. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/json_route.py +2 -12
  4. kiba_core-0.5.3.dev87/core/api/request_body.py +31 -0
  5. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/streaming_json_route.py +2 -11
  6. kiba_core-0.5.3.dev87/core/api/uploaded_file.py +7 -0
  7. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/PKG-INFO +2 -1
  8. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/SOURCES.txt +2 -0
  9. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/requires.txt +1 -0
  10. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/pyproject.toml +2 -1
  11. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_json_route.py +44 -0
  12. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/uv.lock +11 -0
  13. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/pull_request_template.md +0 -0
  14. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/deploy.yml +0 -0
  15. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/publish.yml +0 -0
  16. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/pull-request.yml +0 -0
  17. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.github/workflows/release.yml +0 -0
  18. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/.gitignore +0 -0
  19. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/Dockerfile +0 -0
  20. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/README.md +0 -0
  21. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/__init__.py +0 -0
  22. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/__init__.py +0 -0
  23. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/api_request.py +0 -0
  24. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/api_response.py +0 -0
  25. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/authorizer.py +0 -0
  26. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/default_routes.py +0 -0
  27. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/health.py +0 -0
  28. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/__init__.py +0 -0
  29. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/database_connection_middleware.py +0 -0
  30. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/exception_handling_middleware.py +0 -0
  31. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/logging_middleware.py +0 -0
  32. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/origin_ip_middleware.py +0 -0
  33. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/middleware/server_headers_middleware.py +0 -0
  34. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/openapi.py +0 -0
  35. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/rate_limit.py +0 -0
  36. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/request_context.py +0 -0
  37. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route.py +0 -0
  38. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route_auth.py +0 -0
  39. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/api/route_metadata.py +0 -0
  40. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/aws_requester.py +0 -0
  41. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/__init__.py +0 -0
  42. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/cache.py +0 -0
  43. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/dict_cache.py +0 -0
  44. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/caching/file_cache.py +0 -0
  45. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/__init__.py +0 -0
  46. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/api_request_cron_job_processor.py +0 -0
  47. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/cron_executor.py +0 -0
  48. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/cron_job_processor.py +0 -0
  49. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/cron/model.py +0 -0
  50. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/exceptions.py +0 -0
  51. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/__init__.py +0 -0
  52. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/basic_authentication.py +0 -0
  53. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/jwt.py +0 -0
  54. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/http/rest_method.py +0 -0
  55. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/__init__.py +0 -0
  56. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/lock.py +0 -0
  57. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/model.py +0 -0
  58. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/locks/sql.py +0 -0
  59. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/logging.py +0 -0
  60. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/__init__.py +0 -0
  61. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/discord_client.py +0 -0
  62. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/notification_client.py +0 -0
  63. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/notifications/slack_client.py +0 -0
  64. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/py.typed +0 -0
  65. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/__init__.py +0 -0
  66. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/aqs.py +0 -0
  67. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/cosmos.py +0 -0
  68. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/message_queue.py +0 -0
  69. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/message_queue_processor.py +0 -0
  70. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/model.py +0 -0
  71. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/sql.py +0 -0
  72. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/queues/sqs.py +0 -0
  73. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/requester/__init__.py +0 -0
  74. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/requester/requester.py +0 -0
  75. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/s3_manager.py +0 -0
  76. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/service_client.py +0 -0
  77. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/__init__.py +0 -0
  78. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/database.py +0 -0
  79. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/entity_repository.py +0 -0
  80. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/retriever.py +0 -0
  81. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/store/saver.py +0 -0
  82. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/__init__.py +0 -0
  83. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/async_util.py +0 -0
  84. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/chain_util.py +0 -0
  85. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/date_util.py +0 -0
  86. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/dict_util.py +0 -0
  87. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/file_util.py +0 -0
  88. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/hashing_util.py +0 -0
  89. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/http_util.py +0 -0
  90. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/json_util.py +0 -0
  91. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/list_util.py +0 -0
  92. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/process_util.py +0 -0
  93. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/string_util.py +0 -0
  94. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/typing_util.py +0 -0
  95. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/url_util.py +0 -0
  96. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/util/value_holder.py +0 -0
  97. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/__init__.py +0 -0
  98. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/eth_client.py +0 -0
  99. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/eth_client_registry.py +0 -0
  100. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/core/web3/multicall3.py +0 -0
  101. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/dependency_links.txt +0 -0
  102. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/kiba_core.egg-info/top_level.txt +0 -0
  103. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/makefile +0 -0
  104. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/setup.cfg +0 -0
  105. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/__init__.py +0 -0
  106. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_api_response.py +0 -0
  107. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_authorizer.py +0 -0
  108. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_exception_handling_middleware.py +0 -0
  109. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_openapi.py +0 -0
  110. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_origin_ip_middleware.py +0 -0
  111. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_rate_limit.py +0 -0
  112. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_request_context.py +0 -0
  113. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_route.py +0 -0
  114. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/api/test_streaming_json_route.py +0 -0
  115. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/caching/test_dict_cache.py +0 -0
  116. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/caching/test_file_cache.py +0 -0
  117. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/cron/__init__.py +0 -0
  118. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/cron/test_cron_executor.py +0 -0
  119. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/locks/test_sql_lock.py +0 -0
  120. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/conftest.py +0 -0
  121. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_aqs_queue.py +0 -0
  122. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_cosmos_queue.py +0 -0
  123. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_message_queue_processor.py +0 -0
  124. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_sql_queue.py +0 -0
  125. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/queues/test_sqs_queue.py +0 -0
  126. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/requester/__init__.py +0 -0
  127. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/requester/test_requester.py +0 -0
  128. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_database.py +0 -0
  129. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_entity_repository.py +0 -0
  130. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_exceptions.py +0 -0
  131. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/test_logging.py +0 -0
  132. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/__init__.py +0 -0
  133. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_async_util.py +0 -0
  134. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_chain_util.py +0 -0
  135. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_date_util.py +0 -0
  136. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_dict_util.py +0 -0
  137. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_file_util.py +0 -0
  138. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_json_util.py +0 -0
  139. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_list_util.py +0 -0
  140. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_process_util.py +0 -0
  141. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_string_util.py +0 -0
  142. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/util/test_url_util.py +0 -0
  143. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/web3/__init__.py +0 -0
  144. {kiba_core-0.5.3.dev86 → kiba_core-0.5.3.dev87}/tests/web3/test_eth_client_registry.py +0 -0
  145. {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.dev86
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
- bodyBytes = await receivedRequest.body()
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
- bodyBytes = await receivedRequest.body()
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)
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class UploadedFile(BaseModel):
5
+ filename: str | None
6
+ contentType: str | None
7
+ content: bytes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev86
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
@@ -9,6 +9,7 @@ uvicorn[standard]>=0.54.0
9
9
  [core-api]
10
10
  starlette>=1.7.0
11
11
  uvicorn[standard]>=0.54.0
12
+ python-multipart>=0.0.29
12
13
 
13
14
  [cron]
14
15
  apscheduler<4,>=3.11.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiba-core"
3
- version = "0.5.3.dev86"
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"