@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 +34 -1
- package/backend-sdk/palette_sdk/__init__.py +3 -1
- package/backend-sdk/palette_sdk/data_rooms.py +127 -0
- package/backend-sdk/palette_sdk/plugin_context.py +10 -0
- package/backend-sdk/pyproject.toml +1 -1
- package/package.json +1 -1
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
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.
|
|
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.
|
|
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
|
)
|
package/package.json
CHANGED