shadowob-sdk 1.1.3.dev271__tar.gz → 1.1.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shadowob-sdk
3
- Version: 1.1.3.dev271
3
+ Version: 1.1.4
4
4
  Summary: Shadow SDK — Python client for Shadow server REST API and Socket.IO real-time events
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "shadowob-sdk"
7
- version = "1.1.3.dev271"
7
+ version = "1.1.4"
8
8
  description = "Shadow SDK — Python client for Shadow server REST API and Socket.IO real-time events"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -20,6 +20,7 @@ from shadowob_sdk.types import (
20
20
  ShadowModelProxyBilling,
21
21
  ShadowModelProxyModel,
22
22
  ShadowModelProxyModelsResponse,
23
+ ShadowServerAppTokenIntrospection,
23
24
  ShadowServerAccess,
24
25
  ShadowShop,
25
26
  ShadowUser,
@@ -47,6 +48,7 @@ __all__ = [
47
48
  "ShadowModelProxyBilling",
48
49
  "ShadowModelProxyModel",
49
50
  "ShadowModelProxyModelsResponse",
51
+ "ShadowServerAppTokenIntrospection",
50
52
  "ShadowServerAccess",
51
53
  "ShadowShop",
52
54
  "ShadowSocket",
@@ -248,9 +248,23 @@ class ShadowClient:
248
248
  def list_oauth_accounts(self) -> list[dict[str, Any]]:
249
249
  return self._get("/api/auth/oauth/accounts")
250
250
 
251
+ def create_oauth_connect_url(
252
+ self, provider: str, *, redirect: str | None = None
253
+ ) -> dict[str, Any]:
254
+ payload: dict[str, Any] = {}
255
+ if redirect is not None:
256
+ payload["redirect"] = redirect
257
+ return self._post(f"/api/auth/oauth/{provider}/link", json=payload)
258
+
251
259
  def unlink_oauth_account(self, account_id: str) -> dict[str, Any]:
252
260
  return self._delete(f"/api/auth/oauth/accounts/{account_id}")
253
261
 
262
+ def list_auth_sessions(self) -> list[dict[str, Any]]:
263
+ return self._get("/api/auth/sessions")
264
+
265
+ def revoke_auth_session(self, session_id: str) -> dict[str, Any]:
266
+ return self._delete(f"/api/auth/sessions/{session_id}")
267
+
254
268
  # ── Agents ───────────────────────────────────────────────────────────
255
269
 
256
270
  def list_agents(self, *, include_rentals: bool = False) -> list[dict[str, Any]]:
@@ -404,6 +418,123 @@ class ShadowClient:
404
418
  def get_server_access(self, server_id_or_slug: str) -> dict[str, Any]:
405
419
  return self._get(f"/api/servers/{server_id_or_slug}/access")
406
420
 
421
+ def list_server_apps(self, server_id_or_slug: str) -> list[dict[str, Any]]:
422
+ return self._get(f"/api/servers/{server_id_or_slug}/apps")
423
+
424
+ def list_server_app_catalog(self, server_id_or_slug: str) -> list[dict[str, Any]]:
425
+ return self._get(f"/api/servers/{server_id_or_slug}/apps/catalog")
426
+
427
+ def discover_server_app(
428
+ self,
429
+ server_id_or_slug: str,
430
+ *,
431
+ manifest_url: str | None = None,
432
+ manifest: dict[str, Any] | None = None,
433
+ ) -> dict[str, Any]:
434
+ payload: dict[str, Any] = {}
435
+ if manifest_url:
436
+ payload["manifestUrl"] = manifest_url
437
+ if manifest is not None:
438
+ payload["manifest"] = manifest
439
+ return self._post(f"/api/servers/{server_id_or_slug}/apps/discover", json=payload)
440
+
441
+ def install_server_app(
442
+ self,
443
+ server_id_or_slug: str,
444
+ *,
445
+ manifest_url: str | None = None,
446
+ manifest: dict[str, Any] | None = None,
447
+ ) -> dict[str, Any]:
448
+ payload: dict[str, Any] = {}
449
+ if manifest_url:
450
+ payload["manifestUrl"] = manifest_url
451
+ if manifest is not None:
452
+ payload["manifest"] = manifest
453
+ return self._post(f"/api/servers/{server_id_or_slug}/apps", json=payload)
454
+
455
+ def install_server_app_from_catalog(
456
+ self,
457
+ server_id_or_slug: str,
458
+ catalog_entry_id: str,
459
+ ) -> dict[str, Any]:
460
+ payload: dict[str, Any] = {}
461
+ return self._post(
462
+ f"/api/servers/{server_id_or_slug}/apps/catalog/{catalog_entry_id}/install",
463
+ json=payload,
464
+ )
465
+
466
+ def get_server_app(self, server_id_or_slug: str, app_key: str) -> dict[str, Any]:
467
+ return self._get(f"/api/servers/{server_id_or_slug}/apps/{app_key}")
468
+
469
+ def delete_server_app(self, server_id_or_slug: str, app_key: str) -> dict[str, Any]:
470
+ return self._delete(f"/api/servers/{server_id_or_slug}/apps/{app_key}")
471
+
472
+ def grant_server_app_to_buddy(
473
+ self,
474
+ server_id_or_slug: str,
475
+ app_key: str,
476
+ *,
477
+ buddy_agent_id: str,
478
+ permissions: list[str],
479
+ resource_rules: dict[str, Any] | None = None,
480
+ approval_mode: str = "none",
481
+ expires_at: str | None = None,
482
+ ) -> dict[str, Any]:
483
+ payload: dict[str, Any] = {
484
+ "buddyAgentId": buddy_agent_id,
485
+ "permissions": permissions,
486
+ "approvalMode": approval_mode,
487
+ }
488
+ if resource_rules is not None:
489
+ payload["resourceRules"] = resource_rules
490
+ if expires_at:
491
+ payload["expiresAt"] = expires_at
492
+ return self._post(
493
+ f"/api/servers/{server_id_or_slug}/apps/{app_key}/grants",
494
+ json=payload,
495
+ )
496
+
497
+ def get_server_app_skills(
498
+ self, server_id_or_slug: str, app_key: str
499
+ ) -> dict[str, Any]:
500
+ return self._get(f"/api/servers/{server_id_or_slug}/apps/{app_key}/skills")
501
+
502
+ def create_server_app_launch(
503
+ self, server_id_or_slug: str, app_key: str
504
+ ) -> dict[str, Any]:
505
+ return self._post(f"/api/servers/{server_id_or_slug}/apps/{app_key}/launch")
506
+
507
+ def introspect_server_app_token(
508
+ self, server_id_or_slug: str, app_key: str, token: str
509
+ ) -> dict[str, Any]:
510
+ response = self._http.post(
511
+ f"/api/servers/{server_id_or_slug}/apps/{app_key}/oauth/introspect",
512
+ headers={
513
+ "Authorization": f"Bearer {token}",
514
+ "Content-Type": "application/json",
515
+ },
516
+ json={"token": token},
517
+ )
518
+ response.raise_for_status()
519
+ return response.json()
520
+
521
+ def call_server_app_command(
522
+ self,
523
+ server_id_or_slug: str,
524
+ app_key: str,
525
+ command_name: str,
526
+ *,
527
+ input: Any | None = None,
528
+ channel_id: str | None = None,
529
+ ) -> dict[str, Any]:
530
+ payload: dict[str, Any] = {"input": input if input is not None else {}}
531
+ if channel_id:
532
+ payload["channelId"] = channel_id
533
+ return self._post(
534
+ f"/api/servers/{server_id_or_slug}/apps/{app_key}/commands/{command_name}",
535
+ json=payload,
536
+ )
537
+
407
538
  def update_server(self, server_id: str, **kwargs: Any) -> dict[str, Any]:
408
539
  return self._patch(f"/api/servers/{server_id}", json=kwargs)
409
540
 
@@ -474,6 +605,17 @@ class ShadowClient:
474
605
  def get_channel(self, channel_id: str) -> dict[str, Any]:
475
606
  return self._get(f"/api/channels/{channel_id}")
476
607
 
608
+ def get_channel_bootstrap(
609
+ self,
610
+ channel_id: str,
611
+ *,
612
+ messages_limit: int | None = None,
613
+ ) -> dict[str, Any]:
614
+ params: dict[str, Any] = {}
615
+ if messages_limit is not None:
616
+ params["messagesLimit"] = messages_limit
617
+ return self._get(f"/api/channels/{channel_id}/bootstrap", params=params or None)
618
+
477
619
  def get_channel_access(self, channel_id: str) -> dict[str, Any]:
478
620
  return self._get(f"/api/channels/{channel_id}/access")
479
621
 
@@ -858,9 +1000,13 @@ class ShadowClient:
858
1000
  attachment_id: str,
859
1001
  *,
860
1002
  disposition: str = "inline",
1003
+ variant: str | None = None,
861
1004
  ) -> dict[str, Any]:
862
1005
  path = f"/api/attachments/{attachment_id}/media-url"
863
- return self._get(path, params={"disposition": disposition})
1006
+ params: dict[str, Any] = {"disposition": disposition}
1007
+ if variant:
1008
+ params["variant"] = variant
1009
+ return self._get(path, params=params)
864
1010
 
865
1011
  def resolve_workspace_media_url(
866
1012
  self,
@@ -114,7 +114,6 @@ class ShadowServer:
114
114
  description: str | None = None
115
115
  icon_url: str | None = None
116
116
  banner_url: str | None = None
117
- homepage_html: str | None = None
118
117
  is_public: bool = False
119
118
 
120
119
 
@@ -171,6 +170,20 @@ class ShadowSignedMediaUrl:
171
170
  expires_at: str
172
171
 
173
172
 
173
+ @dataclass
174
+ class ShadowServerAppTokenIntrospection:
175
+ active: bool
176
+ token_type: str | None = None
177
+ iss: str | None = None
178
+ aud: str | None = None
179
+ sub: str | None = None
180
+ scope: str | None = None
181
+ client_id: str | None = None
182
+ exp: int | None = None
183
+ iat: int | None = None
184
+ shadow: dict[str, Any] | None = None
185
+
186
+
174
187
  @dataclass
175
188
  class ShadowMessageMention:
176
189
  kind: str
@@ -184,6 +197,10 @@ class ShadowMessageMention:
184
197
  server_name: str | None = None
185
198
  channel_id: str | None = None
186
199
  channel_name: str | None = None
200
+ app_id: str | None = None
201
+ app_key: str | None = None
202
+ app_name: str | None = None
203
+ icon_url: str | None = None
187
204
  user_id: str | None = None
188
205
  username: str | None = None
189
206
  display_name: str | None = None
@@ -205,6 +222,10 @@ class ShadowMentionSuggestion:
205
222
  server_name: str | None = None
206
223
  channel_id: str | None = None
207
224
  channel_name: str | None = None
225
+ app_id: str | None = None
226
+ app_key: str | None = None
227
+ app_name: str | None = None
228
+ icon_url: str | None = None
208
229
  user_id: str | None = None
209
230
  username: str | None = None
210
231
  display_name: str | None = None
@@ -129,6 +129,61 @@ def test_get_wallet_transactions_with_display_filters(monkeypatch):
129
129
  client.close()
130
130
 
131
131
 
132
+ def test_resolve_attachment_media_url_accepts_variant(monkeypatch):
133
+ client = ShadowClient("https://example.com", "test-token")
134
+ captured = {}
135
+
136
+ def fake_get(path, *, params=None):
137
+ captured["path"] = path
138
+ captured["params"] = params
139
+ return {
140
+ "url": "/api/media/signed/token",
141
+ "expiresAt": "2026-05-13T04:00:00.000Z",
142
+ }
143
+
144
+ monkeypatch.setattr(client, "_get", fake_get)
145
+
146
+ result = client.resolve_attachment_media_url(
147
+ "attachment-1", disposition="inline", variant="preview"
148
+ )
149
+
150
+ assert captured == {
151
+ "path": "/api/attachments/attachment-1/media-url",
152
+ "params": {"disposition": "inline", "variant": "preview"},
153
+ }
154
+ assert result["url"] == "/api/media/signed/token"
155
+ client.close()
156
+
157
+
158
+ def test_get_channel_bootstrap_uses_message_limit(monkeypatch):
159
+ client = ShadowClient("https://example.com", "test-token")
160
+ captured = {}
161
+
162
+ def fake_get(path, *, params=None):
163
+ captured["path"] = path
164
+ captured["params"] = params
165
+ return {
166
+ "access": {"canAccess": True},
167
+ "channel": {"id": "channel-1"},
168
+ "server": None,
169
+ "channels": [],
170
+ "members": [],
171
+ "messages": {"messages": [], "hasMore": False},
172
+ "slashCommands": {"commands": []},
173
+ }
174
+
175
+ monkeypatch.setattr(client, "_get", fake_get)
176
+
177
+ result = client.get_channel_bootstrap("channel-1", messages_limit=50)
178
+
179
+ assert captured == {
180
+ "path": "/api/channels/channel-1/bootstrap",
181
+ "params": {"messagesLimit": 50},
182
+ }
183
+ assert result["messages"]["hasMore"] is False
184
+ client.close()
185
+
186
+
132
187
  def test_socket_creation():
133
188
  sock = ShadowSocket("https://example.com", "test-token")
134
189
  assert sock.connected is False