From 097434e2cffeaa6868d688d809538d25e701ebda Mon Sep 17 00:00:00 2001 From: "hotdata-automation[bot]" <267177015+hotdata-automation[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:54:09 +0000 Subject: [PATCH] feat(uploads): support streaming uploads with on-demand part URLs --- .openapi-generator/FILES | 9 + CHANGELOG.md | 4 + docs/CreateUploadRequest.md | 2 +- docs/MintUploadPartsRequest.md | 30 ++ docs/MintUploadPartsResponse.md | 30 ++ docs/MintedUploadPartResponse.md | 31 ++ docs/UploadSessionResponse.md | 4 +- docs/UploadsApi.md | 98 +++++- hotdata/__init__.py | 6 + hotdata/api/uploads_api.py | 328 +++++++++++++++++- hotdata/models/__init__.py | 3 + hotdata/models/create_upload_request.py | 7 +- hotdata/models/mint_upload_parts_request.py | 88 +++++ hotdata/models/mint_upload_parts_response.py | 96 +++++ hotdata/models/minted_upload_part_response.py | 90 +++++ hotdata/models/upload_session_response.py | 4 +- test/test_create_upload_request.py | 1 - test/test_mint_upload_parts_request.py | 57 +++ test/test_mint_upload_parts_response.py | 61 ++++ test/test_minted_upload_part_response.py | 55 +++ test/test_uploads_api.py | 7 + 21 files changed, 996 insertions(+), 15 deletions(-) create mode 100644 docs/MintUploadPartsRequest.md create mode 100644 docs/MintUploadPartsResponse.md create mode 100644 docs/MintedUploadPartResponse.md create mode 100644 hotdata/models/mint_upload_parts_request.py create mode 100644 hotdata/models/mint_upload_parts_response.py create mode 100644 hotdata/models/minted_upload_part_response.py create mode 100644 test/test_mint_upload_parts_request.py create mode 100644 test/test_mint_upload_parts_response.py create mode 100644 test/test_minted_upload_part_response.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 14fdfd1..96d03ea 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -92,6 +92,9 @@ docs/LoadManagedTableRequest.md docs/LoadManagedTableResponse.md docs/ManagedSchemaResponse.md docs/ManagedTableResponse.md +docs/MintUploadPartsRequest.md +docs/MintUploadPartsResponse.md +docs/MintedUploadPartResponse.md docs/NumericProfileDetail.md docs/QueryApi.md docs/QueryRequest.md @@ -242,6 +245,9 @@ hotdata/models/load_managed_table_request.py hotdata/models/load_managed_table_response.py hotdata/models/managed_schema_response.py hotdata/models/managed_table_response.py +hotdata/models/mint_upload_parts_request.py +hotdata/models/mint_upload_parts_response.py +hotdata/models/minted_upload_part_response.py hotdata/models/numeric_profile_detail.py hotdata/models/query_request.py hotdata/models/query_response.py @@ -371,6 +377,9 @@ test/test_load_managed_table_request.py test/test_load_managed_table_response.py test/test_managed_schema_response.py test/test_managed_table_response.py +test/test_mint_upload_parts_request.py +test/test_mint_upload_parts_response.py +test/test_minted_upload_part_response.py test/test_numeric_profile_detail.py test/test_query_api.py test/test_query_request.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b54858b..b4632bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- feat(uploads): support streaming uploads with on-demand part URLs + ## [0.5.0] - 2026-06-28 ### Added diff --git a/docs/CreateUploadRequest.md b/docs/CreateUploadRequest.md index 76955eb..36417b6 100644 --- a/docs/CreateUploadRequest.md +++ b/docs/CreateUploadRequest.md @@ -10,7 +10,7 @@ Name | Type | Description | Notes **checksum_value** | **str** | Integrity checksum value, paired with `checksum_algo`. Optional. | [optional] **content_encoding** | **str** | Content encoding to record for the uploaded file (for example `gzip`). Optional. | [optional] **content_type** | **str** | Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional. | [optional] -**declared_size_bytes** | **int** | The exact size, in bytes, of the file you will upload. Validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize. | +**declared_size_bytes** | **int** | The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from `POST /v1/uploads/{upload_id}/parts` as you upload, and finalize validates only that the file is non-empty. | [optional] **filename** | **str** | Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect where the bytes are stored or how they are loaded. | [optional] **part_size** | **int** | Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single `PUT`. Omit to let the service choose. | [optional] diff --git a/docs/MintUploadPartsRequest.md b/docs/MintUploadPartsRequest.md new file mode 100644 index 0000000..ce0a58a --- /dev/null +++ b/docs/MintUploadPartsRequest.md @@ -0,0 +1,30 @@ +# MintUploadPartsRequest + +Request body for `POST /v1/uploads/{upload_id}/parts`: mint presigned upload URLs for specific parts of a streaming (unknown-size) multi-part upload. Provide the 1-based part numbers you want URLs for. Mint parts as you upload, and re-request a part number if its URL expires before you finish — the parts you have already uploaded are unaffected. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**part_numbers** | **List[int]** | The 1-based part numbers to mint URLs for. Must be non-empty; each number must be between 1 and the maximum number of parts allowed. | + +## Example + +```python +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest + +# TODO update the JSON string below +json = "{}" +# create an instance of MintUploadPartsRequest from a JSON string +mint_upload_parts_request_instance = MintUploadPartsRequest.from_json(json) +# print the JSON string representation of the object +print(MintUploadPartsRequest.to_json()) + +# convert the object into a dict +mint_upload_parts_request_dict = mint_upload_parts_request_instance.to_dict() +# create an instance of MintUploadPartsRequest from a dict +mint_upload_parts_request_from_dict = MintUploadPartsRequest.from_dict(mint_upload_parts_request_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/MintUploadPartsResponse.md b/docs/MintUploadPartsResponse.md new file mode 100644 index 0000000..f50b109 --- /dev/null +++ b/docs/MintUploadPartsResponse.md @@ -0,0 +1,30 @@ +# MintUploadPartsResponse + +Response body for `POST /v1/uploads/{upload_id}/parts`: the minted part URLs, in ascending part-number order. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**parts** | [**List[MintedUploadPartResponse]**](MintedUploadPartResponse.md) | The minted part URLs, in ascending part-number order. `PUT` each part's bytes to its URL and keep the response's `ETag` to pass to finalize. | + +## Example + +```python +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of MintUploadPartsResponse from a JSON string +mint_upload_parts_response_instance = MintUploadPartsResponse.from_json(json) +# print the JSON string representation of the object +print(MintUploadPartsResponse.to_json()) + +# convert the object into a dict +mint_upload_parts_response_dict = mint_upload_parts_response_instance.to_dict() +# create an instance of MintUploadPartsResponse from a dict +mint_upload_parts_response_from_dict = MintUploadPartsResponse.from_dict(mint_upload_parts_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/MintedUploadPartResponse.md b/docs/MintedUploadPartResponse.md new file mode 100644 index 0000000..8dc7b60 --- /dev/null +++ b/docs/MintedUploadPartResponse.md @@ -0,0 +1,31 @@ +# MintedUploadPartResponse + +One minted part URL. + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**part_number** | **int** | The 1-based part number this URL is for. | +**url** | **str** | Short-lived URL to `PUT` this part's bytes to. Keep the response's `ETag` and pass the `{part_number, e_tag}` pair to finalize. | + +## Example + +```python +from hotdata.models.minted_upload_part_response import MintedUploadPartResponse + +# TODO update the JSON string below +json = "{}" +# create an instance of MintedUploadPartResponse from a JSON string +minted_upload_part_response_instance = MintedUploadPartResponse.from_json(json) +# print the JSON string representation of the object +print(MintedUploadPartResponse.to_json()) + +# convert the object into a dict +minted_upload_part_response_dict = minted_upload_part_response_instance.to_dict() +# create an instance of MintedUploadPartResponse from a dict +minted_upload_part_response_from_dict = MintedUploadPartResponse.from_dict(minted_upload_part_response_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/UploadSessionResponse.md b/docs/UploadSessionResponse.md index f90cccc..608fd15 100644 --- a/docs/UploadSessionResponse.md +++ b/docs/UploadSessionResponse.md @@ -9,8 +9,8 @@ Name | Type | Description | Notes **finalize_token** | **str** | One-time token that authorizes finalizing this upload. Returned exactly once at create time — store it; it cannot be retrieved again. | **headers** | **Dict[str, str]** | Headers you must send verbatim with each `PUT`. Currently always empty; present so a future mode can require signed headers without changing the response shape. | **mode** | **str** | Upload mode: `single` (upload the whole file with one `PUT` to `url`) or `multipart` (upload each part with one `PUT` to the matching entry in `part_urls`). Modeled as a string so additional modes can be added later without breaking clients. | -**part_size** | **int** | For a `multipart` upload, the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` to `part_urls[i - 1]`, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by `part_urls.len()`, which can make a non-final part smaller than the 5 MiB minimum that storage requires (the upload then fails at finalize). Absent for `single` uploads. | [optional] -**part_urls** | **List[str]** | For a `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads. | [optional] +**part_size** | **int** | For a `multipart` upload (both known-size and streaming), the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` as part *i*, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by the number of parts, which can make a non-final part smaller than the 5 MiB minimum that storage requires (the upload then fails at finalize). Absent for `single` uploads. | [optional] +**part_urls** | **List[str]** | For a known-size `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads, and also absent for a streaming (unknown-size) `multipart` upload — there, mint part URLs on demand via `POST /v1/uploads/{upload_id}/parts`. | [optional] **upload_id** | **str** | Identifier for this upload. Pass it to the finalize endpoint and to the managed-table load endpoint once finalized. | **url** | **str** | The URL to `PUT` the raw file bytes to, for a `single` upload. Short-lived — upload promptly and finalize. Absent for `multipart` uploads (use `part_urls`). | [optional] diff --git a/docs/UploadsApi.md b/docs/UploadsApi.md index 5eeed19..4417552 100644 --- a/docs/UploadsApi.md +++ b/docs/UploadsApi.md @@ -8,6 +8,7 @@ Method | HTTP request | Description [**create_upload_sessions_batch_handler**](UploadsApi.md#create_upload_sessions_batch_handler) | **POST** /v1/uploads/batch | Create upload sessions in bulk [**finalize_upload_handler**](UploadsApi.md#finalize_upload_handler) | **POST** /v1/uploads/{upload_id}/finalize | Finalize upload [**list_uploads**](UploadsApi.md#list_uploads) | **GET** /v1/files | List uploads +[**mint_upload_parts_handler**](UploadsApi.md#mint_upload_parts_handler) | **POST** /v1/uploads/{upload_id}/parts | Mint upload part URLs [**upload_file**](UploadsApi.md#upload_file) | **POST** /v1/files | Upload file @@ -16,7 +17,7 @@ Method | HTTP request | Description Create upload session -Create an upload session for a file you will send directly to storage. Based on the declared size, the response is one of two shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). In both cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. +Create an upload session for a file you will send directly to storage. The response is one of three shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). For a file whose size you do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. @@ -198,7 +199,7 @@ Name | Type | Description | Notes Finalize upload -Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. The uploaded file's size is validated against the size declared at create time; a mismatch is rejected. Finalize is exactly-once: a second finalize of the same upload is rejected. +Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected. ### Example @@ -368,6 +369,99 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **mint_upload_parts_handler** +> MintUploadPartsResponse mint_upload_parts_handler(upload_id, x_upload_finalize_token, mint_upload_parts_request) + +Mint upload part URLs + +Mint short-lived presigned URLs for specific parts of a multi-part upload. This is required for a streaming (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint a part number whose URL expired before you finished uploading it. + +### Example + +* Api Key Authentication (WorkspaceId): +* Bearer Authentication (BearerAuth): + +```python +import hotdata +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse +from hotdata.rest import ApiException +from pprint import pprint + +# Defining the host is optional and defaults to https://api.hotdata.dev +# See configuration.py for a list of all supported configuration parameters. +configuration = hotdata.Configuration( + host = "https://api.hotdata.dev" +) + +# The client must configure the authentication and authorization parameters +# in accordance with the API server security policy. +# Examples for each auth method are provided below, use the example that +# satisfies your auth use case. + +# Configure API key authorization: WorkspaceId +configuration.api_key['WorkspaceId'] = os.environ["API_KEY"] + +# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed +# configuration.api_key_prefix['WorkspaceId'] = 'Bearer' + +# Configure Bearer authorization: BearerAuth +configuration = hotdata.Configuration( + access_token = os.environ["BEARER_TOKEN"] +) + +# Enter a context with an instance of the API client +with hotdata.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = hotdata.UploadsApi(api_client) + upload_id = 'upload_id_example' # str | Upload session ID returned at create time + x_upload_finalize_token = 'x_upload_finalize_token_example' # str | One-time finalize token returned when the session was created + mint_upload_parts_request = hotdata.MintUploadPartsRequest() # MintUploadPartsRequest | + + try: + # Mint upload part URLs + api_response = api_instance.mint_upload_parts_handler(upload_id, x_upload_finalize_token, mint_upload_parts_request) + print("The response of UploadsApi->mint_upload_parts_handler:\n") + pprint(api_response) + except Exception as e: + print("Exception when calling UploadsApi->mint_upload_parts_handler: %s\n" % e) +``` + + + +### Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **upload_id** | **str**| Upload session ID returned at create time | + **x_upload_finalize_token** | **str**| One-time finalize token returned when the session was created | + **mint_upload_parts_request** | [**MintUploadPartsRequest**](MintUploadPartsRequest.md)| | + +### Return type + +[**MintUploadPartsResponse**](MintUploadPartsResponse.md) + +### Authorization + +[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | Minted part URLs | - | +**400** | Invalid finalize token, invalid part numbers, batch too large, or the upload is not a multi-part upload | - | +**404** | Upload session not found | - | +**501** | Storage backend cannot issue upload URLs | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **upload_file** > UploadResponse upload_file(body) diff --git a/hotdata/__init__.py b/hotdata/__init__.py index b992a24..0786849 100644 --- a/hotdata/__init__.py +++ b/hotdata/__init__.py @@ -133,6 +133,9 @@ "LoadManagedTableResponse", "ManagedSchemaResponse", "ManagedTableResponse", + "MintUploadPartsRequest", + "MintUploadPartsResponse", + "MintedUploadPartResponse", "NumericProfileDetail", "QueryRequest", "QueryResponse", @@ -283,6 +286,9 @@ from hotdata.models.load_managed_table_response import LoadManagedTableResponse as LoadManagedTableResponse from hotdata.models.managed_schema_response import ManagedSchemaResponse as ManagedSchemaResponse from hotdata.models.managed_table_response import ManagedTableResponse as ManagedTableResponse +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest as MintUploadPartsRequest +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse as MintUploadPartsResponse +from hotdata.models.minted_upload_part_response import MintedUploadPartResponse as MintedUploadPartResponse from hotdata.models.numeric_profile_detail import NumericProfileDetail as NumericProfileDetail from hotdata.models.query_request import QueryRequest as QueryRequest from hotdata.models.query_response import QueryResponse as QueryResponse diff --git a/hotdata/api/uploads_api.py b/hotdata/api/uploads_api.py index c05fffd..bddf5b6 100644 --- a/hotdata/api/uploads_api.py +++ b/hotdata/api/uploads_api.py @@ -25,6 +25,8 @@ from hotdata.models.finalize_upload_request import FinalizeUploadRequest from hotdata.models.finalize_upload_response import FinalizeUploadResponse from hotdata.models.list_uploads_response import ListUploadsResponse +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse from hotdata.models.upload_response import UploadResponse from hotdata.models.upload_session_response import UploadSessionResponse @@ -65,7 +67,7 @@ def create_upload_session_handler( ) -> UploadSessionResponse: """Create upload session - Create an upload session for a file you will send directly to storage. Based on the declared size, the response is one of two shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). In both cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. + Create an upload session for a file you will send directly to storage. The response is one of three shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). For a file whose size you do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -134,7 +136,7 @@ def create_upload_session_handler_with_http_info( ) -> ApiResponse[UploadSessionResponse]: """Create upload session - Create an upload session for a file you will send directly to storage. Based on the declared size, the response is one of two shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). In both cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. + Create an upload session for a file you will send directly to storage. The response is one of three shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). For a file whose size you do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -203,7 +205,7 @@ def create_upload_session_handler_without_preload_content( ) -> RESTResponseType: """Create upload session - Create an upload session for a file you will send directly to storage. Based on the declared size, the response is one of two shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). In both cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. + Create an upload session for a file you will send directly to storage. The response is one of three shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across `part_urls.len()` (which can make a non-final part too small). For a file whose size you do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send the file to the `POST /v1/files` endpoint instead. :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -629,7 +631,7 @@ def finalize_upload_handler( ) -> FinalizeUploadResponse: """Finalize upload - Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. The uploaded file's size is validated against the size declared at create time; a mismatch is rejected. Finalize is exactly-once: a second finalize of the same upload is rejected. + Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected. :param upload_id: Upload session ID returned at create time (required) :type upload_id: str @@ -706,7 +708,7 @@ def finalize_upload_handler_with_http_info( ) -> ApiResponse[FinalizeUploadResponse]: """Finalize upload - Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. The uploaded file's size is validated against the size declared at create time; a mismatch is rejected. Finalize is exactly-once: a second finalize of the same upload is rejected. + Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected. :param upload_id: Upload session ID returned at create time (required) :type upload_id: str @@ -783,7 +785,7 @@ def finalize_upload_handler_without_preload_content( ) -> RESTResponseType: """Finalize upload - Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. The uploaded file's size is validated against the size declared at create time; a mismatch is rejected. Finalize is exactly-once: a second finalize of the same upload is rejected. + Confirm that a file has been uploaded to storage and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected. :param upload_id: Upload session ID returned at create time (required) :type upload_id: str @@ -1180,6 +1182,320 @@ def _list_uploads_serialize( + @validate_call + def mint_upload_parts_handler( + self, + upload_id: Annotated[StrictStr, Field(description="Upload session ID returned at create time")], + x_upload_finalize_token: Annotated[StrictStr, Field(description="One-time finalize token returned when the session was created")], + mint_upload_parts_request: MintUploadPartsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> MintUploadPartsResponse: + """Mint upload part URLs + + Mint short-lived presigned URLs for specific parts of a multi-part upload. This is required for a streaming (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint a part number whose URL expired before you finished uploading it. + + :param upload_id: Upload session ID returned at create time (required) + :type upload_id: str + :param x_upload_finalize_token: One-time finalize token returned when the session was created (required) + :type x_upload_finalize_token: str + :param mint_upload_parts_request: (required) + :type mint_upload_parts_request: MintUploadPartsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._mint_upload_parts_handler_serialize( + upload_id=upload_id, + x_upload_finalize_token=x_upload_finalize_token, + mint_upload_parts_request=mint_upload_parts_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "MintUploadPartsResponse", + '400': "ApiErrorResponse", + '404': "ApiErrorResponse", + '501': "ApiErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def mint_upload_parts_handler_with_http_info( + self, + upload_id: Annotated[StrictStr, Field(description="Upload session ID returned at create time")], + x_upload_finalize_token: Annotated[StrictStr, Field(description="One-time finalize token returned when the session was created")], + mint_upload_parts_request: MintUploadPartsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[MintUploadPartsResponse]: + """Mint upload part URLs + + Mint short-lived presigned URLs for specific parts of a multi-part upload. This is required for a streaming (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint a part number whose URL expired before you finished uploading it. + + :param upload_id: Upload session ID returned at create time (required) + :type upload_id: str + :param x_upload_finalize_token: One-time finalize token returned when the session was created (required) + :type x_upload_finalize_token: str + :param mint_upload_parts_request: (required) + :type mint_upload_parts_request: MintUploadPartsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._mint_upload_parts_handler_serialize( + upload_id=upload_id, + x_upload_finalize_token=x_upload_finalize_token, + mint_upload_parts_request=mint_upload_parts_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "MintUploadPartsResponse", + '400': "ApiErrorResponse", + '404': "ApiErrorResponse", + '501': "ApiErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def mint_upload_parts_handler_without_preload_content( + self, + upload_id: Annotated[StrictStr, Field(description="Upload session ID returned at create time")], + x_upload_finalize_token: Annotated[StrictStr, Field(description="One-time finalize token returned when the session was created")], + mint_upload_parts_request: MintUploadPartsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Mint upload part URLs + + Mint short-lived presigned URLs for specific parts of a multi-part upload. This is required for a streaming (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint a part number whose URL expired before you finished uploading it. + + :param upload_id: Upload session ID returned at create time (required) + :type upload_id: str + :param x_upload_finalize_token: One-time finalize token returned when the session was created (required) + :type x_upload_finalize_token: str + :param mint_upload_parts_request: (required) + :type mint_upload_parts_request: MintUploadPartsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._mint_upload_parts_handler_serialize( + upload_id=upload_id, + x_upload_finalize_token=x_upload_finalize_token, + mint_upload_parts_request=mint_upload_parts_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "MintUploadPartsResponse", + '400': "ApiErrorResponse", + '404': "ApiErrorResponse", + '501': "ApiErrorResponse", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _mint_upload_parts_handler_serialize( + self, + upload_id, + x_upload_finalize_token, + mint_upload_parts_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if upload_id is not None: + _path_params['upload_id'] = upload_id + # process the query parameters + # process the header parameters + if x_upload_finalize_token is not None: + _header_params['X-Upload-Finalize-Token'] = x_upload_finalize_token + # process the form parameters + # process the body parameter + if mint_upload_parts_request is not None: + _body_params = mint_upload_parts_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'WorkspaceId', + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/uploads/{upload_id}/parts', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call def upload_file( self, diff --git a/hotdata/models/__init__.py b/hotdata/models/__init__.py index b967cfa..f106361 100644 --- a/hotdata/models/__init__.py +++ b/hotdata/models/__init__.py @@ -97,6 +97,9 @@ from hotdata.models.load_managed_table_response import LoadManagedTableResponse from hotdata.models.managed_schema_response import ManagedSchemaResponse from hotdata.models.managed_table_response import ManagedTableResponse +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse +from hotdata.models.minted_upload_part_response import MintedUploadPartResponse from hotdata.models.numeric_profile_detail import NumericProfileDetail from hotdata.models.query_request import QueryRequest from hotdata.models.query_response import QueryResponse diff --git a/hotdata/models/create_upload_request.py b/hotdata/models/create_upload_request.py index d05839f..8eeb5f4 100644 --- a/hotdata/models/create_upload_request.py +++ b/hotdata/models/create_upload_request.py @@ -32,7 +32,7 @@ class CreateUploadRequest(BaseModel): checksum_value: Optional[StrictStr] = Field(default=None, description="Integrity checksum value, paired with `checksum_algo`. Optional.") content_encoding: Optional[StrictStr] = Field(default=None, description="Content encoding to record for the uploaded file (for example `gzip`). Optional.") content_type: Optional[StrictStr] = Field(default=None, description="Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional.") - declared_size_bytes: Annotated[int, Field(strict=True, ge=0)] = Field(description="The exact size, in bytes, of the file you will upload. Validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize.") + declared_size_bytes: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=None, description="The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from `POST /v1/uploads/{upload_id}/parts` as you upload, and finalize validates only that the file is non-empty.") filename: Optional[StrictStr] = Field(default=None, description="Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect where the bytes are stored or how they are loaded.") part_size: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=None, description="Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single `PUT`. Omit to let the service choose.") __properties: ClassVar[List[str]] = ["checksum_algo", "checksum_value", "content_encoding", "content_type", "declared_size_bytes", "filename", "part_size"] @@ -96,6 +96,11 @@ def to_dict(self) -> Dict[str, Any]: if self.content_type is None and "content_type" in self.model_fields_set: _dict['content_type'] = None + # set to None if declared_size_bytes (nullable) is None + # and model_fields_set contains the field + if self.declared_size_bytes is None and "declared_size_bytes" in self.model_fields_set: + _dict['declared_size_bytes'] = None + # set to None if filename (nullable) is None # and model_fields_set contains the field if self.filename is None and "filename" in self.model_fields_set: diff --git a/hotdata/models/mint_upload_parts_request.py b/hotdata/models/mint_upload_parts_request.py new file mode 100644 index 0000000..6d760e8 --- /dev/null +++ b/hotdata/models/mint_upload_parts_request.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class MintUploadPartsRequest(BaseModel): + """ + Request body for `POST /v1/uploads/{upload_id}/parts`: mint presigned upload URLs for specific parts of a streaming (unknown-size) multi-part upload. Provide the 1-based part numbers you want URLs for. Mint parts as you upload, and re-request a part number if its URL expires before you finish — the parts you have already uploaded are unaffected. + """ # noqa: E501 + part_numbers: List[StrictInt] = Field(description="The 1-based part numbers to mint URLs for. Must be non-empty; each number must be between 1 and the maximum number of parts allowed.") + __properties: ClassVar[List[str]] = ["part_numbers"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MintUploadPartsRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MintUploadPartsRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "part_numbers": obj.get("part_numbers") + }) + return _obj + + diff --git a/hotdata/models/mint_upload_parts_response.py b/hotdata/models/mint_upload_parts_response.py new file mode 100644 index 0000000..967bb81 --- /dev/null +++ b/hotdata/models/mint_upload_parts_response.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List +from hotdata.models.minted_upload_part_response import MintedUploadPartResponse +from typing import Optional, Set +from typing_extensions import Self + +class MintUploadPartsResponse(BaseModel): + """ + Response body for `POST /v1/uploads/{upload_id}/parts`: the minted part URLs, in ascending part-number order. + """ # noqa: E501 + parts: List[MintedUploadPartResponse] = Field(description="The minted part URLs, in ascending part-number order. `PUT` each part's bytes to its URL and keep the response's `ETag` to pass to finalize.") + __properties: ClassVar[List[str]] = ["parts"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MintUploadPartsResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in parts (list) + _items = [] + if self.parts: + for _item_parts in self.parts: + if _item_parts: + _items.append(_item_parts.to_dict()) + _dict['parts'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MintUploadPartsResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "parts": [MintedUploadPartResponse.from_dict(_item) for _item in obj["parts"]] if obj.get("parts") is not None else None + }) + return _obj + + diff --git a/hotdata/models/minted_upload_part_response.py b/hotdata/models/minted_upload_part_response.py new file mode 100644 index 0000000..b112805 --- /dev/null +++ b/hotdata/models/minted_upload_part_response.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class MintedUploadPartResponse(BaseModel): + """ + One minted part URL. + """ # noqa: E501 + part_number: StrictInt = Field(description="The 1-based part number this URL is for.") + url: StrictStr = Field(description="Short-lived URL to `PUT` this part's bytes to. Keep the response's `ETag` and pass the `{part_number, e_tag}` pair to finalize.") + __properties: ClassVar[List[str]] = ["part_number", "url"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MintedUploadPartResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MintedUploadPartResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "part_number": obj.get("part_number"), + "url": obj.get("url") + }) + return _obj + + diff --git a/hotdata/models/upload_session_response.py b/hotdata/models/upload_session_response.py index 59bfab3..4c69f67 100644 --- a/hotdata/models/upload_session_response.py +++ b/hotdata/models/upload_session_response.py @@ -31,8 +31,8 @@ class UploadSessionResponse(BaseModel): finalize_token: StrictStr = Field(description="One-time token that authorizes finalizing this upload. Returned exactly once at create time — store it; it cannot be retrieved again.") headers: Dict[str, StrictStr] = Field(description="Headers you must send verbatim with each `PUT`. Currently always empty; present so a future mode can require signed headers without changing the response shape.") mode: StrictStr = Field(description="Upload mode: `single` (upload the whole file with one `PUT` to `url`) or `multipart` (upload each part with one `PUT` to the matching entry in `part_urls`). Modeled as a string so additional modes can be added later without breaking clients.") - part_size: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=None, description="For a `multipart` upload, the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` to `part_urls[i - 1]`, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by `part_urls.len()`, which can make a non-final part smaller than the 5 MiB minimum that storage requires (the upload then fails at finalize). Absent for `single` uploads.") - part_urls: Optional[List[StrictStr]] = Field(default=None, description="For a `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads.") + part_size: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=None, description="For a `multipart` upload (both known-size and streaming), the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` as part *i*, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by the number of parts, which can make a non-final part smaller than the 5 MiB minimum that storage requires (the upload then fails at finalize). Absent for `single` uploads.") + part_urls: Optional[List[StrictStr]] = Field(default=None, description="For a known-size `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads, and also absent for a streaming (unknown-size) `multipart` upload — there, mint part URLs on demand via `POST /v1/uploads/{upload_id}/parts`.") upload_id: StrictStr = Field(description="Identifier for this upload. Pass it to the finalize endpoint and to the managed-table load endpoint once finalized.") url: Optional[StrictStr] = Field(default=None, description="The URL to `PUT` the raw file bytes to, for a `single` upload. Short-lived — upload promptly and finalize. Absent for `multipart` uploads (use `part_urls`).") __properties: ClassVar[List[str]] = ["finalize_token", "headers", "mode", "part_size", "part_urls", "upload_id", "url"] diff --git a/test/test_create_upload_request.py b/test/test_create_upload_request.py index f134ebd..d5323c1 100644 --- a/test/test_create_upload_request.py +++ b/test/test_create_upload_request.py @@ -46,7 +46,6 @@ def make_instance(self, include_optional) -> CreateUploadRequest: ) else: return CreateUploadRequest( - declared_size_bytes = 0, ) """ diff --git a/test/test_mint_upload_parts_request.py b/test/test_mint_upload_parts_request.py new file mode 100644 index 0000000..6f674b1 --- /dev/null +++ b/test/test_mint_upload_parts_request.py @@ -0,0 +1,57 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest + +class TestMintUploadPartsRequest(unittest.TestCase): + """MintUploadPartsRequest unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MintUploadPartsRequest: + """Test MintUploadPartsRequest + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MintUploadPartsRequest` + """ + model = MintUploadPartsRequest() + if include_optional: + return MintUploadPartsRequest( + part_numbers = [ + 56 + ] + ) + else: + return MintUploadPartsRequest( + part_numbers = [ + 56 + ], + ) + """ + + def testMintUploadPartsRequest(self): + """Test MintUploadPartsRequest""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_mint_upload_parts_response.py b/test/test_mint_upload_parts_response.py new file mode 100644 index 0000000..f3e3ea0 --- /dev/null +++ b/test/test_mint_upload_parts_response.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from hotdata.models.mint_upload_parts_response import MintUploadPartsResponse + +class TestMintUploadPartsResponse(unittest.TestCase): + """MintUploadPartsResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MintUploadPartsResponse: + """Test MintUploadPartsResponse + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MintUploadPartsResponse` + """ + model = MintUploadPartsResponse() + if include_optional: + return MintUploadPartsResponse( + parts = [ + hotdata.models.minted_upload_part_response.MintedUploadPartResponse( + part_number = 56, + url = '', ) + ] + ) + else: + return MintUploadPartsResponse( + parts = [ + hotdata.models.minted_upload_part_response.MintedUploadPartResponse( + part_number = 56, + url = '', ) + ], + ) + """ + + def testMintUploadPartsResponse(self): + """Test MintUploadPartsResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_minted_upload_part_response.py b/test/test_minted_upload_part_response.py new file mode 100644 index 0000000..160abdb --- /dev/null +++ b/test/test_minted_upload_part_response.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +""" + Hotdata API + + Powerful data platform API for managed databases, queries, and analytics. + + The version of the OpenAPI document: 1.0.0 + Contact: developers@hotdata.dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from hotdata.models.minted_upload_part_response import MintedUploadPartResponse + +class TestMintedUploadPartResponse(unittest.TestCase): + """MintedUploadPartResponse unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> MintedUploadPartResponse: + """Test MintedUploadPartResponse + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `MintedUploadPartResponse` + """ + model = MintedUploadPartResponse() + if include_optional: + return MintedUploadPartResponse( + part_number = 56, + url = '' + ) + else: + return MintedUploadPartResponse( + part_number = 56, + url = '', + ) + """ + + def testMintedUploadPartResponse(self): + """Test MintedUploadPartResponse""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_uploads_api.py b/test/test_uploads_api.py index cb99a7b..7c945c5 100644 --- a/test/test_uploads_api.py +++ b/test/test_uploads_api.py @@ -55,6 +55,13 @@ def test_list_uploads(self) -> None: """ pass + def test_mint_upload_parts_handler(self) -> None: + """Test case for mint_upload_parts_handler + + Mint upload part URLs + """ + pass + def test_upload_file(self) -> None: """Test case for upload_file