From e986eb1efb8531d435bac3e20e186b978f218f97 Mon Sep 17 00:00:00 2001 From: "hotdata-automation[bot]" <267177015+hotdata-automation[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:37:10 +0000 Subject: [PATCH 1/2] chore: remove files endpoints in favor of uploads --- .openapi-generator/FILES | 9 - CHANGELOG.md | 4 + docs/ListUploadsResponse.md | 30 - docs/LoadManagedTableRequest.md | 2 +- docs/UploadInfo.md | 34 -- docs/UploadResponse.md | 34 -- docs/UploadsApi.md | 179 +----- hotdata/__init__.py | 6 - hotdata/api/uploads_api.py | 565 +------------------ hotdata/models/__init__.py | 3 - hotdata/models/list_uploads_response.py | 96 ---- hotdata/models/load_managed_table_request.py | 2 +- hotdata/models/upload_info.py | 102 ---- hotdata/models/upload_response.py | 102 ---- test/test_list_uploads_response.py | 67 --- test/test_upload_info.py | 60 -- test/test_upload_response.py | 60 -- test/test_uploads_api.py | 14 - 18 files changed, 18 insertions(+), 1351 deletions(-) delete mode 100644 docs/ListUploadsResponse.md delete mode 100644 docs/UploadInfo.md delete mode 100644 docs/UploadResponse.md delete mode 100644 hotdata/models/list_uploads_response.py delete mode 100644 hotdata/models/upload_info.py delete mode 100644 hotdata/models/upload_response.py delete mode 100644 test/test_list_uploads_response.py delete mode 100644 test/test_upload_info.py delete mode 100644 test/test_upload_response.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 81479e3..5f07b12 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -87,7 +87,6 @@ docs/ListResultsResponse.md docs/ListSavedQueriesResponse.md docs/ListSavedQueryVersionsResponse.md docs/ListSecretsResponse.md -docs/ListUploadsResponse.md docs/ListWorkspacesResponse.md docs/LoadManagedTableRequest.md docs/LoadManagedTableResponse.md @@ -128,8 +127,6 @@ docs/UpdateEmbeddingProviderResponse.md docs/UpdateSavedQueryRequest.md docs/UpdateSecretRequest.md docs/UpdateSecretResponse.md -docs/UploadInfo.md -docs/UploadResponse.md docs/UploadSessionResponse.md docs/UploadsApi.md docs/UpsertDatabaseContextRequest.md @@ -241,7 +238,6 @@ hotdata/models/list_results_response.py hotdata/models/list_saved_queries_response.py hotdata/models/list_saved_query_versions_response.py hotdata/models/list_secrets_response.py -hotdata/models/list_uploads_response.py hotdata/models/list_workspaces_response.py hotdata/models/load_managed_table_request.py hotdata/models/load_managed_table_response.py @@ -276,8 +272,6 @@ hotdata/models/update_embedding_provider_response.py hotdata/models/update_saved_query_request.py hotdata/models/update_secret_request.py hotdata/models/update_secret_response.py -hotdata/models/upload_info.py -hotdata/models/upload_response.py hotdata/models/upload_session_response.py hotdata/models/upsert_database_context_request.py hotdata/models/upsert_database_context_response.py @@ -374,7 +368,6 @@ test/test_list_results_response.py test/test_list_saved_queries_response.py test/test_list_saved_query_versions_response.py test/test_list_secrets_response.py -test/test_list_uploads_response.py test/test_list_workspaces_response.py test/test_load_managed_table_request.py test/test_load_managed_table_response.py @@ -415,8 +408,6 @@ test/test_update_embedding_provider_response.py test/test_update_saved_query_request.py test/test_update_secret_request.py test/test_update_secret_response.py -test/test_upload_info.py -test/test_upload_response.py test/test_upload_session_response.py test/test_uploads_api.py test/test_upsert_database_context_request.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dba6d43..0f27f08 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 + +- chore: remove files endpoints in favor of uploads + ## [0.8.0] - 2026-07-20 ### Changed diff --git a/docs/ListUploadsResponse.md b/docs/ListUploadsResponse.md deleted file mode 100644 index 33bad7c..0000000 --- a/docs/ListUploadsResponse.md +++ /dev/null @@ -1,30 +0,0 @@ -# ListUploadsResponse - -Response body for GET /v1/files - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**uploads** | [**List[UploadInfo]**](UploadInfo.md) | | - -## Example - -```python -from hotdata.models.list_uploads_response import ListUploadsResponse - -# TODO update the JSON string below -json = "{}" -# create an instance of ListUploadsResponse from a JSON string -list_uploads_response_instance = ListUploadsResponse.from_json(json) -# print the JSON string representation of the object -print(ListUploadsResponse.to_json()) - -# convert the object into a dict -list_uploads_response_dict = list_uploads_response_instance.to_dict() -# create an instance of ListUploadsResponse from a dict -list_uploads_response_from_dict = ListUploadsResponse.from_dict(list_uploads_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/LoadManagedTableRequest.md b/docs/LoadManagedTableRequest.md index 58a18ca..fd91b72 100644 --- a/docs/LoadManagedTableRequest.md +++ b/docs/LoadManagedTableRequest.md @@ -12,7 +12,7 @@ Name | Type | Description | Notes **key** | **List[str]** | Key columns identifying rows for `\"delete\"`, `\"update\"`, and `\"upsert\"` loads — the columns whose values decide which existing row an incoming row removes, updates, or replaces. Omit to use the key the table was created with. Keep the key consistent across loads of the same table: changing it re-targets which rows are matched. Ignored for `\"replace\"` and `\"append\"`. | [optional] **mode** | **str** | How the data is applied: `\"replace\"` overwrites the table's contents, `\"append\"` inserts the new rows on top of the existing data. | **result_id** | **str** | ID of a persisted query result (see `GET /v1/results/{result_id}`) to publish as the table's contents. The result is copied into the table, so the table keeps its data even after the result expires. A result can be loaded into any number of tables. Provide either this or `upload_id`, not both. | [optional] -**upload_id** | **str** | ID of a previously-staged upload (see `POST /v1/files`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. Provide either this or `result_id`, not both. | [optional] +**upload_id** | **str** | ID of a previously-staged upload (see `POST /v1/uploads`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. Provide either this or `result_id`, not both. | [optional] ## Example diff --git a/docs/UploadInfo.md b/docs/UploadInfo.md deleted file mode 100644 index 3b79b22..0000000 --- a/docs/UploadInfo.md +++ /dev/null @@ -1,34 +0,0 @@ -# UploadInfo - -Single upload info for listing - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**content_type** | **str** | | [optional] -**created_at** | **datetime** | | -**id** | **str** | | -**size_bytes** | **int** | | -**status** | **str** | | - -## Example - -```python -from hotdata.models.upload_info import UploadInfo - -# TODO update the JSON string below -json = "{}" -# create an instance of UploadInfo from a JSON string -upload_info_instance = UploadInfo.from_json(json) -# print the JSON string representation of the object -print(UploadInfo.to_json()) - -# convert the object into a dict -upload_info_dict = upload_info_instance.to_dict() -# create an instance of UploadInfo from a dict -upload_info_from_dict = UploadInfo.from_dict(upload_info_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/UploadResponse.md b/docs/UploadResponse.md deleted file mode 100644 index 9a8e3a1..0000000 --- a/docs/UploadResponse.md +++ /dev/null @@ -1,34 +0,0 @@ -# UploadResponse - -Response body for POST /v1/files - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**content_type** | **str** | | [optional] -**created_at** | **datetime** | | -**id** | **str** | | -**size_bytes** | **int** | | -**status** | **str** | | - -## Example - -```python -from hotdata.models.upload_response import UploadResponse - -# TODO update the JSON string below -json = "{}" -# create an instance of UploadResponse from a JSON string -upload_response_instance = UploadResponse.from_json(json) -# print the JSON string representation of the object -print(UploadResponse.to_json()) - -# convert the object into a dict -upload_response_dict = upload_response_instance.to_dict() -# create an instance of UploadResponse from a dict -upload_response_from_dict = UploadResponse.from_dict(upload_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/UploadsApi.md b/docs/UploadsApi.md index 4417552..e2e23ca 100644 --- a/docs/UploadsApi.md +++ b/docs/UploadsApi.md @@ -7,9 +7,7 @@ Method | HTTP request | Description [**create_upload_session_handler**](UploadsApi.md#create_upload_session_handler) | **POST** /v1/uploads | Create upload session [**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 # **create_upload_session_handler** @@ -21,7 +19,7 @@ Create an upload session for a file you will send directly to storage. The respo 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. +If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs (for example a local-filesystem development backend). ### Example @@ -100,7 +98,7 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **201** | Upload session created | - | **400** | Invalid request (e.g. file too large, unsupported checksum algorithm) | - | -**501** | Storage backend cannot issue upload URLs; use POST /v1/files instead | - | +**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) @@ -111,7 +109,7 @@ Create upload sessions in bulk Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. -If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send each file to the `POST /v1/files` endpoint instead. +If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs (for example a local-filesystem development backend). ### Example @@ -190,7 +188,7 @@ Name | Type | Description | Notes |-------------|-------------|------------------| **201** | Upload sessions created | - | **400** | Invalid request (e.g. a file too large, unsupported checksum algorithm) | - | -**501** | Storage backend cannot issue upload URLs; use POST /v1/files instead | - | +**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) @@ -286,89 +284,6 @@ 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) -# **list_uploads** -> ListUploadsResponse list_uploads(status=status) - -List uploads - -### Example - -* Api Key Authentication (WorkspaceId): -* Bearer Authentication (BearerAuth): - -```python -import hotdata -from hotdata.models.list_uploads_response import ListUploadsResponse -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) - status = 'status_example' # str | Filter by upload status (optional) - - try: - # List uploads - api_response = api_instance.list_uploads(status=status) - print("The response of UploadsApi->list_uploads:\n") - pprint(api_response) - except Exception as e: - print("Exception when calling UploadsApi->list_uploads: %s\n" % e) -``` - - - -### Parameters - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **status** | **str**| Filter by upload status | [optional] - -### Return type - -[**ListUploadsResponse**](ListUploadsResponse.md) - -### Authorization - -[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) - -### HTTP request headers - - - **Content-Type**: Not defined - - **Accept**: application/json - -### HTTP response details - -| Status code | Description | Response headers | -|-------------|-------------|------------------| -**200** | List of uploads | - | - -[[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) @@ -462,89 +377,3 @@ 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) -# **upload_file** -> UploadResponse upload_file(body) - -Upload file - -Upload a Parquet file to publish as the contents of a managed table. Send the raw file bytes as the request body with an appropriate Content-Type header (e.g., `application/parquet`). The body is streamed to disk, so files up to 20GB are supported. The returned upload ID can be passed to the managed-table load endpoint. - -### Example - -* Api Key Authentication (WorkspaceId): -* Bearer Authentication (BearerAuth): - -```python -import hotdata -from hotdata.models.upload_response import UploadResponse -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) - body = None # bytearray | - - try: - # Upload file - api_response = api_instance.upload_file(body) - print("The response of UploadsApi->upload_file:\n") - pprint(api_response) - except Exception as e: - print("Exception when calling UploadsApi->upload_file: %s\n" % e) -``` - - - -### Parameters - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **body** | **bytearray**| | - -### Return type - -[**UploadResponse**](UploadResponse.md) - -### Authorization - -[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) - -### HTTP request headers - - - **Content-Type**: application/octet-stream - - **Accept**: application/json - -### HTTP response details - -| Status code | Description | Response headers | -|-------------|-------------|------------------| -**201** | File uploaded | - | -**400** | Invalid request | - | - -[[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) - diff --git a/hotdata/__init__.py b/hotdata/__init__.py index 45f803f..ef63b2e 100644 --- a/hotdata/__init__.py +++ b/hotdata/__init__.py @@ -128,7 +128,6 @@ "ListSavedQueriesResponse", "ListSavedQueryVersionsResponse", "ListSecretsResponse", - "ListUploadsResponse", "ListWorkspacesResponse", "LoadManagedTableRequest", "LoadManagedTableResponse", @@ -163,8 +162,6 @@ "UpdateSavedQueryRequest", "UpdateSecretRequest", "UpdateSecretResponse", - "UploadInfo", - "UploadResponse", "UploadSessionResponse", "UpsertDatabaseContextRequest", "UpsertDatabaseContextResponse", @@ -282,7 +279,6 @@ from hotdata.models.list_saved_queries_response import ListSavedQueriesResponse as ListSavedQueriesResponse from hotdata.models.list_saved_query_versions_response import ListSavedQueryVersionsResponse as ListSavedQueryVersionsResponse from hotdata.models.list_secrets_response import ListSecretsResponse as ListSecretsResponse -from hotdata.models.list_uploads_response import ListUploadsResponse as ListUploadsResponse from hotdata.models.list_workspaces_response import ListWorkspacesResponse as ListWorkspacesResponse from hotdata.models.load_managed_table_request import LoadManagedTableRequest as LoadManagedTableRequest from hotdata.models.load_managed_table_response import LoadManagedTableResponse as LoadManagedTableResponse @@ -317,8 +313,6 @@ from hotdata.models.update_saved_query_request import UpdateSavedQueryRequest as UpdateSavedQueryRequest from hotdata.models.update_secret_request import UpdateSecretRequest as UpdateSecretRequest from hotdata.models.update_secret_response import UpdateSecretResponse as UpdateSecretResponse -from hotdata.models.upload_info import UploadInfo as UploadInfo -from hotdata.models.upload_response import UploadResponse as UploadResponse from hotdata.models.upload_session_response import UploadSessionResponse as UploadSessionResponse from hotdata.models.upsert_database_context_request import UpsertDatabaseContextRequest as UpsertDatabaseContextRequest from hotdata.models.upsert_database_context_response import UpsertDatabaseContextResponse as UpsertDatabaseContextResponse diff --git a/hotdata/api/uploads_api.py b/hotdata/api/uploads_api.py index bddf5b6..5925b8b 100644 --- a/hotdata/api/uploads_api.py +++ b/hotdata/api/uploads_api.py @@ -16,18 +16,16 @@ from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Annotated -from pydantic import Field, StrictBytes, StrictStr -from typing import Optional, Tuple, Union +from pydantic import Field, StrictStr +from typing import Optional from typing_extensions import Annotated from hotdata.models.batch_create_upload_request import BatchCreateUploadRequest from hotdata.models.batch_create_upload_response import BatchCreateUploadResponse from hotdata.models.create_upload_request import CreateUploadRequest 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 from hotdata.api_client import ApiClient, RequestSerialized @@ -67,7 +65,7 @@ def create_upload_session_handler( ) -> UploadSessionResponse: """Create upload session - 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. + 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 (for example a local-filesystem development backend). :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -136,7 +134,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. 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. + 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 (for example a local-filesystem development backend). :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -205,7 +203,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. 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. + 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 (for example a local-filesystem development backend). :param create_upload_request: (required) :type create_upload_request: CreateUploadRequest @@ -348,7 +346,7 @@ def create_upload_sessions_batch_handler( ) -> BatchCreateUploadResponse: """Create upload sessions in bulk - Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send each file to the `POST /v1/files` endpoint instead. + Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs (for example a local-filesystem development backend). :param batch_create_upload_request: (required) :type batch_create_upload_request: BatchCreateUploadRequest @@ -417,7 +415,7 @@ def create_upload_sessions_batch_handler_with_http_info( ) -> ApiResponse[BatchCreateUploadResponse]: """Create upload sessions in bulk - Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send each file to the `POST /v1/files` endpoint instead. + Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs (for example a local-filesystem development backend). :param batch_create_upload_request: (required) :type batch_create_upload_request: BatchCreateUploadRequest @@ -486,7 +484,7 @@ def create_upload_sessions_batch_handler_without_preload_content( ) -> RESTResponseType: """Create upload sessions in bulk - Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs; send each file to the `POST /v1/files` endpoint instead. + Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. If the response status is `501` with error code `PRESIGN_UNSUPPORTED`, the configured storage backend cannot issue upload URLs (for example a local-filesystem development backend). :param batch_create_upload_request: (required) :type batch_create_upload_request: BatchCreateUploadRequest @@ -921,267 +919,6 @@ def _finalize_upload_handler_serialize( - @validate_call - def list_uploads( - self, - status: Annotated[Optional[StrictStr], Field(description="Filter by upload status")] = None, - _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, - ) -> ListUploadsResponse: - """List uploads - - - :param status: Filter by upload status - :type status: str - :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._list_uploads_serialize( - status=status, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "ListUploadsResponse", - } - 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 list_uploads_with_http_info( - self, - status: Annotated[Optional[StrictStr], Field(description="Filter by upload status")] = None, - _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[ListUploadsResponse]: - """List uploads - - - :param status: Filter by upload status - :type status: str - :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._list_uploads_serialize( - status=status, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "ListUploadsResponse", - } - 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 list_uploads_without_preload_content( - self, - status: Annotated[Optional[StrictStr], Field(description="Filter by upload status")] = None, - _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: - """List uploads - - - :param status: Filter by upload status - :type status: str - :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._list_uploads_serialize( - status=status, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "ListUploadsResponse", - } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) - return response_data.response - - - def _list_uploads_serialize( - self, - status, - _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 - # process the query parameters - if status is not None: - - _query_params.append(('status', status)) - - # process the header parameters - # process the form parameters - # process the body parameter - - - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) - - - # authentication setting - _auth_settings: List[str] = [ - 'WorkspaceId', - 'BearerAuth' - ] - - return self.api_client.param_serialize( - method='GET', - resource_path='/v1/files', - 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 mint_upload_parts_handler( self, @@ -1494,289 +1231,3 @@ def _mint_upload_parts_handler_serialize( ) - - - @validate_call - def upload_file( - self, - body: Union[StrictBytes, StrictStr, Tuple[StrictStr, StrictBytes]], - _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, - ) -> UploadResponse: - """Upload file - - Upload a Parquet file to publish as the contents of a managed table. Send the raw file bytes as the request body with an appropriate Content-Type header (e.g., `application/parquet`). The body is streamed to disk, so files up to 20GB are supported. The returned upload ID can be passed to the managed-table load endpoint. - - :param body: (required) - :type body: bytearray - :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._upload_file_serialize( - body=body, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '201': "UploadResponse", - '400': "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 upload_file_with_http_info( - self, - body: Union[StrictBytes, StrictStr, Tuple[StrictStr, StrictBytes]], - _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[UploadResponse]: - """Upload file - - Upload a Parquet file to publish as the contents of a managed table. Send the raw file bytes as the request body with an appropriate Content-Type header (e.g., `application/parquet`). The body is streamed to disk, so files up to 20GB are supported. The returned upload ID can be passed to the managed-table load endpoint. - - :param body: (required) - :type body: bytearray - :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._upload_file_serialize( - body=body, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '201': "UploadResponse", - '400': "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 upload_file_without_preload_content( - self, - body: Union[StrictBytes, StrictStr, Tuple[StrictStr, StrictBytes]], - _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: - """Upload file - - Upload a Parquet file to publish as the contents of a managed table. Send the raw file bytes as the request body with an appropriate Content-Type header (e.g., `application/parquet`). The body is streamed to disk, so files up to 20GB are supported. The returned upload ID can be passed to the managed-table load endpoint. - - :param body: (required) - :type body: bytearray - :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._upload_file_serialize( - body=body, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '201': "UploadResponse", - '400': "ApiErrorResponse", - } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) - return response_data.response - - - def _upload_file_serialize( - self, - body, - _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 - # process the query parameters - # process the header parameters - # process the form parameters - # process the body parameter - if body is not None: - # convert to byte array if the input is a file name (str) - if isinstance(body, str): - with open(body, "rb") as _fp: - _body_params = _fp.read() - elif isinstance(body, tuple): - # drop the filename from the tuple - _body_params = body[1] - else: - _body_params = body - - - # 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/octet-stream' - ] - ) - ) - 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/files', - 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 - ) - - diff --git a/hotdata/models/__init__.py b/hotdata/models/__init__.py index 859c105..9619e4b 100644 --- a/hotdata/models/__init__.py +++ b/hotdata/models/__init__.py @@ -92,7 +92,6 @@ from hotdata.models.list_saved_queries_response import ListSavedQueriesResponse from hotdata.models.list_saved_query_versions_response import ListSavedQueryVersionsResponse from hotdata.models.list_secrets_response import ListSecretsResponse -from hotdata.models.list_uploads_response import ListUploadsResponse from hotdata.models.list_workspaces_response import ListWorkspacesResponse from hotdata.models.load_managed_table_request import LoadManagedTableRequest from hotdata.models.load_managed_table_response import LoadManagedTableResponse @@ -127,8 +126,6 @@ from hotdata.models.update_saved_query_request import UpdateSavedQueryRequest from hotdata.models.update_secret_request import UpdateSecretRequest from hotdata.models.update_secret_response import UpdateSecretResponse -from hotdata.models.upload_info import UploadInfo -from hotdata.models.upload_response import UploadResponse from hotdata.models.upload_session_response import UploadSessionResponse from hotdata.models.upsert_database_context_request import UpsertDatabaseContextRequest from hotdata.models.upsert_database_context_response import UpsertDatabaseContextResponse diff --git a/hotdata/models/list_uploads_response.py b/hotdata/models/list_uploads_response.py deleted file mode 100644 index 05b3032..0000000 --- a/hotdata/models/list_uploads_response.py +++ /dev/null @@ -1,96 +0,0 @@ -# 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 -from typing import Any, ClassVar, Dict, List -from hotdata.models.upload_info import UploadInfo -from typing import Optional, Set -from typing_extensions import Self - -class ListUploadsResponse(BaseModel): - """ - Response body for GET /v1/files - """ # noqa: E501 - uploads: List[UploadInfo] - __properties: ClassVar[List[str]] = ["uploads"] - - 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 ListUploadsResponse 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 uploads (list) - _items = [] - if self.uploads: - for _item_uploads in self.uploads: - if _item_uploads: - _items.append(_item_uploads.to_dict()) - _dict['uploads'] = _items - return _dict - - @classmethod - def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: - """Create an instance of ListUploadsResponse from a dict""" - if obj is None: - return None - - if not isinstance(obj, dict): - return cls.model_validate(obj) - - _obj = cls.model_validate({ - "uploads": [UploadInfo.from_dict(_item) for _item in obj["uploads"]] if obj.get("uploads") is not None else None - }) - return _obj - - diff --git a/hotdata/models/load_managed_table_request.py b/hotdata/models/load_managed_table_request.py index a4ca859..4a88ab9 100644 --- a/hotdata/models/load_managed_table_request.py +++ b/hotdata/models/load_managed_table_request.py @@ -34,7 +34,7 @@ class LoadManagedTableRequest(BaseModel): key: Optional[List[StrictStr]] = Field(default=None, description="Key columns identifying rows for `\"delete\"`, `\"update\"`, and `\"upsert\"` loads — the columns whose values decide which existing row an incoming row removes, updates, or replaces. Omit to use the key the table was created with. Keep the key consistent across loads of the same table: changing it re-targets which rows are matched. Ignored for `\"replace\"` and `\"append\"`.") mode: StrictStr = Field(description="How the data is applied: `\"replace\"` overwrites the table's contents, `\"append\"` inserts the new rows on top of the existing data.") result_id: Optional[StrictStr] = Field(default=None, description="ID of a persisted query result (see `GET /v1/results/{result_id}`) to publish as the table's contents. The result is copied into the table, so the table keeps its data even after the result expires. A result can be loaded into any number of tables. Provide either this or `upload_id`, not both.") - upload_id: Optional[StrictStr] = Field(default=None, description="ID of a previously-staged upload (see `POST /v1/files`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. Provide either this or `result_id`, not both.") + upload_id: Optional[StrictStr] = Field(default=None, description="ID of a previously-staged upload (see `POST /v1/uploads`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. Provide either this or `result_id`, not both.") __properties: ClassVar[List[str]] = ["async", "async_after_ms", "format", "key", "mode", "result_id", "upload_id"] model_config = ConfigDict( diff --git a/hotdata/models/upload_info.py b/hotdata/models/upload_info.py deleted file mode 100644 index 68cce28..0000000 --- a/hotdata/models/upload_info.py +++ /dev/null @@ -1,102 +0,0 @@ -# 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 datetime import datetime -from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr -from typing import Any, ClassVar, Dict, List, Optional -from typing import Optional, Set -from typing_extensions import Self - -class UploadInfo(BaseModel): - """ - Single upload info for listing - """ # noqa: E501 - content_type: Optional[StrictStr] = None - created_at: datetime - id: StrictStr - size_bytes: StrictInt - status: StrictStr - __properties: ClassVar[List[str]] = ["content_type", "created_at", "id", "size_bytes", "status"] - - 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 UploadInfo 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, - ) - # set to None if content_type (nullable) is None - # and model_fields_set contains the field - if self.content_type is None and "content_type" in self.model_fields_set: - _dict['content_type'] = None - - return _dict - - @classmethod - def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: - """Create an instance of UploadInfo from a dict""" - if obj is None: - return None - - if not isinstance(obj, dict): - return cls.model_validate(obj) - - _obj = cls.model_validate({ - "content_type": obj.get("content_type"), - "created_at": obj.get("created_at"), - "id": obj.get("id"), - "size_bytes": obj.get("size_bytes"), - "status": obj.get("status") - }) - return _obj - - diff --git a/hotdata/models/upload_response.py b/hotdata/models/upload_response.py deleted file mode 100644 index 43f30fd..0000000 --- a/hotdata/models/upload_response.py +++ /dev/null @@ -1,102 +0,0 @@ -# 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 datetime import datetime -from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr -from typing import Any, ClassVar, Dict, List, Optional -from typing import Optional, Set -from typing_extensions import Self - -class UploadResponse(BaseModel): - """ - Response body for POST /v1/files - """ # noqa: E501 - content_type: Optional[StrictStr] = None - created_at: datetime - id: StrictStr - size_bytes: StrictInt - status: StrictStr - __properties: ClassVar[List[str]] = ["content_type", "created_at", "id", "size_bytes", "status"] - - 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 UploadResponse 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, - ) - # set to None if content_type (nullable) is None - # and model_fields_set contains the field - if self.content_type is None and "content_type" in self.model_fields_set: - _dict['content_type'] = None - - return _dict - - @classmethod - def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: - """Create an instance of UploadResponse from a dict""" - if obj is None: - return None - - if not isinstance(obj, dict): - return cls.model_validate(obj) - - _obj = cls.model_validate({ - "content_type": obj.get("content_type"), - "created_at": obj.get("created_at"), - "id": obj.get("id"), - "size_bytes": obj.get("size_bytes"), - "status": obj.get("status") - }) - return _obj - - diff --git a/test/test_list_uploads_response.py b/test/test_list_uploads_response.py deleted file mode 100644 index 8e3847a..0000000 --- a/test/test_list_uploads_response.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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.list_uploads_response import ListUploadsResponse - -class TestListUploadsResponse(unittest.TestCase): - """ListUploadsResponse unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> ListUploadsResponse: - """Test ListUploadsResponse - 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 `ListUploadsResponse` - """ - model = ListUploadsResponse() - if include_optional: - return ListUploadsResponse( - uploads = [ - hotdata.models.upload_info.UploadInfo( - content_type = '', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '', ) - ] - ) - else: - return ListUploadsResponse( - uploads = [ - hotdata.models.upload_info.UploadInfo( - content_type = '', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '', ) - ], - ) - """ - - def testListUploadsResponse(self): - """Test ListUploadsResponse""" - # 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_upload_info.py b/test/test_upload_info.py deleted file mode 100644 index aab7cfd..0000000 --- a/test/test_upload_info.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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.upload_info import UploadInfo - -class TestUploadInfo(unittest.TestCase): - """UploadInfo unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> UploadInfo: - """Test UploadInfo - 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 `UploadInfo` - """ - model = UploadInfo() - if include_optional: - return UploadInfo( - content_type = '', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '' - ) - else: - return UploadInfo( - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '', - ) - """ - - def testUploadInfo(self): - """Test UploadInfo""" - # 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_upload_response.py b/test/test_upload_response.py deleted file mode 100644 index 9d24c1d..0000000 --- a/test/test_upload_response.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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.upload_response import UploadResponse - -class TestUploadResponse(unittest.TestCase): - """UploadResponse unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass - - def make_instance(self, include_optional) -> UploadResponse: - """Test UploadResponse - 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 `UploadResponse` - """ - model = UploadResponse() - if include_optional: - return UploadResponse( - content_type = '', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '' - ) - else: - return UploadResponse( - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - id = '', - size_bytes = 56, - status = '', - ) - """ - - def testUploadResponse(self): - """Test UploadResponse""" - # 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 7c945c5..b645f69 100644 --- a/test/test_uploads_api.py +++ b/test/test_uploads_api.py @@ -48,13 +48,6 @@ def test_finalize_upload_handler(self) -> None: """ pass - def test_list_uploads(self) -> None: - """Test case for list_uploads - - List uploads - """ - pass - def test_mint_upload_parts_handler(self) -> None: """Test case for mint_upload_parts_handler @@ -62,13 +55,6 @@ def test_mint_upload_parts_handler(self) -> None: """ pass - def test_upload_file(self) -> None: - """Test case for upload_file - - Upload file - """ - pass - if __name__ == '__main__': unittest.main() From 82f7d770bc5122bf441f6141aaff8742c136e60c Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Tue, 21 Jul 2026 09:52:52 -0700 Subject: [PATCH 2/2] chore(uploads): drop upload_stream and files-endpoint refs --- CHANGELOG.md | 8 +- README.md | 14 -- hotdata/uploads.py | 147 ++--------------- tests/integration/conftest.py | 2 +- .../test_managed_tables_lifecycle.py | 8 +- tests/integration/test_uploads_crud.py | 24 --- tests/test_uploads.py | 155 +----------------- 7 files changed, 24 insertions(+), 334 deletions(-) delete mode 100644 tests/integration/test_uploads_crud.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f27f08..e0e4829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed +### Removed -- chore: remove files endpoints in favor of uploads +- The legacy `POST /v1/files` upload endpoints have been removed in favor of the + presigned upload flow. This drops `hotdata.UploadsApi.upload_stream`, the raw + `upload_file(body=...)` / `list_uploads` operations, and the `UploadResponse`, + `UploadInfo`, and `ListUploadsResponse` models. Use + `hotdata.UploadsApi.upload_file(source, ...)` (direct-to-storage) instead. ## [0.8.0] - 2026-07-20 diff --git a/README.md b/README.md index 5eb7e8b..29ab4cc 100644 --- a/README.md +++ b/README.md @@ -156,20 +156,6 @@ Pass a `threading.Event` as `cancel_event` to abort an in-flight upload; tune th control-plane calls with `request_timeout` (storage-PUT timeouts are automatic and size-scaled). -For that fallback (or to upload from a non-seekable stream), use `upload_stream`, -which sends the bytes to the legacy `POST /v1/files` endpoint in one request, -streaming a file object without buffering it in memory: - -```python -with open("data.parquet", "rb") as f: - resp = uploads.upload_stream(f, content_type="application/parquet") -print(resp.id) -``` - -Note `upload_file` shadows the generated raw-body `upload_file(body=...)`; that -raw operation is still reachable at -`hotdata.api.uploads_api.UploadsApi.upload_file`. - ## API reference Generated Markdown for every operation and model is in [`docs/`](https://github.com/hotdata-dev/sdk-python/tree/main/docs): diff --git a/hotdata/uploads.py b/hotdata/uploads.py index f05565e..a9cf639 100644 --- a/hotdata/uploads.py +++ b/hotdata/uploads.py @@ -70,16 +70,7 @@ from hotdata.models.finalize_upload_request import FinalizeUploadRequest from hotdata.models.finalize_upload_response import FinalizeUploadResponse from hotdata.models.mint_upload_parts_request import MintUploadPartsRequest -from hotdata.models.upload_response import UploadResponse from hotdata.models.upload_session_response import UploadSessionResponse -from hotdata.rest import RESTResponse - -#: Response-type map for the legacy ``POST /v1/files`` op, mirroring the -#: generated ``upload_file`` handler. -_UPLOAD_FILE_RESPONSE_TYPES: Dict[str, Optional[str]] = { - "201": "UploadResponse", - "400": "ApiErrorResponse", -} #: The accepted input types for :meth:`UploadsApi.upload_file`: a filesystem #: path, raw bytes, or a seekable binary file object. @@ -356,8 +347,9 @@ def __init__(self, api_exception: ApiException) -> None: class SessionCreateError(_ApiPhaseError): """Opening the upload session (``POST /v1/uploads``) failed. A ``501`` - ``PRESIGN_UNSUPPORTED`` (``status == 501``) means the backend cannot issue - presigned URLs — fall back to :meth:`UploadsApi.upload_stream`. + ``PRESIGN_UNSUPPORTED`` (``status == 501``) means the configured storage + backend cannot issue presigned upload URLs (e.g. a local-filesystem + development backend). """ _phase = "opening the upload session" @@ -805,20 +797,6 @@ def default_part_retry() -> Retry: return retry -def _to_timeout(request_timeout: Any) -> Any: - """Convert the SDK's ``_request_timeout`` (a number or ``(connect, read)`` - tuple) into a :class:`urllib3.Timeout`, or ``None`` for the pool default — - matching the generated REST client's conversion. - """ - if not request_timeout: - return None - if isinstance(request_timeout, (int, float)): - return urllib3.Timeout(total=request_timeout) - if isinstance(request_timeout, tuple) and len(request_timeout) == 2: - return urllib3.Timeout(connect=request_timeout[0], read=request_timeout[1]) - return None - - def _storage_headers(session_headers: Dict[str, str], content_length: int) -> Dict[str, str]: """Build the bare PUT headers: an explicit ``Content-Length`` plus the server-provided session headers replayed verbatim (currently always empty; @@ -948,8 +926,8 @@ def upload_file( # type: ignore[override] bytes never round-trip through the API. ``source`` may be a filesystem path, raw ``bytes`` / ``bytearray``, or a - seekable binary file object. A non-seekable stream is not accepted here - (multipart needs positioned reads) — use :meth:`upload_stream` for that. + seekable binary file object. A non-seekable stream is not accepted + (multipart needs positioned reads) — buffer it into ``bytes`` first. A file object is uploaded from its current position to the end (its cursor is consumed, not restored); pass ``size`` only if its length cannot be inferred by seeking. @@ -990,8 +968,9 @@ def upload_file( # type: ignore[override] a local read failure): :raises SessionCreateError: opening the session failed (e.g. a ``501`` - ``PRESIGN_UNSUPPORTED`` — check ``.status``; fall back to - :meth:`upload_stream`). Wraps the ``ApiException`` as ``__cause__``. + ``PRESIGN_UNSUPPORTED`` — the storage backend cannot issue presigned + upload URLs; check ``.status``). Wraps the ``ApiException`` as + ``__cause__``. :raises SizeLimitError: the part-size hint exceeds the wire's 64-bit field. :raises MalformedSessionError: the session response was inconsistent. :raises StorageError: a storage ``PUT`` returned a non-2xx status. Its @@ -1130,105 +1109,6 @@ def upload_file( # type: ignore[override] timeout, ) - def upload_stream( - self, - body: Union[bytes, bytearray, BinaryIO], - *, - content_type: Optional[str] = None, - content_length: Optional[int] = None, - _request_timeout: Any = None, - ) -> UploadResponse: - """Stream an arbitrary byte source to the legacy ``POST /v1/files`` raw - upload endpoint, returning the - :class:`~hotdata.models.upload_response.UploadResponse`. - - Use this when the presigned :meth:`upload_file` path is unavailable - (e.g. a ``501`` ``PRESIGN_UNSUPPORTED``) or when the bytes come from a - non-seekable stream that :meth:`upload_file` cannot use. The body is sent - through the SDK's authenticated client (workspace + bearer headers) — not - the bare storage pool — because this request goes to the API, not object - storage. - - ``body`` may be ``bytes`` / ``bytearray`` or a readable binary file - object. A file object is streamed without being buffered into memory; set - ``content_length`` (or pass a seekable file, whose length is inferred) so - the server can reject an oversized upload before reading the body. - - :param body: The bytes or binary stream to upload. - :param content_type: Content type for the body; defaults to - ``application/octet-stream``. - :param content_length: Explicit body length in bytes. Inferred from - ``bytes`` length or a seekable file; required for a non-seekable - stream to avoid chunked transfer. - :raises hotdata.exceptions.ApiException: the upload was rejected. - """ - if not isinstance(body, (bytes, bytearray)) and not hasattr(body, "read"): - raise TypeError( - f"upload_stream accepts bytes or a readable binary file object, " - f"not {type(body).__name__}" - ) - - # The generated /v1/files op carries only WorkspaceId + BearerAuth; add - # the X-Session-Id scope header here when a session is configured so - # sandbox-scoped uploads keep their session context (matches the Rust SDK). - scope_headers: Dict[str, str] = {} - session_id = self.api_client.configuration.session_id - if session_id: - scope_headers["X-Session-Id"] = session_id - - if isinstance(body, (bytes, bytearray)): - # bytes go through the generated serialize + transport unchanged; - # urllib3 sets Content-Length from the buffer length automatically. - data = bytes(body) - params = self._upload_file_serialize( - body=data, - _request_auth=None, - _content_type=content_type, - _headers=scope_headers or None, - _host_index=0, - ) - response_data = self.api_client.call_api(*params, _request_timeout=_request_timeout) - response_data.read() - return self.api_client.response_deserialize( - response_data=response_data, - response_types_map=_UPLOAD_FILE_RESPONSE_TYPES, - ).data - - # A file object: infer its length when seekable so the request is framed - # (not chunked), then stream it through the SDK's authenticated pool. - if content_length is None: - seekable = getattr(body, "seekable", None) - if callable(seekable) and body.seekable(): - current = body.tell() - content_length = body.seek(0, io.SEEK_END) - current - body.seek(current) - headers: Dict[str, str] = dict(scope_headers) - if content_length is not None: - headers["Content-Length"] = str(content_length) - method, url, header_params, _body, _post = self._upload_file_serialize( - body=None, - _request_auth=None, - _content_type=content_type, - _headers=headers or None, - _host_index=0, - ) - # Stream via the SDK's configured pool (auth + TLS/proxy), bypassing the - # generated rest layer, which buffers and rejects file-like bodies. - raw = self.api_client.rest_client.pool_manager.request( - method, - url, - body=body, - headers=header_params, - preload_content=False, - timeout=_to_timeout(_request_timeout), - ) - rest_response = RESTResponse(raw) - rest_response.read() - return self.api_client.response_deserialize( - response_data=rest_response, - response_types_map=_UPLOAD_FILE_RESPONSE_TYPES, - ).data - # -- internals --------------------------------------------------------- def _create_session( @@ -1633,8 +1513,7 @@ def _make_source(source: UploadSource, size: Optional[int]) -> _Source: """Normalize an upload input into a :class:`_Source`. Accepts a path, ``bytes`` / ``bytearray``, or a seekable binary file object. - A non-seekable stream (or any other type) raises ``TypeError`` pointing at - :meth:`UploadsApi.upload_stream`, which streams to the legacy endpoint. + A non-seekable stream (or any other type) raises ``TypeError``. """ if isinstance(source, (bytes, bytearray)): return _BytesSource(bytes(source)) @@ -1645,15 +1524,13 @@ def _make_source(source: UploadSource, size: Optional[int]) -> _Source: if not (callable(seekable) and source.seekable()): raise TypeError( "upload_file needs a seekable file object (it does positioned " - "reads for multipart). For a non-seekable stream use " - "upload_stream, which sends to POST /v1/files in one request." + "reads for multipart). Buffer a non-seekable stream into bytes " + "first." ) return _FileObjSource(source, size) raise TypeError( f"upload_file accepts a path, bytes, or a seekable binary file object, " - f"not {type(source).__name__}. (Did you mean the generated " - f"hotdata.api.uploads_api.UploadsApi.upload_file(body=...) raw op, or " - f"upload_stream?)" + f"not {type(source).__name__}." ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 981bde8..edf7d2f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -25,10 +25,10 @@ from hotdata.api.refresh_api import RefreshApi from hotdata.api.saved_queries_api import SavedQueriesApi from hotdata.api.secrets_api import SecretsApi -from hotdata.api.uploads_api import UploadsApi from hotdata.api.workspaces_api import WorkspacesApi from hotdata.exceptions import ApiException from hotdata.models.create_database_request import CreateDatabaseRequest +from hotdata.uploads import UploadsApi REQUIRED_ENV = ("HOTDATA_SDK_TEST_API_KEY", "HOTDATA_SDK_TEST_WORKSPACE_ID") diff --git a/tests/integration/test_managed_tables_lifecycle.py b/tests/integration/test_managed_tables_lifecycle.py index 4a5cb16..7215e68 100644 --- a/tests/integration/test_managed_tables_lifecycle.py +++ b/tests/integration/test_managed_tables_lifecycle.py @@ -36,10 +36,10 @@ from hotdata.api.connections_api import ConnectionsApi from hotdata.api.databases_api import DatabasesApi -from hotdata.api.uploads_api import UploadsApi from hotdata.models.add_managed_schema_request import AddManagedSchemaRequest from hotdata.models.add_managed_table_request import AddManagedTableRequest from hotdata.models.load_managed_table_request import LoadManagedTableRequest +from hotdata.uploads import UploadsApi def _parquet_bytes() -> bytes: @@ -69,15 +69,15 @@ def test_managed_tables_lifecycle( ) upload = uploads_api.upload_file( - body=_parquet_bytes(), _content_type="application/parquet" + _parquet_bytes(), content_type="application/parquet" ) - assert upload.id + assert upload.upload_id loaded = connections_api.load_managed_table( connection_id, schema_name, table_name, - LoadManagedTableRequest(mode="replace", upload_id=upload.id), + LoadManagedTableRequest(mode="replace", upload_id=upload.upload_id), ) assert loaded.schema_name == schema_name assert loaded.table_name == table_name diff --git a/tests/integration/test_uploads_crud.py b/tests/integration/test_uploads_crud.py deleted file mode 100644 index 770a37c..0000000 --- a/tests/integration/test_uploads_crud.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Scenario: uploads_crud. - -Upload a small file via upload_file, then confirm it appears in list_uploads. -There is no delete-upload endpoint, so the upload is not torn down in-test — -orphaned `sdkci` uploads are reclaimed by the nightly sweep. UploadInfo carries -no name field, so we identify the upload by the id returned from upload_file. -""" - -from __future__ import annotations - -from hotdata.api.uploads_api import UploadsApi - - -def test_uploads_crud(uploads_api: UploadsApi) -> None: - body = b"col_a,col_b\n1,2\n3,4\n" - - uploaded = uploads_api.upload_file(body=body, _content_type="text/csv") - assert uploaded.id - assert uploaded.size_bytes == len(body) - - listing = uploads_api.list_uploads() - match = next((u for u in listing.uploads if u.id == uploaded.id), None) - assert match is not None, f"upload {uploaded.id} not in list_uploads" - assert match.size_bytes == len(body) diff --git a/tests/test_uploads.py b/tests/test_uploads.py index 6e46242..e54c41a 100644 --- a/tests/test_uploads.py +++ b/tests/test_uploads.py @@ -18,7 +18,6 @@ from __future__ import annotations import io -import json import os import threading from typing import Any, Dict, List, Optional, Tuple @@ -50,7 +49,6 @@ effective_in_flight, ) from hotdata.models.finalize_upload_response import FinalizeUploadResponse -from hotdata.models.upload_response import UploadResponse from hotdata.models.upload_session_response import UploadSessionResponse @@ -77,20 +75,6 @@ def __init__(self, status: int, *, etag: Optional[str] = None, body: bytes = b"" self.data = body -class _FakeUrllib3Response: - """Minimal urllib3.HTTPResponse stand-in for the API transport (not storage): - enough for rest.RESTResponse + ApiClient.response_deserialize. - """ - - def __init__(self, status: int, data: bytes, headers: Dict[str, str]): - self.status = status - self.reason = "OK" if 200 <= status < 300 else "Error" - self.data = data - # A plain dict (lowercased keys): ApiResponse validates headers as a dict, - # and response_deserialize looks up 'content-type'. - self.headers = {k.lower(): v for k, v in headers.items()} - - class _FakeStoragePool: """Stand-in for the bare storage urllib3.PoolManager. @@ -1072,7 +1056,7 @@ def seekable(self) -> bool: return False api = _make_api() - with pytest.raises(TypeError, match="upload_stream"): + with pytest.raises(TypeError, match="seekable file object"): api.upload_file(_NonSeekable()) # type: ignore[arg-type] @@ -1082,143 +1066,6 @@ def test_upload_file_bad_type_raises(fake_pool: _FakeStoragePool) -> None: api.upload_file(12345) # type: ignore[arg-type] -# --- upload_stream (legacy POST /v1/files) -------------------------------- - - -def _upload_response_json() -> bytes: - return json.dumps( - { - "content_type": "application/parquet", - "created_at": "2026-01-01T00:00:00Z", - "id": "file_123", - "size_bytes": 5, - "status": "ready", - } - ).encode() - - -def test_upload_stream_bytes_posts_to_v1_files( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("HOTDATA_DISABLE_JWT_EXCHANGE", "1") - from hotdata import rest - - captured: Dict[str, Any] = {} - - def fake_request( - self: Any, - method: str, - url: str, - headers: Any = None, - body: Any = None, - post_params: Any = None, - _request_timeout: Any = None, - ) -> Any: - captured.update(method=method, url=url, body=body, headers=dict(headers or {})) - resp = _FakeUrllib3Response( - 201, _upload_response_json(), {"content-type": "application/json"} - ) - return rest.RESTResponse(resp) - - monkeypatch.setattr(rest.RESTClientObject, "request", fake_request) - api = _make_api() - out = api.upload_stream(b"hello", content_type="application/parquet") - assert captured["method"] == "POST" - assert captured["url"].endswith("/v1/files") - assert captured["body"] == b"hello" - assert captured["headers"]["Content-Type"] == "application/parquet" - assert isinstance(out, UploadResponse) - assert out.id == "file_123" - - -def test_upload_stream_file_object_streams_with_inferred_length( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("HOTDATA_DISABLE_JWT_EXCHANGE", "1") - api = _make_api() - - captured: Dict[str, Any] = {} - - class _FakePoolManager: - def request(self, method: str, url: str, **kwargs: Any) -> Any: - body = kwargs["body"] - captured.update( - method=method, - url=url, - headers=dict(kwargs.get("headers") or {}), - streamed=body.read(), - preload_content=kwargs.get("preload_content"), - ) - return _FakeUrllib3Response( - 201, _upload_response_json(), {"content-type": "application/json"} - ) - - monkeypatch.setattr(api.api_client.rest_client, "pool_manager", _FakePoolManager()) - - payload = b"streamed-from-a-file-object" - out = api.upload_stream(io.BytesIO(payload)) - assert captured["method"] == "POST" - assert captured["url"].endswith("/v1/files") - # Length inferred from the seekable stream -> framed (not chunked). - assert captured["headers"]["Content-Length"] == str(len(payload)) - assert captured["streamed"] == payload - assert captured["preload_content"] is False - # Default content type when unset. - assert captured["headers"]["Content-Type"] == "application/octet-stream" - # The SDK's User-Agent and scope auth ride the streamed request too. - assert "User-Agent" in captured["headers"] - assert captured["headers"]["X-Workspace-Id"] == "ws_test" - assert isinstance(out, UploadResponse) - - -def test_upload_stream_bad_type_raises() -> None: - api = _make_api() - with pytest.raises(TypeError, match="bytes or a readable"): - api.upload_stream(12345) # type: ignore[arg-type] - - -def _make_api_with_session(session_id: str) -> UploadsApi: - from hotdata import ApiClient, Configuration - - config = Configuration( - host="https://api.hotdata.test", - api_key="test-key", - workspace_id="ws_test", - session_id=session_id, - ) - return UploadsApi(ApiClient(config)) - - -def test_upload_stream_sends_session_and_workspace_scope( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("HOTDATA_DISABLE_JWT_EXCHANGE", "1") - from hotdata import rest - - captured: Dict[str, Any] = {} - - def fake_request( - self: Any, - method: str, - url: str, - headers: Any = None, - body: Any = None, - post_params: Any = None, - _request_timeout: Any = None, - ) -> Any: - captured["headers"] = dict(headers or {}) - resp = _FakeUrllib3Response( - 201, _upload_response_json(), {"content-type": "application/json"} - ) - return rest.RESTResponse(resp) - - monkeypatch.setattr(rest.RESTClientObject, "request", fake_request) - api = _make_api_with_session("sb_xyz") - api.upload_stream(b"hi") - assert captured["headers"]["X-Session-Id"] == "sb_xyz" - assert captured["headers"]["X-Workspace-Id"] == "ws_test" - - # --- Finalize hardening ----------------------------------------------------