render_sdk 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- render_sdk/__init__.py +41 -4
- render_sdk/client/__init__.py +25 -0
- render_sdk/client/client.py +5 -0
- render_sdk/client/sse.py +5 -1
- render_sdk/client/tests/test_client.py +6 -4
- render_sdk/client/tests/test_sse.py +1 -0
- render_sdk/client/types.py +2 -1
- render_sdk/client/workflows.py +13 -3
- render_sdk/experimental/__init__.py +31 -0
- render_sdk/experimental/experimental.py +71 -0
- render_sdk/experimental/object/__init__.py +30 -0
- render_sdk/experimental/object/api.py +260 -0
- render_sdk/experimental/object/client.py +475 -0
- render_sdk/experimental/object/types.py +87 -0
- render_sdk/public_api/api/audit_logs/list_organization_audit_logs.py +303 -0
- render_sdk/public_api/api/audit_logs/list_owner_audit_logs.py +303 -0
- render_sdk/public_api/api/blob_storage/delete_blob.py +215 -0
- render_sdk/public_api/api/blob_storage/get_blob.py +221 -0
- render_sdk/public_api/api/{workflows/list_workflow_versions.py → blob_storage/list_blobs.py} +52 -30
- render_sdk/public_api/api/blob_storage/put_blob.py +248 -0
- render_sdk/public_api/api/blueprints/validate_blueprint.py +212 -0
- render_sdk/public_api/api/key_value/resume_key_value.py +203 -0
- render_sdk/public_api/api/key_value/suspend_key_value.py +203 -0
- render_sdk/public_api/api/metrics/get_bandwidth_sources.py +251 -0
- render_sdk/public_api/api/postgres/create_postgres_user.py +229 -0
- render_sdk/public_api/api/postgres/delete_postgres_user.py +201 -0
- render_sdk/public_api/api/postgres/list_postgres_users.py +195 -0
- render_sdk/public_api/api/redis_deprecated/__init__.py +1 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/create_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/delete_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/list_redis.py +4 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis_connection_info.py +4 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/update_redis.py +4 -4
- render_sdk/public_api/api/services/create_service.py +4 -4
- render_sdk/public_api/api/workflow_tasks_ea/__init__.py +1 -0
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/cancel_task_run.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/create_task.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task_run.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_task_runs.py +12 -0
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_tasks.py +24 -12
- render_sdk/public_api/api/workflows_ea/__init__.py +1 -0
- render_sdk/public_api/api/workflows_ea/create_workflow.py +199 -0
- render_sdk/public_api/api/{workflows/deploy_workflow.py → workflows_ea/create_workflow_version.py} +31 -14
- render_sdk/public_api/api/{workflows → workflows_ea}/delete_workflow.py +12 -4
- render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow.py +32 -14
- render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow_version.py +12 -4
- render_sdk/public_api/api/workflows_ea/list_workflow_versions.py +275 -0
- render_sdk/public_api/api/{workflows → workflows_ea}/list_workflows.py +41 -14
- render_sdk/public_api/api/workflows_ea/update_workflow.py +212 -0
- render_sdk/public_api/api/workspaces/remove_workspace_member.py +206 -0
- render_sdk/public_api/api/workspaces/update_workspace_member.py +235 -0
- render_sdk/public_api/models/__init__.py +82 -4
- render_sdk/public_api/models/audit_log.py +113 -0
- render_sdk/public_api/models/audit_log_actor.py +80 -0
- render_sdk/public_api/models/audit_log_actor_type.py +10 -0
- render_sdk/public_api/models/audit_log_event.py +80 -0
- render_sdk/public_api/models/audit_log_metadata.py +49 -0
- render_sdk/public_api/models/audit_log_status.py +9 -0
- render_sdk/public_api/models/audit_log_with_cursor.py +73 -0
- render_sdk/public_api/models/background_worker_details.py +2 -2
- render_sdk/public_api/models/background_worker_details_patch.py +1 -1
- render_sdk/public_api/models/background_worker_details_post.py +1 -1
- render_sdk/public_api/models/blob_metadata.py +85 -0
- render_sdk/public_api/models/blob_with_cursor.py +73 -0
- render_sdk/public_api/models/cache.py +6 -4
- render_sdk/public_api/models/cache_profile.py +10 -0
- render_sdk/public_api/models/create_deploy_body.py +23 -0
- render_sdk/public_api/models/create_version.py +70 -0
- render_sdk/public_api/models/credential_create_input.py +59 -0
- render_sdk/public_api/models/cron_job_details.py +2 -2
- render_sdk/public_api/models/cron_job_details_patch.py +1 -1
- render_sdk/public_api/models/cron_job_details_post.py +1 -1
- render_sdk/public_api/models/deploy_mode.py +9 -0
- render_sdk/public_api/models/event.py +11 -27
- render_sdk/public_api/models/event_type.py +1 -1
- render_sdk/public_api/models/get_bandwidth_sources_response_200.py +75 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item.py +101 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels.py +78 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels_traffic_source.py +12 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_values_item.py +68 -0
- render_sdk/public_api/models/{server_unhealthy.py → get_bandwidth_sources_response_400.py} +12 -12
- render_sdk/public_api/models/get_blob_output.py +71 -0
- render_sdk/public_api/models/list_postgres_users_response_200_item.py +86 -0
- render_sdk/public_api/models/otel_provider_type.py +2 -0
- render_sdk/public_api/models/postgres.py +8 -0
- render_sdk/public_api/models/postgres_detail.py +26 -0
- render_sdk/public_api/models/postgres_parameter_overrides.py +44 -0
- render_sdk/public_api/models/postgres_patch_input.py +27 -0
- render_sdk/public_api/models/postgres_post_input.py +27 -0
- render_sdk/public_api/models/postgres_version.py +1 -0
- render_sdk/public_api/models/preview_input.py +2 -2
- render_sdk/public_api/models/private_service_details.py +2 -2
- render_sdk/public_api/models/private_service_details_patch.py +1 -1
- render_sdk/public_api/models/private_service_details_post.py +1 -1
- render_sdk/public_api/models/project_post_environment_input.py +26 -1
- render_sdk/public_api/models/put_blob_input.py +59 -0
- render_sdk/public_api/models/put_blob_output.py +79 -0
- render_sdk/public_api/models/read_replica.py +25 -1
- render_sdk/public_api/models/read_replica_input.py +25 -1
- render_sdk/public_api/models/run_task.py +35 -7
- render_sdk/public_api/models/service_event.py +12 -27
- render_sdk/public_api/models/service_event_type.py +0 -1
- render_sdk/public_api/models/service_post.py +9 -6
- render_sdk/public_api/models/task_attempt.py +88 -0
- render_sdk/public_api/models/task_attempt_details.py +108 -0
- render_sdk/public_api/models/task_data_type_1.py +44 -0
- render_sdk/public_api/models/task_run.py +23 -1
- render_sdk/public_api/models/task_run_details.py +50 -5
- render_sdk/public_api/models/task_run_status.py +1 -0
- render_sdk/public_api/models/task_with_cursor.py +73 -0
- render_sdk/public_api/models/team_member.py +5 -4
- render_sdk/public_api/models/team_member_role.py +12 -0
- render_sdk/public_api/models/update_workspace_member_body.py +61 -0
- render_sdk/public_api/models/validate_blueprint_request.py +84 -0
- render_sdk/public_api/models/validate_blueprint_response.py +105 -0
- render_sdk/public_api/models/validation_error.py +88 -0
- render_sdk/public_api/models/validation_plan_summary.py +107 -0
- render_sdk/public_api/models/web_service_details.py +2 -2
- render_sdk/public_api/models/web_service_details_patch.py +6 -5
- render_sdk/public_api/models/web_service_details_post.py +6 -5
- render_sdk/public_api/models/workflow.py +144 -0
- render_sdk/public_api/models/workflow_create.py +99 -0
- render_sdk/public_api/models/workflow_update.py +90 -0
- render_sdk/public_api/models/workflow_version.py +10 -14
- render_sdk/public_api/models/workflow_version_status.py +13 -0
- render_sdk/public_api/models/workflow_version_with_cursor.py +73 -0
- render_sdk/public_api/models/workflow_with_cursor.py +73 -0
- render_sdk/render.py +65 -0
- render_sdk/version.py +27 -0
- render_sdk/workflows/__init__.py +5 -1
- render_sdk/workflows/app.py +262 -0
- render_sdk/workflows/callback_api/models/__init__.py +2 -0
- render_sdk/workflows/callback_api/models/task.py +21 -0
- render_sdk/workflows/callback_api/models/task_options.py +18 -0
- render_sdk/workflows/callback_api/models/task_parameter.py +88 -0
- render_sdk/workflows/callback_api/py.typed +1 -1
- render_sdk/workflows/cli.py +58 -0
- render_sdk/workflows/client.py +8 -9
- render_sdk/workflows/executor.py +19 -7
- render_sdk/workflows/runner.py +43 -10
- render_sdk/workflows/task.py +84 -5
- render_sdk/workflows/tests/test_app.py +412 -0
- render_sdk/workflows/tests/test_cli.py +134 -0
- render_sdk/workflows/tests/test_end_to_end.py +71 -1
- render_sdk/workflows/tests/test_registration.py +58 -1
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/METADATA +4 -3
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/RECORD +155 -83
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/WHEEL +1 -1
- render_sdk-0.2.0.dist-info/entry_points.txt +3 -0
- render_sdk/public_api/models/image_version.py +0 -79
- /render_sdk/public_api/api/{redis → audit_logs}/__init__.py +0 -0
- /render_sdk/public_api/api/{workflows → blob_storage}/__init__.py +0 -0
- /render_sdk/public_api/api/{workflows → workflow_tasks_ea}/stream_task_runs_events.py +0 -0
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""High-level object storage client.
|
|
2
|
+
|
|
3
|
+
Provides simple put/get/delete operations for object storage.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from render_sdk.client.errors import RenderError
|
|
11
|
+
from render_sdk.client.util import handle_http_error
|
|
12
|
+
from render_sdk.experimental.object.api import ObjectApi
|
|
13
|
+
from render_sdk.experimental.object.types import (
|
|
14
|
+
ListObjectsResponse,
|
|
15
|
+
ObjectData,
|
|
16
|
+
OwnerID,
|
|
17
|
+
PutObjectResult,
|
|
18
|
+
)
|
|
19
|
+
from render_sdk.public_api.models.region import Region
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from render_sdk.public_api.client import AuthenticatedClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ObjectClient:
|
|
26
|
+
"""ObjectClient is a high level client for interacting with object storage.
|
|
27
|
+
|
|
28
|
+
It exposes methods to put/get/delete objects.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, client: "AuthenticatedClient"):
|
|
32
|
+
self.client = client
|
|
33
|
+
self.api = ObjectApi(client)
|
|
34
|
+
|
|
35
|
+
async def put(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
owner_id: OwnerID,
|
|
39
|
+
region: Region | str,
|
|
40
|
+
key: str,
|
|
41
|
+
data: bytes | BinaryIO,
|
|
42
|
+
size: int | None = None,
|
|
43
|
+
content_type: str | None = None,
|
|
44
|
+
) -> PutObjectResult:
|
|
45
|
+
"""Upload an object to storage.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
owner_id: Owner ID (workspace team ID) in format tea-xxxxx
|
|
49
|
+
region: Storage region
|
|
50
|
+
key: Object key (path) for the object
|
|
51
|
+
data: Binary data as bytes or a file-like stream
|
|
52
|
+
size: Size in bytes (optional for bytes, required for streams)
|
|
53
|
+
content_type: MIME type of the content (optional)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
PutObjectResult: Result with optional ETag
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
RenderError: If size validation fails or upload fails
|
|
60
|
+
ClientError: For 4xx client errors
|
|
61
|
+
ServerError: For 5xx server errors
|
|
62
|
+
TimeoutError: If the request times out
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
```python
|
|
66
|
+
# Upload bytes
|
|
67
|
+
await object_client.put(
|
|
68
|
+
owner_id="tea-xxxxx",
|
|
69
|
+
region="oregon",
|
|
70
|
+
key="path/to/file.png",
|
|
71
|
+
data=b"binary content",
|
|
72
|
+
content_type="image/png"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Upload from file stream
|
|
76
|
+
with open("/path/to/file.zip", "rb") as f:
|
|
77
|
+
import os
|
|
78
|
+
size = os.path.getsize("/path/to/file.zip")
|
|
79
|
+
await object_client.put(
|
|
80
|
+
owner_id="tea-xxxxx",
|
|
81
|
+
region="oregon",
|
|
82
|
+
key="file.zip",
|
|
83
|
+
data=f,
|
|
84
|
+
size=size
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
# Resolve and validate size
|
|
89
|
+
resolved_size = self._resolve_size(data, size)
|
|
90
|
+
|
|
91
|
+
# Convert region to Region enum if it's a string
|
|
92
|
+
region_enum = Region(region) if isinstance(region, str) else region
|
|
93
|
+
|
|
94
|
+
# Step 1: Get presigned upload URL from Render API
|
|
95
|
+
presigned = await self.api.get_upload_url(
|
|
96
|
+
owner_id=owner_id,
|
|
97
|
+
region=region_enum,
|
|
98
|
+
key=key,
|
|
99
|
+
size_bytes=resolved_size,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Step 2: Upload to storage via presigned URL
|
|
103
|
+
headers = {
|
|
104
|
+
"Content-Length": str(resolved_size),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if content_type:
|
|
108
|
+
headers["Content-Type"] = content_type
|
|
109
|
+
|
|
110
|
+
async with httpx.AsyncClient() as http_client:
|
|
111
|
+
response = await http_client.put(
|
|
112
|
+
presigned.url,
|
|
113
|
+
headers=headers,
|
|
114
|
+
content=data,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
handle_http_error(response, "upload object")
|
|
118
|
+
|
|
119
|
+
return PutObjectResult(
|
|
120
|
+
etag=response.headers.get("ETag"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def get(
|
|
124
|
+
self, *, owner_id: OwnerID, region: Region | str, key: str
|
|
125
|
+
) -> ObjectData:
|
|
126
|
+
"""Download an object from storage.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
owner_id: Owner ID (workspace team ID) in format tea-xxxxx
|
|
130
|
+
region: Storage region
|
|
131
|
+
key: Object key (path) for the object
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
ObjectData: Object data with content
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ClientError: For 4xx client errors
|
|
138
|
+
ServerError: For 5xx server errors
|
|
139
|
+
TimeoutError: If the request times out
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
```python
|
|
143
|
+
obj = await object_client.get(
|
|
144
|
+
owner_id="tea-xxxxx",
|
|
145
|
+
region="oregon",
|
|
146
|
+
key="path/to/file.png"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
print(obj.size) # Size in bytes
|
|
150
|
+
print(obj.content_type) # MIME type if available
|
|
151
|
+
# obj.data is bytes
|
|
152
|
+
```
|
|
153
|
+
"""
|
|
154
|
+
# Convert region to Region enum if it's a string
|
|
155
|
+
region_enum = Region(region) if isinstance(region, str) else region
|
|
156
|
+
|
|
157
|
+
# Step 1: Get presigned download URL from Render API
|
|
158
|
+
presigned = await self.api.get_download_url(
|
|
159
|
+
owner_id=owner_id,
|
|
160
|
+
region=region_enum,
|
|
161
|
+
key=key,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Step 2: Download from storage via presigned URL
|
|
165
|
+
async with httpx.AsyncClient() as http_client:
|
|
166
|
+
response = await http_client.get(presigned.url)
|
|
167
|
+
|
|
168
|
+
handle_http_error(response, "download object")
|
|
169
|
+
|
|
170
|
+
data = response.content
|
|
171
|
+
|
|
172
|
+
return ObjectData(
|
|
173
|
+
data=data,
|
|
174
|
+
size=len(data),
|
|
175
|
+
content_type=response.headers.get("Content-Type"),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def delete(
|
|
179
|
+
self, *, owner_id: OwnerID, region: Region | str, key: str
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Delete an object from storage.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
owner_id: Owner ID (workspace team ID) in format tea-xxxxx
|
|
185
|
+
region: Storage region
|
|
186
|
+
key: Object key (path) for the object
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ClientError: For 4xx client errors
|
|
190
|
+
ServerError: For 5xx server errors
|
|
191
|
+
TimeoutError: If the request times out
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
```python
|
|
195
|
+
await object_client.delete(
|
|
196
|
+
owner_id="tea-xxxxx",
|
|
197
|
+
region="oregon",
|
|
198
|
+
key="path/to/file.png"
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
"""
|
|
202
|
+
# Convert region to Region enum if it's a string
|
|
203
|
+
region_enum = Region(region) if isinstance(region, str) else region
|
|
204
|
+
|
|
205
|
+
# DELETE goes directly to Render API (no presigned URL)
|
|
206
|
+
await self.api.delete(
|
|
207
|
+
owner_id=owner_id,
|
|
208
|
+
region=region_enum,
|
|
209
|
+
key=key,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async def list(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
owner_id: OwnerID,
|
|
216
|
+
region: Region | str,
|
|
217
|
+
cursor: str | None = None,
|
|
218
|
+
limit: int | None = None,
|
|
219
|
+
) -> ListObjectsResponse:
|
|
220
|
+
"""List objects in storage.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
owner_id: Owner ID (workspace team ID) in format tea-xxxxx
|
|
224
|
+
region: Storage region
|
|
225
|
+
cursor: Pagination cursor from previous response
|
|
226
|
+
limit: Maximum number of objects to return (default 20)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
ListObjectsResponse: List of object metadata with optional next cursor
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ClientError: For 4xx client errors
|
|
233
|
+
ServerError: For 5xx server errors
|
|
234
|
+
TimeoutError: If the request times out
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
```python
|
|
238
|
+
# List first page
|
|
239
|
+
response = await object_client.list(
|
|
240
|
+
owner_id="tea-xxxxx",
|
|
241
|
+
region="oregon"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
for obj in response.objects:
|
|
245
|
+
print(f"{obj.key}: {obj.size} bytes")
|
|
246
|
+
|
|
247
|
+
# Get next page if available
|
|
248
|
+
if response.next_cursor:
|
|
249
|
+
next_page = await object_client.list(
|
|
250
|
+
owner_id="tea-xxxxx",
|
|
251
|
+
region="oregon",
|
|
252
|
+
cursor=response.next_cursor
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
"""
|
|
256
|
+
# Convert region to Region enum if it's a string
|
|
257
|
+
region_enum = Region(region) if isinstance(region, str) else region
|
|
258
|
+
|
|
259
|
+
return await self.api.list_objects(
|
|
260
|
+
owner_id=owner_id,
|
|
261
|
+
region=region_enum,
|
|
262
|
+
cursor=cursor,
|
|
263
|
+
limit=limit,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def scoped(
|
|
267
|
+
self, *, owner_id: OwnerID, region: Region | str
|
|
268
|
+
) -> "ScopedObjectClient":
|
|
269
|
+
"""Create a scoped object client for a specific owner and region.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
owner_id: Owner ID (workspace team ID) in format tea-xxxxx
|
|
273
|
+
region: Storage region
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
ScopedObjectClient: Scoped object client that doesn't require
|
|
277
|
+
owner_id/region on each call
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
```python
|
|
281
|
+
scoped = object_client.scoped(
|
|
282
|
+
owner_id="tea-xxxxx",
|
|
283
|
+
region="oregon"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Subsequent calls only need the key
|
|
287
|
+
await scoped.put(key="file.png", data=buffer)
|
|
288
|
+
await scoped.get(key="file.png")
|
|
289
|
+
await scoped.delete(key="file.png")
|
|
290
|
+
```
|
|
291
|
+
"""
|
|
292
|
+
return ScopedObjectClient(self, owner_id, region)
|
|
293
|
+
|
|
294
|
+
def _resolve_size(self, data: bytes | BinaryIO, size: int | None) -> int:
|
|
295
|
+
"""Resolve and validate the size for a put operation.
|
|
296
|
+
|
|
297
|
+
- For bytes: auto-calculate size, validate if provided
|
|
298
|
+
- For streams: require explicit size
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
data: Binary data (bytes or stream)
|
|
302
|
+
size: Optional size in bytes
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
int: The size in bytes
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
RenderError: If size validation fails
|
|
309
|
+
"""
|
|
310
|
+
if isinstance(data, bytes):
|
|
311
|
+
# Auto-calculate for bytes
|
|
312
|
+
actual_size = len(data)
|
|
313
|
+
|
|
314
|
+
if size is not None and size != actual_size:
|
|
315
|
+
raise RenderError(
|
|
316
|
+
f"Size mismatch: provided size {size} does not match "
|
|
317
|
+
f"actual size {actual_size}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return actual_size
|
|
321
|
+
else:
|
|
322
|
+
# Require explicit size for streams
|
|
323
|
+
if size is None:
|
|
324
|
+
raise RenderError("size is required for stream uploads")
|
|
325
|
+
if size <= 0:
|
|
326
|
+
raise RenderError("size must be a positive integer")
|
|
327
|
+
|
|
328
|
+
return size
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class ScopedObjectClient:
|
|
332
|
+
"""Scoped Object Client
|
|
333
|
+
|
|
334
|
+
Pre-configured client for a specific owner and region.
|
|
335
|
+
Eliminates the need to specify owner_id and region on every operation.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
```python
|
|
339
|
+
scoped = object_client.scoped(
|
|
340
|
+
owner_id="tea-xxxxx",
|
|
341
|
+
region="oregon"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Methods have the same signature as ObjectClient but without owner_id/region
|
|
345
|
+
await scoped.put(key="file.png", data=b"content")
|
|
346
|
+
obj = await scoped.get(key="file.png")
|
|
347
|
+
await scoped.delete(key="file.png")
|
|
348
|
+
```
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def __init__(
|
|
352
|
+
self, object_client: ObjectClient, owner_id: OwnerID, region: Region | str
|
|
353
|
+
):
|
|
354
|
+
self._object_client = object_client
|
|
355
|
+
self._owner_id = owner_id
|
|
356
|
+
self._region = region
|
|
357
|
+
|
|
358
|
+
async def put(
|
|
359
|
+
self,
|
|
360
|
+
*,
|
|
361
|
+
key: str,
|
|
362
|
+
data: bytes | BinaryIO,
|
|
363
|
+
size: int | None = None,
|
|
364
|
+
content_type: str | None = None,
|
|
365
|
+
) -> PutObjectResult:
|
|
366
|
+
"""Upload an object to storage using scoped owner and region.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
key: Object key (path) for the object
|
|
370
|
+
data: Binary data as bytes or a file-like stream
|
|
371
|
+
size: Size in bytes (optional for bytes, required for streams)
|
|
372
|
+
content_type: MIME type of the content (optional)
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
PutObjectResult: Result with optional ETag
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
```python
|
|
379
|
+
scoped = object_client.scoped(
|
|
380
|
+
owner_id="tea-xxxxx",
|
|
381
|
+
region="oregon"
|
|
382
|
+
)
|
|
383
|
+
await scoped.put(
|
|
384
|
+
key="file.png",
|
|
385
|
+
data=b"content",
|
|
386
|
+
content_type="image/png"
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
"""
|
|
390
|
+
return await self._object_client.put(
|
|
391
|
+
owner_id=self._owner_id,
|
|
392
|
+
region=self._region,
|
|
393
|
+
key=key,
|
|
394
|
+
data=data,
|
|
395
|
+
size=size,
|
|
396
|
+
content_type=content_type,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
async def get(self, *, key: str) -> ObjectData:
|
|
400
|
+
"""Download an object from storage using scoped owner and region.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
key: Object key (path) for the object
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
ObjectData: Object data with content
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
```python
|
|
410
|
+
scoped = object_client.scoped(
|
|
411
|
+
owner_id="tea-xxxxx",
|
|
412
|
+
region="oregon"
|
|
413
|
+
)
|
|
414
|
+
obj = await scoped.get(key="file.png")
|
|
415
|
+
```
|
|
416
|
+
"""
|
|
417
|
+
return await self._object_client.get(
|
|
418
|
+
owner_id=self._owner_id,
|
|
419
|
+
region=self._region,
|
|
420
|
+
key=key,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
async def delete(self, *, key: str) -> None:
|
|
424
|
+
"""Delete an object from storage using scoped owner and region.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
key: Object key (path) for the object
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
```python
|
|
431
|
+
scoped = object_client.scoped(
|
|
432
|
+
owner_id="tea-xxxxx",
|
|
433
|
+
region="oregon"
|
|
434
|
+
)
|
|
435
|
+
await scoped.delete(key="file.png")
|
|
436
|
+
```
|
|
437
|
+
"""
|
|
438
|
+
await self._object_client.delete(
|
|
439
|
+
owner_id=self._owner_id,
|
|
440
|
+
region=self._region,
|
|
441
|
+
key=key,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def list(
|
|
445
|
+
self,
|
|
446
|
+
*,
|
|
447
|
+
cursor: str | None = None,
|
|
448
|
+
limit: int | None = None,
|
|
449
|
+
) -> ListObjectsResponse:
|
|
450
|
+
"""List objects in storage using scoped owner and region.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
cursor: Pagination cursor from previous response
|
|
454
|
+
limit: Maximum number of objects to return (default 20)
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
ListObjectsResponse: List of object metadata with optional next cursor
|
|
458
|
+
|
|
459
|
+
Example:
|
|
460
|
+
```python
|
|
461
|
+
scoped = object_client.scoped(
|
|
462
|
+
owner_id="tea-xxxxx",
|
|
463
|
+
region="oregon"
|
|
464
|
+
)
|
|
465
|
+
response = await scoped.list()
|
|
466
|
+
for obj in response.objects:
|
|
467
|
+
print(f"{obj.key}: {obj.size} bytes")
|
|
468
|
+
```
|
|
469
|
+
"""
|
|
470
|
+
return await self._object_client.list(
|
|
471
|
+
owner_id=self._owner_id,
|
|
472
|
+
region=self._region,
|
|
473
|
+
cursor=cursor,
|
|
474
|
+
limit=limit,
|
|
475
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Type definitions for the experimental object storage API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
# Re-export Region from the generated models for user convenience.
|
|
7
|
+
# While region accepts strings, exporting Region allows users to see
|
|
8
|
+
# available values via IDE autocomplete.
|
|
9
|
+
from render_sdk.public_api.models.region import Region # noqa: F401
|
|
10
|
+
|
|
11
|
+
# Type alias for owner ID format
|
|
12
|
+
OwnerID = str # Should match pattern tea-xxxxx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class UploadResponse:
|
|
17
|
+
"""Response containing upload URL and metadata."""
|
|
18
|
+
|
|
19
|
+
url: str
|
|
20
|
+
"""Presigned upload URL"""
|
|
21
|
+
|
|
22
|
+
expires_at: datetime
|
|
23
|
+
"""Expiration timestamp"""
|
|
24
|
+
|
|
25
|
+
max_size_bytes: int
|
|
26
|
+
"""Maximum size allowed for upload"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DownloadResponse:
|
|
31
|
+
"""Response containing download URL and metadata."""
|
|
32
|
+
|
|
33
|
+
url: str
|
|
34
|
+
"""Presigned download URL"""
|
|
35
|
+
|
|
36
|
+
expires_at: datetime
|
|
37
|
+
"""Expiration timestamp"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ObjectData:
|
|
42
|
+
"""Downloaded object data."""
|
|
43
|
+
|
|
44
|
+
data: bytes
|
|
45
|
+
"""Binary content"""
|
|
46
|
+
|
|
47
|
+
size: int
|
|
48
|
+
"""Size in bytes"""
|
|
49
|
+
|
|
50
|
+
content_type: str | None = None
|
|
51
|
+
"""MIME type if available"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class PutObjectResult:
|
|
56
|
+
"""Result from uploading an object."""
|
|
57
|
+
|
|
58
|
+
etag: str | None = None
|
|
59
|
+
"""ETag from storage provider"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ObjectMetadata:
|
|
64
|
+
"""Metadata for a stored object."""
|
|
65
|
+
|
|
66
|
+
key: str
|
|
67
|
+
"""Object key (path)"""
|
|
68
|
+
|
|
69
|
+
size: int
|
|
70
|
+
"""Size in bytes"""
|
|
71
|
+
|
|
72
|
+
last_modified: datetime
|
|
73
|
+
"""When the object was last modified"""
|
|
74
|
+
|
|
75
|
+
content_type: str
|
|
76
|
+
"""MIME type of the object"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ListObjectsResponse:
|
|
81
|
+
"""Response from listing objects."""
|
|
82
|
+
|
|
83
|
+
objects: list[ObjectMetadata]
|
|
84
|
+
"""List of object metadata"""
|
|
85
|
+
|
|
86
|
+
next_cursor: str | None = None
|
|
87
|
+
"""Cursor for next page, None if no more results"""
|