waveium 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.
@@ -0,0 +1,88 @@
1
+ """User models for the Waveium SDK."""
2
+
3
+ from datetime import datetime
4
+ from typing import List, Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from .common import PaginationInfo
9
+
10
+
11
+ class CreateUserRequest(BaseModel):
12
+ """Request to create a user (admin only)."""
13
+
14
+ email: str = Field(description="User email")
15
+ organization_id: str = Field(description="Organization ID")
16
+ name: Optional[str] = Field(default=None, description="User name")
17
+ temporary_password: Optional[str] = Field(
18
+ default=None, description="Temporary password"
19
+ )
20
+
21
+
22
+ class UpdateOwnUserRequest(BaseModel):
23
+ """Request to update own profile."""
24
+
25
+ email: Optional[str] = Field(default=None, description="New email")
26
+ name: Optional[str] = Field(default=None, description="New name")
27
+
28
+
29
+ class AdminUpdateUserRequest(BaseModel):
30
+ """Request to update a user (admin only)."""
31
+
32
+ email: Optional[str] = Field(default=None, description="New email")
33
+ name: Optional[str] = Field(default=None, description="New name")
34
+ is_active: Optional[bool] = Field(default=None, description="Active status")
35
+ role_ids: Optional[List[str]] = Field(
36
+ default=None, description="Role IDs to assign"
37
+ )
38
+
39
+
40
+ class RoleInUserResponse(BaseModel):
41
+ """Inline role info in user role response."""
42
+
43
+ id: str = Field(description="Role ID")
44
+ name: str = Field(description="Role name")
45
+ description: Optional[str] = Field(default=None, description="Description")
46
+ scope: str = Field(description="Scope")
47
+ is_system_role: bool = Field(description="Is system role")
48
+ created_at: datetime = Field(description="Created at")
49
+
50
+
51
+ class UserRoleResponse(BaseModel):
52
+ """User role assignment."""
53
+
54
+ id: str = Field(description="User role ID")
55
+ role: RoleInUserResponse = Field(description="Role details")
56
+ scope_type: str = Field(description="Scope type")
57
+ scope_id: Optional[str] = Field(default=None, description="Scope ID")
58
+ granted_at: datetime = Field(description="Granted at")
59
+ expires_at: Optional[datetime] = Field(
60
+ default=None, description="Expires at"
61
+ )
62
+
63
+
64
+ class UserResponse(BaseModel):
65
+ """User information."""
66
+
67
+ id: str = Field(description="User ID")
68
+ email: str = Field(description="Email")
69
+ name: Optional[str] = Field(default=None, description="Name")
70
+ organization_id: str = Field(description="Organization ID")
71
+ is_active: bool = Field(description="Active status")
72
+ created_at: datetime = Field(description="Created at")
73
+ updated_at: Optional[datetime] = Field(
74
+ default=None, description="Updated at"
75
+ )
76
+ last_login: Optional[datetime] = Field(
77
+ default=None, description="Last login"
78
+ )
79
+ user_roles: Optional[List[UserRoleResponse]] = Field(
80
+ default=None, description="User roles"
81
+ )
82
+
83
+
84
+ class PaginatedUserResponse(BaseModel):
85
+ """Paginated user list."""
86
+
87
+ data: List[UserResponse] = Field(description="List of users")
88
+ pagination: PaginationInfo = Field(description="Pagination info")
@@ -0,0 +1,41 @@
1
+ """API resources for the Waveium SDK."""
2
+
3
+ from .auth import AsyncAuthResource, AuthResource
4
+ from .environments import (
5
+ AsyncEnvironmentsResource,
6
+ EnvironmentsResource,
7
+ )
8
+ from .executions import (
9
+ AsyncExecutionsResource,
10
+ ExecutionsResource,
11
+ )
12
+ from .organizations import (
13
+ AsyncOrganizationsResource,
14
+ OrganizationsResource,
15
+ )
16
+ from .projects import AsyncProjectsResource, ProjectsResource
17
+ from .roles import AsyncRolesResource, RolesResource
18
+ from .scenes import AsyncScenesResource, ScenesResource
19
+ from .teams import AsyncTeamsResource, TeamsResource
20
+ from .users import AsyncUsersResource, UsersResource
21
+
22
+ __all__ = [
23
+ "AuthResource",
24
+ "AsyncAuthResource",
25
+ "EnvironmentsResource",
26
+ "AsyncEnvironmentsResource",
27
+ "ScenesResource",
28
+ "AsyncScenesResource",
29
+ "ExecutionsResource",
30
+ "AsyncExecutionsResource",
31
+ "UsersResource",
32
+ "AsyncUsersResource",
33
+ "OrganizationsResource",
34
+ "AsyncOrganizationsResource",
35
+ "RolesResource",
36
+ "AsyncRolesResource",
37
+ "TeamsResource",
38
+ "AsyncTeamsResource",
39
+ "ProjectsResource",
40
+ "AsyncProjectsResource",
41
+ ]
@@ -0,0 +1,30 @@
1
+ """Base resource classes for the Waveium SDK."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .._http import AsyncHTTPClient, HTTPClient
7
+
8
+
9
+ class BaseResource:
10
+ """Base class for synchronous API resources."""
11
+
12
+ def __init__(self, http_client: "HTTPClient") -> None:
13
+ """Initialize base resource.
14
+
15
+ Args:
16
+ http_client: HTTP client instance
17
+ """
18
+ self._http = http_client
19
+
20
+
21
+ class AsyncBaseResource:
22
+ """Base class for asynchronous API resources."""
23
+
24
+ def __init__(self, http_client: "AsyncHTTPClient") -> None:
25
+ """Initialize async base resource.
26
+
27
+ Args:
28
+ http_client: Async HTTP client instance
29
+ """
30
+ self._http = http_client
@@ -0,0 +1,371 @@
1
+ """Reusable file operation mixins for resources."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Optional, Union
6
+
7
+ import anyio
8
+ import httpx
9
+
10
+ from ..exceptions import FileDownloadError
11
+ from ..models.files import (
12
+ BatchDownloadFile,
13
+ BatchDownloadResponse,
14
+ FileDownloadResponse,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from .._http import AsyncHTTPClient, HTTPClient
19
+
20
+
21
+ class FileOperations:
22
+ """Sync file operations for a resource."""
23
+
24
+ def __init__(self, http: "HTTPClient", base_path: str) -> None:
25
+ self._http = http
26
+ self._base_path = base_path
27
+
28
+ def list_files(
29
+ self,
30
+ resource_id: str,
31
+ subdir: Optional[str] = None,
32
+ response_model: type = None, # type: ignore[assignment]
33
+ ) -> object:
34
+ """List files for a resource.
35
+
36
+ Args:
37
+ resource_id: The resource identifier.
38
+ subdir: Optional subdirectory filter.
39
+ response_model: Pydantic model for response.
40
+
41
+ Returns:
42
+ Parsed response object.
43
+ """
44
+ params = {}
45
+ if subdir:
46
+ params["subdir"] = subdir
47
+ return self._http.request(
48
+ "GET",
49
+ f"{self._base_path}/{resource_id}/files",
50
+ params=params if params else None,
51
+ response_model=response_model,
52
+ )
53
+
54
+ def get_download_url(
55
+ self, resource_id: str, file_id: str
56
+ ) -> FileDownloadResponse:
57
+ """Get a signed download URL for a file.
58
+
59
+ Args:
60
+ resource_id: The resource identifier.
61
+ file_id: The file identifier.
62
+
63
+ Returns:
64
+ Download URL response with signed URL.
65
+ """
66
+ return self._http.request(
67
+ "GET",
68
+ (f"{self._base_path}/{resource_id}" f"/files/{file_id}/download"),
69
+ response_model=FileDownloadResponse,
70
+ )
71
+
72
+ def download(
73
+ self,
74
+ resource_id: str,
75
+ file_id: str,
76
+ destination: Union[str, Path],
77
+ ) -> Path:
78
+ """Download a file to a local path.
79
+
80
+ Args:
81
+ resource_id: The resource identifier.
82
+ file_id: The file identifier.
83
+ destination: Local file path to write to.
84
+
85
+ Returns:
86
+ Path to the downloaded file.
87
+
88
+ Raises:
89
+ FileDownloadError: If download fails.
90
+ """
91
+ resp = self.get_download_url(resource_id, file_id)
92
+ destination = Path(destination)
93
+ destination.parent.mkdir(parents=True, exist_ok=True)
94
+ try:
95
+ with httpx.Client() as client:
96
+ r = client.request(
97
+ method=resp.method,
98
+ url=resp.download_url,
99
+ )
100
+ r.raise_for_status()
101
+ with open(destination, "wb") as f:
102
+ f.write(r.content)
103
+ return destination
104
+ except Exception as e:
105
+ raise FileDownloadError(f"Failed to download file: {e}")
106
+
107
+ def read_bytes(
108
+ self,
109
+ resource_id: str,
110
+ file_id: str,
111
+ ) -> bytes:
112
+ """Read a file's content into memory.
113
+
114
+ Fetches the file via its signed download URL and
115
+ returns the raw bytes without writing to disk.
116
+ Suitable for small-to-medium files (configs, CSVs,
117
+ metadata). For large files, use download() instead.
118
+
119
+ Args:
120
+ resource_id: The resource identifier.
121
+ file_id: The file identifier.
122
+
123
+ Returns:
124
+ File content as bytes.
125
+
126
+ Raises:
127
+ FileDownloadError: If download fails.
128
+ """
129
+ resp = self.get_download_url(resource_id, file_id)
130
+ try:
131
+ with httpx.Client() as client:
132
+ r = client.request(
133
+ method=resp.method,
134
+ url=resp.download_url,
135
+ )
136
+ r.raise_for_status()
137
+ return r.content
138
+ except Exception as e:
139
+ raise FileDownloadError(f"Failed to read file: {e}")
140
+
141
+ def download_batch(
142
+ self,
143
+ resource_id: str,
144
+ destination_dir: Union[str, Path],
145
+ subdir: Optional[str] = None,
146
+ ) -> BatchDownloadResponse:
147
+ """Download all files for a resource.
148
+
149
+ Args:
150
+ resource_id: The resource identifier.
151
+ destination_dir: Local directory to write to.
152
+ subdir: Optional subdirectory filter.
153
+
154
+ Returns:
155
+ Batch download response metadata.
156
+
157
+ Raises:
158
+ FileDownloadError: If any download fails.
159
+ """
160
+ params = {}
161
+ if subdir:
162
+ params["subdir"] = subdir
163
+ batch: BatchDownloadResponse = self._http.request(
164
+ "POST",
165
+ f"{self._base_path}/{resource_id}/downloads",
166
+ params=params if params else None,
167
+ response_model=BatchDownloadResponse,
168
+ )
169
+ dest = Path(destination_dir)
170
+ dest.mkdir(parents=True, exist_ok=True)
171
+ with httpx.Client() as client:
172
+ for fi in batch.files:
173
+ try:
174
+ r = client.request(
175
+ method=fi.method,
176
+ url=fi.download_url,
177
+ )
178
+ r.raise_for_status()
179
+ fp = (dest / fi.filename).resolve()
180
+ if not str(fp).startswith(str(dest.resolve())):
181
+ raise FileDownloadError(
182
+ f"Unsafe filename rejected: " f"{fi.filename}"
183
+ )
184
+ with open(fp, "wb") as f:
185
+ f.write(r.content)
186
+ except FileDownloadError:
187
+ raise
188
+ except Exception as e:
189
+ raise FileDownloadError(
190
+ f"Failed to download {fi.filename}: {e}"
191
+ )
192
+ return batch
193
+
194
+
195
+ class AsyncFileOperations:
196
+ """Async file operations for a resource."""
197
+
198
+ def __init__(self, http: "AsyncHTTPClient", base_path: str) -> None:
199
+ self._http = http
200
+ self._base_path = base_path
201
+
202
+ async def list_files(
203
+ self,
204
+ resource_id: str,
205
+ subdir: Optional[str] = None,
206
+ response_model: type = None, # type: ignore[assignment]
207
+ ) -> object:
208
+ """List files for a resource.
209
+
210
+ Args:
211
+ resource_id: The resource identifier.
212
+ subdir: Optional subdirectory filter.
213
+ response_model: Pydantic model for response.
214
+
215
+ Returns:
216
+ Parsed response object.
217
+ """
218
+ params = {}
219
+ if subdir:
220
+ params["subdir"] = subdir
221
+ return await self._http.request(
222
+ "GET",
223
+ f"{self._base_path}/{resource_id}/files",
224
+ params=params if params else None,
225
+ response_model=response_model,
226
+ )
227
+
228
+ async def get_download_url(
229
+ self, resource_id: str, file_id: str
230
+ ) -> FileDownloadResponse:
231
+ """Get a signed download URL for a file.
232
+
233
+ Args:
234
+ resource_id: The resource identifier.
235
+ file_id: The file identifier.
236
+
237
+ Returns:
238
+ Download URL response with signed URL.
239
+ """
240
+ return await self._http.request(
241
+ "GET",
242
+ (f"{self._base_path}/{resource_id}" f"/files/{file_id}/download"),
243
+ response_model=FileDownloadResponse,
244
+ )
245
+
246
+ async def read_bytes(
247
+ self,
248
+ resource_id: str,
249
+ file_id: str,
250
+ ) -> bytes:
251
+ """Read a file's content into memory.
252
+
253
+ Async counterpart of FileOperations.read_bytes().
254
+
255
+ Args:
256
+ resource_id: The resource identifier.
257
+ file_id: The file identifier.
258
+
259
+ Returns:
260
+ File content as bytes.
261
+
262
+ Raises:
263
+ FileDownloadError: If download fails.
264
+ """
265
+ resp = await self.get_download_url(resource_id, file_id)
266
+ try:
267
+ async with httpx.AsyncClient() as client:
268
+ r = await client.request(
269
+ method=resp.method,
270
+ url=resp.download_url,
271
+ )
272
+ r.raise_for_status()
273
+ return r.content
274
+ except Exception as e:
275
+ raise FileDownloadError(f"Failed to read file: {e}")
276
+
277
+ async def download(
278
+ self,
279
+ resource_id: str,
280
+ file_id: str,
281
+ destination: Union[str, Path],
282
+ ) -> Path:
283
+ """Download a file to a local path.
284
+
285
+ Args:
286
+ resource_id: The resource identifier.
287
+ file_id: The file identifier.
288
+ destination: Local file path to write to.
289
+
290
+ Returns:
291
+ Path to the downloaded file.
292
+
293
+ Raises:
294
+ FileDownloadError: If download fails.
295
+ """
296
+ resp = await self.get_download_url(resource_id, file_id)
297
+ destination = Path(destination)
298
+ await anyio.Path(destination.parent).mkdir(parents=True, exist_ok=True)
299
+ try:
300
+ async with httpx.AsyncClient() as client:
301
+ r = await client.request(
302
+ method=resp.method,
303
+ url=resp.download_url,
304
+ )
305
+ r.raise_for_status()
306
+ async with await anyio.open_file(destination, "wb") as f:
307
+ await f.write(r.content)
308
+ return destination
309
+ except Exception as e:
310
+ raise FileDownloadError(f"Failed to download file: {e}")
311
+
312
+ async def download_batch(
313
+ self,
314
+ resource_id: str,
315
+ destination_dir: Union[str, Path],
316
+ subdir: Optional[str] = None,
317
+ ) -> BatchDownloadResponse:
318
+ """Download all files for a resource.
319
+
320
+ Args:
321
+ resource_id: The resource identifier.
322
+ destination_dir: Local directory to write to.
323
+ subdir: Optional subdirectory filter.
324
+
325
+ Returns:
326
+ Batch download response metadata.
327
+
328
+ Raises:
329
+ FileDownloadError: If any download fails.
330
+ """
331
+ params = {}
332
+ if subdir:
333
+ params["subdir"] = subdir
334
+ batch: BatchDownloadResponse = await self._http.request(
335
+ "POST",
336
+ (f"{self._base_path}/{resource_id}" f"/downloads"),
337
+ params=params if params else None,
338
+ response_model=BatchDownloadResponse,
339
+ )
340
+ dest = Path(destination_dir)
341
+ await anyio.Path(dest).mkdir(parents=True, exist_ok=True)
342
+
343
+ async def _download_one(
344
+ client: httpx.AsyncClient,
345
+ fi: BatchDownloadFile,
346
+ ) -> None:
347
+ try:
348
+ r = await client.request(
349
+ method=fi.method,
350
+ url=fi.download_url,
351
+ )
352
+ r.raise_for_status()
353
+ fp = (dest / fi.filename).resolve()
354
+ if not str(fp).startswith(str(dest.resolve())):
355
+ raise FileDownloadError(
356
+ f"Unsafe filename rejected: " f"{fi.filename}"
357
+ )
358
+ async with await anyio.open_file(fp, "wb") as f:
359
+ await f.write(r.content)
360
+ except FileDownloadError:
361
+ raise
362
+ except Exception as e:
363
+ raise FileDownloadError(
364
+ f"Failed to download {fi.filename}: {e}"
365
+ )
366
+
367
+ async with httpx.AsyncClient() as client:
368
+ await asyncio.gather(
369
+ *[_download_one(client, fi) for fi in batch.files]
370
+ )
371
+ return batch
@@ -0,0 +1,175 @@
1
+ """Reusable share operation mixins for resources."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from ..models.common import MessageResponse, PermissionLevel, ShareTargetType
6
+ from ..models.shares import (
7
+ CreateShareRequest,
8
+ PaginatedShareResponse,
9
+ ShareResponse,
10
+ )
11
+
12
+ if TYPE_CHECKING:
13
+ from .._http import AsyncHTTPClient, HTTPClient
14
+
15
+
16
+ class ShareOperations:
17
+ """Sync share operations for a resource."""
18
+
19
+ def __init__(self, http: "HTTPClient", base_path: str) -> None:
20
+ self._http = http
21
+ self._base_path = base_path
22
+
23
+ def create_share(
24
+ self,
25
+ resource_id: str,
26
+ shared_with_type: ShareTargetType,
27
+ shared_with_id: str,
28
+ permission_level: PermissionLevel,
29
+ ) -> ShareResponse:
30
+ """Create a share for a resource.
31
+
32
+ Args:
33
+ resource_id: The resource identifier.
34
+ shared_with_type: Target type (user/team/org).
35
+ shared_with_id: Target entity identifier.
36
+ permission_level: Permission (view/use/full).
37
+
38
+ Returns:
39
+ The created share record.
40
+ """
41
+ request = CreateShareRequest(
42
+ shared_with_type=shared_with_type,
43
+ shared_with_id=shared_with_id,
44
+ permission_level=permission_level,
45
+ )
46
+ return self._http.request(
47
+ "POST",
48
+ f"{self._base_path}/{resource_id}/shares",
49
+ json_data=request.model_dump(),
50
+ response_model=ShareResponse,
51
+ )
52
+
53
+ def list_shares(
54
+ self,
55
+ resource_id: str,
56
+ limit: int = 50,
57
+ offset: int = 0,
58
+ ) -> PaginatedShareResponse:
59
+ """List shares for a resource.
60
+
61
+ Args:
62
+ resource_id: The resource identifier.
63
+ limit: Maximum number of results.
64
+ offset: Number of results to skip.
65
+
66
+ Returns:
67
+ Paginated list of share records.
68
+ """
69
+ return self._http.request(
70
+ "GET",
71
+ f"{self._base_path}/{resource_id}/shares",
72
+ params={"limit": limit, "offset": offset},
73
+ response_model=PaginatedShareResponse,
74
+ )
75
+
76
+ def revoke_share(
77
+ self,
78
+ resource_id: str,
79
+ share_id: str,
80
+ ) -> MessageResponse:
81
+ """Revoke a share for a resource.
82
+
83
+ Args:
84
+ resource_id: The resource identifier.
85
+ share_id: The share identifier to revoke.
86
+
87
+ Returns:
88
+ Confirmation message response.
89
+ """
90
+ return self._http.request(
91
+ "DELETE",
92
+ (f"{self._base_path}/{resource_id}" f"/shares/{share_id}"),
93
+ response_model=MessageResponse,
94
+ )
95
+
96
+
97
+ class AsyncShareOperations:
98
+ """Async share operations for a resource."""
99
+
100
+ def __init__(self, http: "AsyncHTTPClient", base_path: str) -> None:
101
+ self._http = http
102
+ self._base_path = base_path
103
+
104
+ async def create_share(
105
+ self,
106
+ resource_id: str,
107
+ shared_with_type: ShareTargetType,
108
+ shared_with_id: str,
109
+ permission_level: PermissionLevel,
110
+ ) -> ShareResponse:
111
+ """Create a share for a resource.
112
+
113
+ Args:
114
+ resource_id: The resource identifier.
115
+ shared_with_type: Target type (user/team/org).
116
+ shared_with_id: Target entity identifier.
117
+ permission_level: Permission (view/use/full).
118
+
119
+ Returns:
120
+ The created share record.
121
+ """
122
+ request = CreateShareRequest(
123
+ shared_with_type=shared_with_type,
124
+ shared_with_id=shared_with_id,
125
+ permission_level=permission_level,
126
+ )
127
+ return await self._http.request(
128
+ "POST",
129
+ f"{self._base_path}/{resource_id}/shares",
130
+ json_data=request.model_dump(),
131
+ response_model=ShareResponse,
132
+ )
133
+
134
+ async def list_shares(
135
+ self,
136
+ resource_id: str,
137
+ limit: int = 50,
138
+ offset: int = 0,
139
+ ) -> PaginatedShareResponse:
140
+ """List shares for a resource.
141
+
142
+ Args:
143
+ resource_id: The resource identifier.
144
+ limit: Maximum number of results.
145
+ offset: Number of results to skip.
146
+
147
+ Returns:
148
+ Paginated list of share records.
149
+ """
150
+ return await self._http.request(
151
+ "GET",
152
+ f"{self._base_path}/{resource_id}/shares",
153
+ params={"limit": limit, "offset": offset},
154
+ response_model=PaginatedShareResponse,
155
+ )
156
+
157
+ async def revoke_share(
158
+ self,
159
+ resource_id: str,
160
+ share_id: str,
161
+ ) -> MessageResponse:
162
+ """Revoke a share for a resource.
163
+
164
+ Args:
165
+ resource_id: The resource identifier.
166
+ share_id: The share identifier to revoke.
167
+
168
+ Returns:
169
+ Confirmation message response.
170
+ """
171
+ return await self._http.request(
172
+ "DELETE",
173
+ (f"{self._base_path}/{resource_id}" f"/shares/{share_id}"),
174
+ response_model=MessageResponse,
175
+ )