Skip to content

Commit c43b98d

Browse files
authored
Merge pull request #1 from InsForge/storage-admin-sdks
Add storage admin client helpers
2 parents accc804 + ea26659 commit c43b98d

File tree

4 files changed

+518
-7
lines changed

4 files changed

+518
-7
lines changed

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.pyo
4+
*.pyd
5+
6+
build/
7+
dist/
8+
*.egg-info/
9+
10+
.pytest_cache/
11+
12+
.venv/
13+
venv/
14+
env/
15+
ENV/
16+
17+
.mypy_cache/
18+
.ruff_cache/
19+
20+
.DS_Store

insforge/storage/client.py

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
from __future__ import annotations
22

33
from collections.abc import Mapping
4+
from typing import Any
5+
from typing import Iterable
46

57
import httpx
68

79
from .._base_client import BaseClient
810
from ..exceptions import InsforgeHTTPError
911
from .._utils import quote_path_segment
12+
from .models import DownloadStrategy
1013
from .models import StorageBucketListResponse
14+
from .models import StorageBucketResponse
15+
from .models import StorageBucketUpdateResponse
16+
from .models import StorageDeleteBucketResponse
1117
from .models import StorageDeleteObjectResponse
18+
from .models import StorageDownloadResult
1219
from .models import StorageObjectResponse
20+
from .models import StoredFileList
21+
from .models import UploadStrategy
1322

1423

1524
class StorageClient:
1625
def __init__(self, client: BaseClient) -> None:
1726
self._client = client
1827

28+
def _bucket_path(self, bucket_name: str) -> str:
29+
return f"/api/storage/buckets/{quote_path_segment(bucket_name)}"
30+
1931
def _object_url_path(self, bucket_name: str, object_key: str) -> str:
2032
return (
2133
"/api/storage/buckets/"
@@ -32,6 +44,81 @@ async def list_buckets(self, *, access_token: str | None = None) -> StorageBucke
3244
)
3345
return StorageBucketListResponse.model_validate(payload)
3446

