Skip to content

Commit

Permalink
feat: add transform options to signed_url,download, and public_url
Browse files Browse the repository at this point in the history
  • Loading branch information
[email protected] committed Dec 13, 2022
1 parent 7352f61 commit 122b2a3
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 48 deletions.
57 changes: 35 additions & 22 deletions storage3/_async/file_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
from httpx import HTTPError, Response

from ..constants import DEFAULT_FILE_OPTIONS, DEFAULT_SEARCH_OPTIONS
from ..types import BaseBucket, ListBucketFilesOptions, CreateSignedURLOptions, TransformOptions, RequestMethod
from ..types import (
BaseBucket,
ListBucketFilesOptions,
CreateSignedURLOptions,
TransformOptions,
RequestMethod,
)
from ..utils import AsyncClient, StorageException

__all__ = ["AsyncBucket"]
Expand Down Expand Up @@ -45,7 +51,9 @@ async def _request(

return response

async def create_signed_url(self, path: str, expires_in: int, options: CreateSignedURLOptions = {}) -> dict[str, str]:
async def create_signed_url(
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
) -> dict[str, str]:
"""
Parameters
----------
Expand All @@ -66,11 +74,14 @@ async def create_signed_url(self, path: str, expires_in: int, options: CreateSig
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
return data

async def create_signed_urls(self, paths: List[str], expires_in: int, options: dict[str, str]) ->dict[str, str]:
response = await self._request("POST",
f"/object/sign/{self.bucket_id}",json={
"expires_in": expires_in,
"paths": paths})
async def create_signed_urls(
self, paths: List[str], expires_in: int, options: dict[str, str]
) -> dict[str, str]:
response = await self._request(
"POST",
f"/object/sign/{self.bucket_id}",
json={"expires_in": expires_in, "paths": paths},
)
# TODO(joel): add support for download option
return response.json()

Expand All @@ -83,9 +94,11 @@ async def get_public_url(self, path: str, options: TransformOptions = {}) -> str
path
file path, including the path and file name. For example `folder/image.png`.
"""
render_path = 'render/image/authenticated' if options.get('transform') else 'object'
render_path = (
"render/image/authenticated" if options.get("transform") else "object"
)
transformation_query = urllib.parse.urlencode(options)
query_string = f"?{transformation_query}" if transformation_query else ''
query_string = f"?{transformation_query}" if transformation_query else ""
_path = self._get_final_path(path)
return f"{self._client.base_url}{render_path}/public/{_path}/${query_string}"

Expand Down Expand Up @@ -123,17 +136,16 @@ async def copy(self, from_path: str, to_path: str) -> dict[str, str]:
The new file path, including the new file name. For example `folder/image-copy.png`.
"""
res = await self._request(
"POST",
"/object/copy",
json={
"bucketId": self.id,
"sourceKey": from_path,
"destinationKey": to_path
}
)
"POST",
"/object/copy",
json={
"bucketId": self.id,
"sourceKey": from_path,
"destinationKey": to_path,
},
)
return res.json()


async def remove(self, paths: list) -> dict[str, str]:
"""
Deletes files within the same bucket
Expand Down Expand Up @@ -176,7 +188,7 @@ async def list(
)
return response.json()

async def download(self, path: str, options: TransformOptions={}) -> bytes:
async def download(self, path: str, options: TransformOptions = {}) -> bytes:
"""
Downloads a file.
Expand All @@ -185,9 +197,11 @@ async def download(self, path: str, options: TransformOptions={}) -> bytes:
path
The file path to be downloaded, including the path and file name. For example `folder/image.png`.
"""
render_path = 'render/image/authenticated' if options.get('transform') else 'object'
render_path = (
"render/image/authenticated" if options.get("transform") else "object"
)
transformation_query = urllib.parse.urlencode(options)
query_string = f"?{transformation_query}" if transformation_query else ''
query_string = f"?{transformation_query}" if transformation_query else ""

_path = self._get_final_path(path)
response = await self._request(
Expand Down Expand Up @@ -241,7 +255,6 @@ def _get_final_path(self, path: str) -> str:
return f"{self.id}/{path}"



# this class is returned by methods that fetch buckets, for example StorageBucketAPI.get_bucket
# adding this mixin on the BaseBucket means that those bucket objects can also be used to
# run methods like `upload` and `download`
Expand Down
63 changes: 42 additions & 21 deletions storage3/_sync/file_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations
import urllib.parse

from dataclasses import dataclass, field
from io import BufferedReader, FileIO
Expand All @@ -8,7 +9,13 @@
from httpx import HTTPError, Response

from ..constants import DEFAULT_FILE_OPTIONS, DEFAULT_SEARCH_OPTIONS
from ..types import BaseBucket, ListBucketFilesOptions, CreateSignedURLOptions, RequestMethod
from ..types import (
BaseBucket,
ListBucketFilesOptions,
CreateSignedURLOptions,
TransformOptions,
RequestMethod,
)
from ..utils import SyncClient, StorageException

__all__ = ["SyncBucket"]
Expand Down Expand Up @@ -44,7 +51,9 @@ def _request(

return response

def create_signed_url(self, path: str, expires_in: int, options: CreateSignedURLOptions = {}) -> dict[str, str]:
def create_signed_url(
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
) -> dict[str, str]:
"""
Parameters
----------
Expand All @@ -65,25 +74,33 @@ def create_signed_url(self, path: str, expires_in: int, options: CreateSignedURL
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
return data

def create_signed_urls(self, paths: List[str], expires_in: int, options: dict[str, str]) ->dict[str, str]:
response = self._request("POST",
f"/object/sign/{self.bucket_id}",json={
"expires_in": expires_in,
"paths": paths})
def create_signed_urls(
self, paths: List[str], expires_in: int, options: dict[str, str]
) -> dict[str, str]:
response = self._request(
"POST",
f"/object/sign/{self.bucket_id}",
json={"expires_in": expires_in, "paths": paths},
)
# TODO(joel): add support for download option
return response.json()

pass

def get_public_url(self, path: str) -> str:
def get_public_url(self, path: str, options: TransformOptions = {}) -> str:
"""
Parameters
----------
path
file path, including the path and file name. For example `folder/image.png`.
"""
render_path = (
"render/image/authenticated" if options.get("transform") else "object"
)
transformation_query = urllib.parse.urlencode(options)
query_string = f"?{transformation_query}" if transformation_query else ""
_path = self._get_final_path(path)
return f"{self._client.base_url}object/public/{_path}"
return f"{self._client.base_url}{render_path}/public/{_path}/${query_string}"

def move(self, from_path: str, to_path: str) -> dict[str, str]:
"""
Expand Down Expand Up @@ -119,17 +136,16 @@ def copy(self, from_path: str, to_path: str) -> dict[str, str]:
The new file path, including the new file name. For example `folder/image-copy.png`.
"""
res = self._request(
"POST",
"/object/copy",
json={
"bucketId": self.id,
"sourceKey": from_path,
"destinationKey": to_path
}
)
"POST",
"/object/copy",
json={
"bucketId": self.id,
"sourceKey": from_path,
"destinationKey": to_path,
},
)
return res.json()


def remove(self, paths: list) -> dict[str, str]:
"""
Deletes files within the same bucket
Expand Down Expand Up @@ -172,7 +188,7 @@ def list(
)
return response.json()

def download(self, path: str) -> bytes:
def download(self, path: str, options: TransformOptions = {}) -> bytes:
"""
Downloads a file.
Expand All @@ -181,10 +197,16 @@ def download(self, path: str) -> bytes:
path
The file path to be downloaded, including the path and file name. For example `folder/image.png`.
"""
render_path = (
"render/image/authenticated" if options.get("transform") else "object"
)
transformation_query = urllib.parse.urlencode(options)
query_string = f"?{transformation_query}" if transformation_query else ""

_path = self._get_final_path(path)
response = self._request(
"GET",
f"/object/{_path}",
f"{render_path}/{_path}{query_string}",
)
return response.content

Expand Down Expand Up @@ -233,7 +255,6 @@ def _get_final_path(self, path: str) -> str:
return f"{self.id}/{path}"



# this class is returned by methods that fetch buckets, for example StorageBucketAPI.get_bucket
# adding this mixin on the BaseBucket means that those bucket objects can also be used to
# run methods like `upload` and `download`
Expand Down
4 changes: 3 additions & 1 deletion storage3/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ class ListBucketFilesOptions(TypedDict):
offset: int
sortBy: _sortByType


class TransformOptions(TypedDict):
height: Optional[float]
width: Optional[float]
resize: Optional[Literal['cover'] | Literal['contain'] | Literal['fill']]
resize: Optional[Literal["cover"] | Literal["contain"] | Literal["fill"]]


class CreateSignedURLOptions(TypedDict):
download: Optional[str | bool]
Expand Down
1 change: 0 additions & 1 deletion storage3/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ def aclose(self) -> None:
self.close()



class StorageException(Exception):
"""Error raised when an operation on the storage API fails."""
4 changes: 1 addition & 3 deletions tests/_sync/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ def bucket(storage: SyncStorageClient, uuid_factory: Callable[[], str]) -> str:


@pytest.fixture(scope="module")
def public_bucket(
storage: SyncStorageClient, uuid_factory: Callable[[], str]
) -> str:
def public_bucket(storage: SyncStorageClient, uuid_factory: Callable[[], str]) -> str:
"""Creates a test public bucket which will be used in the whole storage tests run and deleted at the end"""
bucket_id = uuid_factory()

Expand Down

0 comments on commit 122b2a3

Please sign in to comment.