@palettelab/cli 0.3.24 → 0.3.26

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.
package/README.md CHANGED
@@ -266,12 +266,45 @@ Backend SDK features for app-owned data:
266
266
 
267
267
  - `PluginContext` exposes `user_id`, `organization_id`, `plugin_id`, `permissions`, `config`, `storage`, and `ctx.db`.
268
268
  - `ctx.repo(Model)` gives org-safe CRUD helpers for app tables.
269
- - `ctx.has_permission("...")` checks declared permissions.
269
+ - `ctx.data_rooms` gives backend access to Palette Data Rooms without importing platform internals.
270
+ - `ctx.has_permission("...")`, `ctx.has_any_permission([...])`, and `ctx.has_all_permissions([...])` check declared permissions.
270
271
  - `ctx.config_value("key")` and `ctx.require_config("key")` read app install/config values.
271
272
  - `ctx.secret("KEY")` reads app secrets from config or environment variables.
272
273
  - `LifecycleHooks` lets apps define install/update/enable/disable/uninstall hooks.
273
274
  - `OrgScopedTable` and `PluginBase` keep app data inside the plugin schema model set.
274
275
 
276
+ Python backend Data Room example:
277
+
278
+ ```python
279
+ @router.post("/sync-invoices", dependencies=[require_permission("data_rooms:write")])
280
+ async def sync_invoices(ctx: PluginContext = Depends(get_plugin_context)):
281
+ room = await ctx.data_rooms.ensure_room("Finance")
282
+ folder = await ctx.data_rooms.resolve_folder_path(
283
+ room["id"],
284
+ "Clients/Acme/Invoices",
285
+ create=True,
286
+ )
287
+ file = await ctx.data_rooms.find_file_by_name(
288
+ room["id"],
289
+ "jan.pdf",
290
+ folder_id=folder["id"] if folder else None,
291
+ )
292
+ content = await ctx.data_rooms.read_file_bytes(file["id"]) if file else None
293
+ if folder:
294
+ await ctx.data_rooms.upload_file(
295
+ room["id"],
296
+ "summary.txt",
297
+ b"Generated by my app",
298
+ folder_id=folder["id"],
299
+ content_type="text/plain",
300
+ )
301
+ return {"room": room, "folder": folder, "bytes": len(content or b"")}
302
+ ```
303
+
304
+ The npm `@palettelab/sdk` package is for frontend JavaScript/React apps.
305
+ Python backend code uses `palette_sdk`, which is embedded in the CLI for
306
+ local dev/tests and injected by the hosted Palette runtime.
307
+
275
308
  Lifecycle example:
276
309
 
