From c90ad3ff6a66d1c46c6edfc57096b7d8af1843ce Mon Sep 17 00:00:00 2001 From: Leonardo Santiago Date: Tue, 9 Dec 2025 14:05:31 -0300 Subject: [PATCH 1/3] fix(storage): add query parameters option to download --- src/storage/src/storage3/_async/file_api.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/storage/src/storage3/_async/file_api.py b/src/storage/src/storage3/_async/file_api.py index 9d2b314b..6b43271f 100644 --- a/src/storage/src/storage3/_async/file_api.py +++ b/src/storage/src/storage3/_async/file_api.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from io import BufferedReader, FileIO from pathlib import Path -from typing import Any, List, Literal, Optional, Union, cast +from typing import Any, Dict, List, Literal, Optional, Union, cast from httpx import AsyncClient, Headers, HTTPStatusError, Response from yarl import URL @@ -439,7 +439,10 @@ async def list( return response.json() async def download( - self, path: str, options: Optional[DownloadOptions] = None + self, + path: str, + options: Optional[DownloadOptions] = None, + query_params: Optional[Dict[str, str]] = None, ) -> bytes: """ Downloads a file. @@ -449,20 +452,20 @@ async def download( path The file path to be downloaded, including the path and file name. For example `folder/image.png`. """ - url_options = options or {} + url_options = options or DownloadOptions() render_path = ( ["render", "image", "authenticated"] if url_options.get("transform") else ["object"] ) - transform_options = url_options.get("transform") or {} + transform_options = url_options.get("transform") or TransformOptions() path_parts = relative_path_to_parts(path) response = await self._request( "GET", [*render_path, self.id, *path_parts], - query_params=transform_to_dict(transform_options), + query_params={**transform_to_dict(transform_options), **query_params}, ) return response.content From 02e930c9b1e86da432d8149bfea616b7db52244f Mon Sep 17 00:00:00 2001 From: Leonardo Santiago Date: Tue, 9 Dec 2025 14:46:28 -0300 Subject: [PATCH 2/3] fix(storage): add default argument when query_parameters aren't passed in --- src/storage/src/storage3/_async/file_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/storage/src/storage3/_async/file_api.py b/src/storage/src/storage3/_async/file_api.py index 6b43271f..289c20b0 100644 --- a/src/storage/src/storage3/_async/file_api.py +++ b/src/storage/src/storage3/_async/file_api.py @@ -465,7 +465,10 @@ async def download( response = await self._request( "GET", [*render_path, self.id, *path_parts], - query_params={**transform_to_dict(transform_options), **query_params}, + query_params={ + **transform_to_dict(transform_options), + **(query_params or {}), + }, ) return response.content From f17d974df6112775a85e53af7ff6866dfbf8eef4 Mon Sep 17 00:00:00 2001 From: Leonardo Santiago Date: Tue, 9 Dec 2025 16:55:29 -0300 Subject: [PATCH 3/3] chore(storage): add tests ensuring that download calls with query params work --- src/storage/tests/_async/test_client.py | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/storage/tests/_async/test_client.py b/src/storage/tests/_async/test_client.py index 6d934f40..9d178750 100644 --- a/src/storage/tests/_async/test_client.py +++ b/src/storage/tests/_async/test_client.py @@ -283,6 +283,45 @@ async def test_client_upload( assert image_info.get("metadata", {}).get("mimetype") == file.mime_type +async def test_client_upload_with_query( + storage_file_client: AsyncBucketProxy, file: FileForTesting +) -> None: + """Ensure we can upload files to a bucket, even with query parameters""" + await storage_file_client.upload( + file.bucket_path, file.local_path, {"content-type": file.mime_type} + ) + + image = await storage_file_client.download( + file.bucket_path, query_params={"my-param": "test"} + ) + files = await storage_file_client.list(file.bucket_folder) + image_info = next((f for f in files if f.get("name") == file.name), None) + + assert image == file.file_content + assert image_info is not None + assert image_info.get("metadata", {}).get("mimetype") == file.mime_type + + +async def test_client_download_with_query_doesnt_lose_params( + storage_file_client: AsyncBucketProxy, file: FileForTesting +) -> None: + """Ensure query params aren't lost""" + from yarl import URL + + params = {"my-param": "test"} + mock_response = Mock() + with patch.object(HttpxClient, "request") as mock_request: + mock_request.return_value = mock_response + await storage_file_client.download(file.bucket_path, query_params=params) + expected_url = storage_file_client._base_url.joinpath( + "object", storage_file_client.id, *URL(file.bucket_path).parts + ).with_query(params) + actual_url = mock_request.call_args[0][1] + + assert URL(actual_url).query == params + assert str(expected_url) == actual_url + + async def test_client_update( storage_file_client: AsyncBucketProxy, two_files: list[FileForTesting],