47+
async def create_bucket(
48+
self,
49+
*,
50+
bucket_name: str,
51+
is_public: bool | None = None,
52+
access_token: str | None = None,
53+
) -> StorageBucketResponse:
54+
payload: dict[str, Any] = {"bucketName": bucket_name}
55+
if is_public is not None:
56+
payload["isPublic"] = is_public
57+
58+
response = await self._client._request_json(
59+
"POST",
60+
"/api/storage/buckets",
61+
json=payload,
62+
access_token=access_token,
63+
)
64+
return StorageBucketResponse.model_validate(response)
65+
66+
async def update_bucket(
67+
self,
68+
bucket_name: str,
69+
*,
70+
is_public: bool,
71+
access_token: str | None = None,
72+
) -> StorageBucketUpdateResponse:
73+
response = await self._client._request_json(
74+
"PATCH",
75+
self._bucket_path(bucket_name),
76+
json={"isPublic": is_public},
77+
access_token=access_token,
78+
)
79+
return StorageBucketUpdateResponse.model_validate(response)
80+
81+
async def delete_bucket(
82+
self,
83+
bucket_name: str,
84+
*,
85+
access_token: str | None = None,
86+
) -> StorageDeleteBucketResponse:
87+
response = await self._client._request_json(
88+
"DELETE",
89+
self._bucket_path(bucket_name),
90+
access_token=access_token,
91+
)
92+
return StorageDeleteBucketResponse.model_validate(response)
93+
94+
async def list_objects(
95+
self,
96+
bucket_name: str,
97+
*,
98+
prefix: str | None = None,
99+
limit: int | None = None,
100+
offset: int | None = None,
101+
search: str | None = None,
102+
access_token: str | None = None,
103+
) -> StoredFileList:
104+
params: dict[str, str] = {}
105+
if prefix is not None:
106+
params["prefix"] = prefix
107+
if limit is not None:
108+
params["limit"] = str(limit)
109+
if offset is not None:
110+
params["offset"] = str(offset)
111+
if search is not None:
112+
params["search"] = search
113+
114+
payload = await self._client._request_json(
115+
"GET",
116+
f"/api/storage/buckets/{quote_path_segment(bucket_name)}/objects",
117+
params=params or None,
118+
access_token=access_token,
119+
)
120+
return StoredFileList.model_validate(payload)
121+
35122
async def upload_object(
36123
self,
37124
bucket_name: str,
@@ -69,7 +156,7 @@ async def download_object(
69156
*,
70157
access_token: str | None = None,
71158
extra_headers: Mapping[str, str] | None = None,
72-
) -> bytes:
159+
) -> StorageDownloadResult:
73160
path = self._object_url_path(bucket_name, object_key)
74161
response = await self._client.http_client.request(
75162
"GET",
@@ -87,7 +174,18 @@ async def download_object(
87174
response,
88175
)
89176

90-
return response.content
177+
headers = response.headers
178+
content_type = (headers.get("Content-Type") or headers.get("content-type"))
179+
content_length = (
180+
headers.get("Content-Length")
181+
or headers.get("content-length")
182+
or str(len(response.content))
183+
)
184+
return StorageDownloadResult(
185+
content=response.content,
186+
content_type=content_type,
187+
content_length=content_length,
188+
)
91189

92190
async def delete_object(
93191
self,
@@ -105,3 +203,92 @@ async def delete_object(
105203
extra_headers=extra_headers,
106204
)
107205
return StorageDeleteObjectResponse.model_validate(payload)
206+
207+
async def upload_object_auto(
208+
self,
209+
bucket_name: str,
210+
*,
211+
data: bytes,
212+
filename: str,
213+
content_type: str | None = None,
214+
access_token: str | None = None,
215+
) -> StorageObjectResponse:
216+
path = f"/api/storage/buckets/{quote_path_segment(bucket_name)}/objects"
217+
response = await self._client.http_client.request(
218+
"POST",
219+
self._client._build_url(path),
220+
files={"file": (filename, data, content_type or "application/octet-stream")},
221+
headers=self._client._build_headers(access_token=access_token),
222+
)
223+
224+
if response.is_error:
225+
raise InsforgeHTTPError.from_response("POST", path, response)
226+
227+
return StorageObjectResponse.model_validate(response.json())
228+
229+
async def confirm_upload(
230+
self,
231+
bucket_name: str,
232+
object_key: str,
233+
*,
234+
size: int,
235+
content_type: str | None = None,
236+
etag: str | None = None,
237+
access_token: str | None = None,
238+
) -> StorageObjectResponse:
239+
payload: dict[str, Any] = {"size": size}
240+
if content_type is not None:
241+
payload["contentType"] = content_type
242+
if etag is not None:
243+
payload["etag"] = etag
244+
245+
response = await self._client._request_json(
246+
"POST",
247+
f"{self._object_url_path(bucket_name, object_key)}/confirm-upload",
248+
json=payload,
249+
access_token=access_token,
250+
)
251+
return StorageObjectResponse.model_validate(response)
252+
253+
async def get_upload_strategy(
254+
self,
255+
bucket_name: str,
256+
*,
257+
filename: str,
258+
content_type: str | None = None,
259+
size: int | None = None,
260+
access_token: str | None = None,
261+
) -> UploadStrategy:
262+
payload: dict[str, Any] = {"filename": filename}
263+
if content_type is not None:
264+
payload["contentType"] = content_type
265+
if size is not None:
266+
payload["size"] = size
267+
268+
response = await self._client._request_json(
269+
"POST",
270+
f"/api/storage/buckets/{quote_path_segment(bucket_name)}/upload-strategy",
271+
json=payload,
272+
access_token=access_token,
273+
)
274+
return UploadStrategy.model_validate(response)
275+
276+
async def get_download_strategy(
277+
self,
278+
bucket_name: str,
279+
object_key: str,
280+
*,
281+
expires_in: int | None = None,
282+
access_token: str | None = None,
283+
) -> DownloadStrategy:
284+
payload: dict[str, Any] = {}
285+
if expires_in is not None:
286+
payload["expiresIn"] = expires_in
287+
288+
response = await self._client._request_json(
289+
"POST",
290+
f"{self._object_url_path(bucket_name, object_key)}/download-strategy",
291+
json=payload or None,
292+
access_token=access_token,
293+
)
294+
return DownloadStrategy.model_validate(response)

insforge/storage/models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
from datetime import datetime
4+
from dataclasses import dataclass
5+
from typing import Any
46

57
from pydantic import BaseModel, ConfigDict, Field
68

@@ -11,6 +13,21 @@ class StorageBucketListResponse(BaseModel):
1113
buckets: list[str] = Field(default_factory=list)
1214

1315

16+
class StorageBucketResponse(BaseModel):
17+
model_config = ConfigDict(extra="ignore")
18+
19+
message: str
20+
bucket_name: str = Field(alias="bucketName")
21+
22+
23+
class StorageBucketUpdateResponse(BaseModel):
24+
model_config = ConfigDict(extra="ignore")
25+
26+
message: str
27+
bucket: str
28+
is_public: bool = Field(alias="isPublic")
29+
30+
1431
class StorageObjectResponse(BaseModel):
1532
model_config = ConfigDict(extra="ignore", populate_by_name=True)
1633

@@ -26,3 +43,54 @@ class StorageDeleteObjectResponse(BaseModel):
2643
model_config = ConfigDict(extra="ignore")
2744

2845
message: str
46+
47+
48+
class StoragePagination(BaseModel):
49+
model_config = ConfigDict(extra="ignore")
50+
51+
limit: int | None = None
52+
offset: int | None = None
53+
total: int | None = None
54+
55+
56+
class StoredFileList(BaseModel):
57+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
58+
59+
data: list[StorageObjectResponse] = Field(default_factory=list)
60+
pagination: StoragePagination | None = None
61+
next_actions: str | None = Field(default=None, alias="nextActions")
62+
63+
64+
class UploadStrategy(BaseModel):
65+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
66+
67+
method: str
68+
upload_url: str = Field(alias="uploadUrl")
69+
key: str
70+
confirm_required: bool = Field(alias="confirmRequired")
71+
confirm_url: str | None = Field(default=None, alias="confirmUrl")
72+
expires_at: datetime | None = Field(default=None, alias="expiresAt")
73+
fields: dict[str, Any] | None = None
74+
75+
76+
class DownloadStrategy(BaseModel):
77+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
78+
79+
method: str
80+
url: str
81+
expires_at: datetime | None = Field(default=None, alias="expiresAt")
82+
headers: dict[str, str] | None = None
83+
84+
85+
class StorageDeleteBucketResponse(BaseModel):
86+
model_config = ConfigDict(extra="ignore")
87+
88+
message: str
89+
next_actions: str | None = Field(default=None, alias="nextActions")
90+
91+
92+
@dataclass
93+
class StorageDownloadResult:
94+
content: bytes
95+
content_type: str | None = None
96+
content_length: str | None = None

0 commit comments

Comments
 (0)