277
310
  ```python
@@ -2,6 +2,7 @@
2
2
 
3
3
  from palette_sdk.plugin_router import PluginRouter
4
4
  from palette_sdk.plugin_context import PluginContext, get_plugin_context
5
+ from palette_sdk.data_rooms import DataRoomsClient
5
6
  from palette_sdk.repository import OrgRepository
6
7
  from palette_sdk.lifecycle import LifecycleHooks
7
8
  from palette_sdk.tool_definition import ToolDefinition
@@ -26,6 +27,7 @@ __all__ = [
26
27
  "PluginRouter",
27
28
  "PluginContext",
28
29
  "get_plugin_context",
30
+ "DataRoomsClient",
29
31
  "OrgRepository",
30
32
  "LifecycleHooks",
31
33
  "ToolDefinition",
@@ -49,4 +51,4 @@ __all__ = [
49
51
  "route_permission_issues",
50
52
  ]
51
53
 
52
- __version__ = "0.1.4"
54
+ __version__ = "0.1.6"
@@ -0,0 +1,127 @@
1
+ """Backend Data Room helpers for plugin Python code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class DataRoomsClient:
9
+ """Thin wrapper around the platform-injected Data Room service.
10
+
11
+ In production/hosted sandbox the platform injects the service into
12
+ `PluginContext`. In local unit tests, pass a fake service with the same
13
+ async methods.
14
+ """
15
+
16
+ def __init__(self, service: Any = None):
17
+ self._service = service
18
+
19
+ def _require_service(self) -> Any:
20
+ if self._service is None:
21
+ raise RuntimeError(
22
+ "Data Room service is not available in this runtime. "
23
+ "Run inside Palette OS/hosted sandbox or inject a fake service in tests."
24
+ )
25
+ return self._service
26
+
27
+ async def list(self) -> list[dict[str, Any]]:
28
+ return await self._require_service().list_rooms()
29
+
30
+ async def create(self, name: str, description: str | None = None) -> dict[str, Any]:
31
+ return await self._require_service().create_room(name, description)
32
+
33
+ async def find_room_by_name(self, name: str, *, case_sensitive: bool = False) -> dict[str, Any] | None:
34
+ return await self._require_service().find_room_by_name(name, case_sensitive=case_sensitive)
35
+
36
+ async def require_room_by_name(self, name: str, *, case_sensitive: bool = False) -> dict[str, Any]:
37
+ room = await self.find_room_by_name(name, case_sensitive=case_sensitive)
38
+ if room is None:
39
+ raise LookupError(f"Data Room not found: {name}")
40
+ return room
41
+
42
+ async def ensure_room(self, name: str, description: str | None = None) -> dict[str, Any]:
43
+ return await self._require_service().ensure_room(name, description)
44
+
45
+ async def contents(self, room_id: int, folder_id: int | None = None) -> dict[str, Any]:
46
+ return await self._require_service().contents(room_id, folder_id)
47
+
48
+ async def create_folder(
49
+ self,
50
+ room_id: int,
51
+ name: str,
52
+ parent_folder_id: int | None = None,
53
+ ) -> dict[str, Any]:
54
+ return await self._require_service().create_folder(room_id, name, parent_folder_id)
55
+
56
+ async def find_folder_by_name(
57
+ self,
58
+ room_id: int,
59
+ name: str,
60
+ *,
61
+ parent_folder_id: int | None = None,
62
+ case_sensitive: bool = False,
63
+ ) -> dict[str, Any] | None:
64
+ return await self._require_service().find_folder_by_name(
65
+ room_id,
66
+ name,
67
+ parent_folder_id=parent_folder_id,
68
+ case_sensitive=case_sensitive,
69
+ )
70
+
71
+ async def ensure_folder(
72
+ self,
73
+ room_id: int,
74
+ name: str,
75
+ parent_folder_id: int | None = None,
76
+ ) -> dict[str, Any]:
77
+ return await self._require_service().ensure_folder(room_id, name, parent_folder_id)
78
+
79
+ async def resolve_folder_path(
80
+ self,
81
+ room_id: int,
82
+ path: str | list[str],
83
+ *,
84
+ create: bool = False,
85
+ case_sensitive: bool = False,
86
+ ) -> dict[str, Any] | None:
87
+ return await self._require_service().resolve_folder_path(
88
+ room_id,
89
+ path,
90
+ create=create,
91
+ case_sensitive=case_sensitive,
92
+ )
93
+
94
+ async def find_file_by_name(
95
+ self,
96
+ room_id: int,
97
+ name: str,
98
+ *,
99
+ folder_id: int | None = None,
100
+ case_sensitive: bool = False,
101
+ ) -> dict[str, Any] | None:
102
+ return await self._require_service().find_file_by_name(
103
+ room_id,
104
+ name,
105
+ folder_id=folder_id,
106
+ case_sensitive=case_sensitive,
107
+ )
108
+
109
+ async def read_file_bytes(self, file_id: int) -> bytes:
110
+ return await self._require_service().read_file_bytes(file_id)
111
+
112
+ async def upload_file(
113
+ self,
114
+ room_id: int,
115
+ filename: str,
116
+ content: bytes,
117
+ *,
118
+ folder_id: int | None = None,
119
+ content_type: str | None = None,
120
+ ) -> dict[str, Any]:
121
+ return await self._require_service().upload_file(
122
+ room_id,
123
+ filename,
124
+ content,
125
+ folder_id=folder_id,
126
+ content_type=content_type,
127
+ )
@@ -10,6 +10,8 @@ from typing import Any
10
10
  from fastapi import Depends, Request
11
11
  from sqlalchemy.ext.asyncio import AsyncSession
12
12
 
13
+ from palette_sdk.data_rooms import DataRoomsClient
14
+
13
15
 
14
16
  @dataclass
15
17
  class PluginContext:
@@ -36,12 +38,19 @@ class PluginContext:
36
38
  plugin_id: str = ""
37
39
  permissions: list[str] = field(default_factory=list)
38
40
  storage: Any = None # Platform storage service injected at runtime
41
+ data_rooms: DataRoomsClient = field(default_factory=DataRoomsClient)
39
42
  config: dict[str, Any] = field(default_factory=dict)
40
43
  logger: logging.Logger = field(default_factory=lambda: logging.getLogger("palette_sdk.plugin"))
41
44
 
42
45
  def has_permission(self, permission: str) -> bool:
43
46
  return permission in self.permissions
44
47
 
48
+ def has_any_permission(self, permissions: list[str] | tuple[str, ...] | set[str]) -> bool:
49
+ return any(permission in self.permissions for permission in permissions)
50
+
51
+ def has_all_permissions(self, permissions: list[str] | tuple[str, ...] | set[str]) -> bool:
52
+ return all(permission in self.permissions for permission in permissions)
53
+
45
54
  def config_value(self, key: str, default: Any = None) -> Any:
46
55
  return self.config.get(key, default)
47
56
 
@@ -84,6 +93,7 @@ async def get_plugin_context(request: Request) -> PluginContext:
84
93
  plugin_id=getattr(state, "plugin_id", ""),
85
94
  permissions=getattr(state, "plugin_permissions", []),
86
95
  storage=getattr(state, "storage", None),
96
+ data_rooms=DataRoomsClient(getattr(state, "data_rooms", None)),
87
97
  config=getattr(state, "plugin_config", {}),
88
98
  logger=getattr(state, "plugin_logger", logging.getLogger(f"palette_sdk.plugin.{getattr(state, 'plugin_id', 'unknown')}")),
89
99
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "palette-sdk"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Palette Platform SDK for building backend plugins"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "description": "A Palette platform plugin",
6
6
  "dependencies": {
7
- "@palettelab/sdk": "^0.1.9"
7
+ "@palettelab/sdk": "^0.1.10"
8
8
  },
9
9
  "devDependencies": {
10
10
  "typescript": "^5.0.0",
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.9",
6
+ "@palettelab/sdk": "^0.1.10",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }
@@ -2,5 +2,5 @@
2
2
  "name": "my-db-plugin",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.9", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.10", "react": "^19.0.0" }
6
6
  }
@@ -2,5 +2,5 @@
2
2
  "name": "my-external-svc",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.9", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.10", "react": "^19.0.0" }
6
6
  }
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.9",
6
+ "@palettelab/sdk": "^0.1.10",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }