planelet-sdk 0.1.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,52 @@
1
+ from .planelet import Planelet
2
+ from .types import (
3
+ ActionContext,
4
+ CleanupContext,
5
+ HttpResponse,
6
+ Param,
7
+ ParamOption,
8
+ SetupContext,
9
+ WebhookContext,
10
+ WebhookError,
11
+ WebhookInfo,
12
+ WebhookRequest,
13
+ WebhookResult,
14
+ WebhookSkip,
15
+ )
16
+ from .trigger import TriggerBuilder
17
+
18
+
19
+ def create_planelet(
20
+ *,
21
+ id: str,
22
+ label: str,
23
+ icon: str | None = None,
24
+ icon_url: str | None = None,
25
+ description: str | None = None,
26
+ ) -> Planelet:
27
+ return Planelet(
28
+ id=id,
29
+ label=label,
30
+ icon=icon,
31
+ icon_url=icon_url,
32
+ description=description,
33
+ )
34
+
35
+
36
+ __all__ = [
37
+ "create_planelet",
38
+ "ActionContext",
39
+ "CleanupContext",
40
+ "HttpResponse",
41
+ "Param",
42
+ "ParamOption",
43
+ "Planelet",
44
+ "SetupContext",
45
+ "TriggerBuilder",
46
+ "WebhookContext",
47
+ "WebhookError",
48
+ "WebhookInfo",
49
+ "WebhookRequest",
50
+ "WebhookResult",
51
+ "WebhookSkip",
52
+ ]
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Awaitable
4
+
5
+ from .trigger import TriggerBuilder
6
+ from .types import ActionContext, Param
7
+
8
+
9
+ ActionHandler = Callable[[ActionContext], Awaitable[dict[str, Any]]]
10
+
11
+
12
+ class _ActionDef:
13
+ def __init__(
14
+ self,
15
+ id: str,
16
+ *,
17
+ label: str,
18
+ description: str | None,
19
+ icon: str | None,
20
+ icon_url: str | None,
21
+ parameters: dict[str, Param],
22
+ handler: ActionHandler,
23
+ ) -> None:
24
+ self.id = id
25
+ self.label = label
26
+ self.description = description
27
+ self.icon = icon
28
+ self.icon_url = icon_url
29
+ self.parameters = parameters
30
+ self.handler = handler
31
+
32
+
33
+ class Planelet:
34
+ def __init__(
35
+ self,
36
+ *,
37
+ id: str,
38
+ label: str,
39
+ icon: str | None = None,
40
+ icon_url: str | None = None,
41
+ description: str | None = None,
42
+ ) -> None:
43
+ self.id = id
44
+ self.label = label
45
+ self.icon = icon
46
+ self.icon_url = icon_url
47
+ self.description = description
48
+
49
+ self._actions: dict[str, _ActionDef] = {}
50
+ self._triggers: dict[str, TriggerBuilder] = {}
51
+
52
+ def action(
53
+ self,
54
+ id: str,
55
+ *,
56
+ label: str,
57
+ description: str | None = None,
58
+ icon: str | None = None,
59
+ icon_url: str | None = None,
60
+ parameters: dict[str, Param] | None = None,
61
+ ) -> Callable[[ActionHandler], ActionHandler]:
62
+ params = parameters or {}
63
+
64
+ def decorator(fn: ActionHandler) -> ActionHandler:
65
+ self._actions[id] = _ActionDef(
66
+ id=id,
67
+ label=label,
68
+ description=description,
69
+ icon=icon,
70
+ icon_url=icon_url,
71
+ parameters=params,
72
+ handler=fn,
73
+ )
74
+ return fn
75
+
76
+ return decorator
77
+
78
+ def trigger(
79
+ self,
80
+ id: str,
81
+ *,
82
+ label: str,
83
+ description: str | None = None,
84
+ icon: str | None = None,
85
+ icon_url: str | None = None,
86
+ parameters: dict[str, Param] | None = None,
87
+ ) -> TriggerBuilder:
88
+ builder = TriggerBuilder(
89
+ id,
90
+ label=label,
91
+ description=description,
92
+ icon=icon,
93
+ icon_url=icon_url,
94
+ parameters=parameters,
95
+ )
96
+ self._triggers[id] = builder
97
+ return builder
98
+
99
+ def listen(self, port: int = 3000) -> None:
100
+ from .server import build_app
101
+
102
+ import uvicorn
103
+
104
+ app = build_app(self)
105
+ uvicorn.run(app, host="0.0.0.0", port=port)
planelet_sdk/py.typed ADDED
File without changes
planelet_sdk/server.py ADDED
@@ -0,0 +1,268 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from typing import Any, TYPE_CHECKING
5
+
6
+ from fastapi import FastAPI, Request
7
+ from fastapi.responses import JSONResponse
8
+
9
+ from .types import (
10
+ ActionContext,
11
+ CleanupContext,
12
+ HttpResponse,
13
+ SetupContext,
14
+ WebhookContext,
15
+ WebhookError,
16
+ WebhookInfo,
17
+ WebhookRequest,
18
+ WebhookResult,
19
+ WebhookSkip,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from .planelet import Planelet
24
+
25
+
26
+ def _serialize_params(parameters: dict[str, Any]) -> list[dict[str, Any]]:
27
+ result = []
28
+ for param_id, param in parameters.items():
29
+ entry: dict[str, Any] = {
30
+ "id": param_id,
31
+ "label": param.label,
32
+ "type": param.type,
33
+ }
34
+ if param.description is not None:
35
+ entry["description"] = param.description
36
+ if param.required:
37
+ entry["required"] = True
38
+ if param.default is not None:
39
+ entry["default"] = param.default
40
+ if param.options is not None:
41
+ entry["options"] = [
42
+ {"label": o.label, "value": o.value} for o in param.options
43
+ ]
44
+ result.append(entry)
45
+ return result
46
+
47
+
48
+ def _serialize_http_response(resp: HttpResponse | None) -> dict[str, Any] | None:
49
+ if resp is None:
50
+ return None
51
+ out: dict[str, Any] = {"status": resp.status}
52
+ if resp.headers:
53
+ out["headers"] = resp.headers
54
+ if resp.body is not None:
55
+ out["body"] = resp.body
56
+ return out
57
+
58
+
59
+ def build_app(planelet: Planelet) -> FastAPI:
60
+ app = FastAPI(title=planelet.label)
61
+
62
+ @app.get("/manifest")
63
+ async def manifest() -> dict[str, Any]:
64
+ actions = []
65
+ for action_def in planelet._actions.values():
66
+ entry: dict[str, Any] = {
67
+ "id": action_def.id,
68
+ "label": action_def.label,
69
+ "parameters": _serialize_params(action_def.parameters),
70
+ }
71
+ if action_def.description:
72
+ entry["description"] = action_def.description
73
+ if action_def.icon:
74
+ entry["icon"] = action_def.icon
75
+ if action_def.icon_url:
76
+ entry["iconUrl"] = action_def.icon_url
77
+ actions.append(entry)
78
+
79
+ triggers = []
80
+ for trigger in planelet._triggers.values():
81
+ entry = {
82
+ "id": trigger.id,
83
+ "label": trigger.label,
84
+ "parameters": _serialize_params(trigger.parameters),
85
+ "webhook": {"setup": "plugin"},
86
+ }
87
+ if trigger.description:
88
+ entry["description"] = trigger.description
89
+ if trigger.icon:
90
+ entry["icon"] = trigger.icon
91
+ if trigger.icon_url:
92
+ entry["iconUrl"] = trigger.icon_url
93
+ triggers.append(entry)
94
+
95
+ result: dict[str, Any] = {
96
+ "id": planelet.id,
97
+ "label": planelet.label,
98
+ "actions": actions,
99
+ "triggers": triggers,
100
+ }
101
+ if planelet.icon:
102
+ result["icon"] = planelet.icon
103
+ if planelet.icon_url:
104
+ result["iconUrl"] = planelet.icon_url
105
+ if planelet.description:
106
+ result["description"] = planelet.description
107
+ return result
108
+
109
+ @app.post("/actions/{action_id}/execute")
110
+ async def execute_action(action_id: str, request: Request) -> JSONResponse:
111
+ action_def = planelet._actions.get(action_id)
112
+ if not action_def:
113
+ return JSONResponse(
114
+ {"success": False, "error": f'Action "{action_id}" not found'},
115
+ status_code=404,
116
+ )
117
+
118
+ body = await request.json()
119
+ ctx = ActionContext(
120
+ parameters=body.get("parameters", {}),
121
+ input=body.get("input"),
122
+ )
123
+
124
+ try:
125
+ data = await action_def.handler(ctx)
126
+ return JSONResponse({"success": True, "data": data})
127
+ except Exception as exc:
128
+ return JSONResponse(
129
+ {"success": False, "error": str(exc)},
130
+ status_code=500,
131
+ )
132
+
133
+ @app.post("/triggers/{trigger_id}/setup")
134
+ async def setup_trigger(trigger_id: str, request: Request) -> JSONResponse:
135
+ trigger = planelet._triggers.get(trigger_id)
136
+ if not trigger:
137
+ return JSONResponse(
138
+ {"success": False, "error": f'Trigger "{trigger_id}" not found'},
139
+ status_code=404,
140
+ )
141
+ if not trigger._setup:
142
+ return JSONResponse(
143
+ {"success": False, "error": f'Trigger "{trigger_id}" has no setup handler'},
144
+ status_code=501,
145
+ )
146
+
147
+ body = await request.json()
148
+ webhook_data = body.get("webhook", {})
149
+ ctx = SetupContext(
150
+ parameters=body.get("parameters", {}),
151
+ webhook=WebhookInfo(
152
+ url=webhook_data.get("url", ""),
153
+ secret=webhook_data.get("secret"),
154
+ ),
155
+ )
156
+
157
+ try:
158
+ metadata = await trigger._setup(ctx)
159
+ result: dict[str, Any] = {"success": True}
160
+ if metadata:
161
+ result["metadata"] = metadata
162
+ return JSONResponse(result)
163
+ except Exception as exc:
164
+ return JSONResponse(
165
+ {"success": False, "error": str(exc)},
166
+ status_code=500,
167
+ )
168
+
169
+ @app.post("/triggers/{trigger_id}/cleanup")
170
+ async def cleanup_trigger(trigger_id: str, request: Request) -> JSONResponse:
171
+ trigger = planelet._triggers.get(trigger_id)
172
+ if not trigger:
173
+ return JSONResponse(
174
+ {"success": False, "error": f'Trigger "{trigger_id}" not found'},
175
+ status_code=404,
176
+ )
177
+ if not trigger._cleanup:
178
+ return JSONResponse(
179
+ {"success": False, "error": f'Trigger "{trigger_id}" has no cleanup handler'},
180
+ status_code=501,
181
+ )
182
+
183
+ body = await request.json()
184
+ ctx = CleanupContext(
185
+ parameters=body.get("parameters", {}),
186
+ metadata=body.get("metadata"),
187
+ )
188
+
189
+ try:
190
+ await trigger._cleanup(ctx)
191
+ return JSONResponse({"success": True})
192
+ except Exception as exc:
193
+ return JSONResponse(
194
+ {"success": False, "error": str(exc)},
195
+ status_code=500,
196
+ )
197
+
198
+ @app.post("/triggers/{trigger_id}/webhook")
199
+ async def handle_webhook(trigger_id: str, request: Request) -> JSONResponse:
200
+ trigger = planelet._triggers.get(trigger_id)
201
+ if not trigger:
202
+ return JSONResponse(
203
+ {"success": False, "error": f'Trigger "{trigger_id}" not found'},
204
+ status_code=404,
205
+ )
206
+ if not trigger._webhook:
207
+ return JSONResponse(
208
+ {"success": False, "error": f'Trigger "{trigger_id}" has no webhook handler'},
209
+ status_code=501,
210
+ )
211
+
212
+ body = await request.json()
213
+ req_data = body.get("request", {})
214
+ raw_body_b64 = req_data.get("rawBodyBase64", "")
215
+ raw_body = base64.b64decode(raw_body_b64) if raw_body_b64 else b""
216
+
217
+ ctx = WebhookContext(
218
+ parameters=body.get("parameters", {}),
219
+ metadata=body.get("metadata"),
220
+ request=WebhookRequest(
221
+ method=req_data.get("method", "POST"),
222
+ headers=req_data.get("headers", {}),
223
+ query=req_data.get("query"),
224
+ raw_body=raw_body,
225
+ ),
226
+ )
227
+
228
+ try:
229
+ result = await trigger._webhook(ctx)
230
+ except Exception as exc:
231
+ return JSONResponse(
232
+ {"success": False, "error": str(exc)},
233
+ status_code=500,
234
+ )
235
+
236
+ if isinstance(result, WebhookResult):
237
+ resp: dict[str, Any] = {
238
+ "success": True,
239
+ "emit": True,
240
+ "eventType": result.event_type,
241
+ "payload": result.payload,
242
+ }
243
+ http_resp = _serialize_http_response(result.response)
244
+ if http_resp:
245
+ resp["response"] = http_resp
246
+ return JSONResponse(resp)
247
+
248
+ if isinstance(result, WebhookSkip):
249
+ resp = {"success": True, "emit": False}
250
+ if result.reason:
251
+ resp["reason"] = result.reason
252
+ http_resp = _serialize_http_response(result.response)
253
+ if http_resp:
254
+ resp["response"] = http_resp
255
+ return JSONResponse(resp)
256
+
257
+ if isinstance(result, WebhookError):
258
+ err_resp: dict[str, Any] = {"success": False, "error": result.error}
259
+ if result.status is not None:
260
+ err_resp["status"] = result.status
261
+ return JSONResponse(err_resp, status_code=result.status or 500)
262
+
263
+ return JSONResponse(
264
+ {"success": False, "error": "Webhook handler returned invalid type"},
265
+ status_code=500,
266
+ )
267
+
268
+ return app
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Awaitable
4
+
5
+ from .types import (
6
+ CleanupContext,
7
+ Param,
8
+ SetupContext,
9
+ WebhookContext,
10
+ WebhookError,
11
+ WebhookResult,
12
+ WebhookSkip,
13
+ )
14
+
15
+ SetupHandler = Callable[[SetupContext], Awaitable[dict[str, Any] | None]]
16
+ CleanupHandler = Callable[[CleanupContext], Awaitable[None]]
17
+ WebhookHandler = Callable[
18
+ [WebhookContext], Awaitable[WebhookResult | WebhookSkip | WebhookError]
19
+ ]
20
+
21
+
22
+ class TriggerBuilder:
23
+ def __init__(
24
+ self,
25
+ id: str,
26
+ *,
27
+ label: str,
28
+ description: str | None = None,
29
+ icon: str | None = None,
30
+ icon_url: str | None = None,
31
+ parameters: dict[str, Param] | None = None,
32
+ ) -> None:
33
+ self.id = id
34
+ self.label = label
35
+ self.description = description
36
+ self.icon = icon
37
+ self.icon_url = icon_url
38
+ self.parameters = parameters or {}
39
+
40
+ self._setup: SetupHandler | None = None
41
+ self._cleanup: CleanupHandler | None = None
42
+ self._webhook: WebhookHandler | None = None
43
+
44
+ def on_setup(self, fn: SetupHandler) -> SetupHandler:
45
+ self._setup = fn
46
+ return fn
47
+
48
+ def on_cleanup(self, fn: CleanupHandler) -> CleanupHandler:
49
+ self._cleanup = fn
50
+ return fn
51
+
52
+ def on_webhook(self, fn: WebhookHandler) -> WebhookHandler:
53
+ self._webhook = fn
54
+ return fn
planelet_sdk/types.py ADDED
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Param:
9
+ label: str
10
+ type: str # "string" | "text" | "number" | "bool" | "select" | "object"
11
+ description: str | None = None
12
+ required: bool = False
13
+ default: Any = None
14
+ options: list[ParamOption] | None = None
15
+
16
+
17
+ @dataclass
18
+ class ParamOption:
19
+ label: str
20
+ value: str
21
+
22
+
23
+ # --- Contexts passed to handlers ---
24
+
25
+
26
+ @dataclass
27
+ class ActionContext:
28
+ parameters: dict[str, Any]
29
+ input: Any | None = None
30
+
31
+
32
+ @dataclass
33
+ class WebhookInfo:
34
+ url: str
35
+ secret: str | None = None
36
+
37
+
38
+ @dataclass
39
+ class SetupContext:
40
+ parameters: dict[str, Any]
41
+ webhook: WebhookInfo = field(default_factory=lambda: WebhookInfo(url=""))
42
+
43
+
44
+ @dataclass
45
+ class CleanupContext:
46
+ parameters: dict[str, Any]
47
+ metadata: dict[str, Any] | None = None
48
+
49
+
50
+ @dataclass
51
+ class WebhookRequest:
52
+ method: str
53
+ headers: dict[str, list[str]]
54
+ query: dict[str, list[str]] | None = None
55
+ raw_body: bytes = b""
56
+
57
+
58
+ @dataclass
59
+ class WebhookContext:
60
+ parameters: dict[str, Any]
61
+ metadata: dict[str, Any] | None = None
62
+ request: WebhookRequest = field(
63
+ default_factory=lambda: WebhookRequest(method="POST", headers={})
64
+ )
65
+
66
+
67
+ # --- Result types returned by handlers ---
68
+
69
+
70
+ @dataclass
71
+ class HttpResponse:
72
+ status: int = 200
73
+ headers: dict[str, str] | None = None
74
+ body: str | None = None
75
+
76
+
77
+ @dataclass
78
+ class WebhookResult:
79
+ event_type: str
80
+ payload: Any = None
81
+ response: HttpResponse | None = None
82
+
83
+
84
+ @dataclass
85
+ class WebhookSkip:
86
+ reason: str | None = None
87
+ response: HttpResponse | None = None
88
+
89
+
90
+ @dataclass
91
+ class WebhookError:
92
+ error: str
93
+ status: int | None = None
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.3
2
+ Name: planelet-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for building SuperPlane planelets
5
+ Author: ThatXliner
6
+ Author-email: ThatXliner <thatxliner@gmail.com>
7
+ Requires-Dist: fastapi>=0.100.0
8
+ Requires-Dist: uvicorn[standard]>=0.20.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # SuperPlane Planelet SDK for Python
13
+
14
+ Build custom SuperPlane integrations in Python. Define actions and webhook triggers with decorators, and the SDK serves the full Planelet protocol over HTTP.
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Create a new project
19
+
20
+ ```bash
21
+ mkdir my-planelet && cd my-planelet
22
+ python -m venv .venv && source .venv/bin/activate
23
+ pip install superplane-planelet-sdk
24
+ ```
25
+
26
+ ### 2. Write your Planelet
27
+
28
+ ```python
29
+ # main.py
30
+ from superplane import create_planelet, Param, ActionContext
31
+
32
+ planelet = create_planelet(
33
+ id="my-planelet",
34
+ label="My Planelet",
35
+ description="Does useful things",
36
+ )
37
+
38
+ @planelet.action(
39
+ "hello",
40
+ label="Say Hello",
41
+ description="Generates a greeting",
42
+ parameters={
43
+ "name": Param(label="Name", type="string", required=True),
44
+ },
45
+ )
46
+ async def hello(ctx: ActionContext):
47
+ return {"message": f"Hello, {ctx.parameters['name']}!"}
48
+
49
+ planelet.listen(3001)
50
+ ```
51
+
52
+ ### 3. Run it
53
+
54
+ ```bash
55
+ python main.py
56
+ # Uvicorn running on http://0.0.0.0:3001
57
+ ```
58
+
59
+ ### 4. Connect to SuperPlane
60
+
61
+ 1. In SuperPlane, add a new **Planelets** integration
62
+ 2. Set **Server URL** to your Planelet server's address
63
+ 3. Optionally set an **Auth Token**
64
+ 4. Save — SuperPlane fetches your manifest and the integration goes ready
65
+
66
+ ## Documentation
67
+
68
+ See [PLANELET-DOCS.md](PLANELET-DOCS.md) for the full SDK reference, trigger/webhook guide, and protocol details.
69
+
70
+ ## Example
71
+
72
+ See [`examples/quotes.py`](examples/quotes.py) for a complete example with two actions. Run it:
73
+
74
+ ```bash
75
+ cd examples
76
+ python quotes.py
77
+ ```
78
+
79
+ Then connect SuperPlane to `http://localhost:3001`.
@@ -0,0 +1,9 @@
1
+ planelet_sdk/__init__.py,sha256=7wDPuHMmI26-lB_-FYKsnB59JPEtZ2kpEsiynmNeFRk,933
2
+ planelet_sdk/planelet.py,sha256=0CdW6P-DAFLqE4PSiKbcSPG25wrM8rHK-hPox8SbT9c,2631
3
+ planelet_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ planelet_sdk/server.py,sha256=QzDr2vqHYbH-VEaeBG-dW2jG8hFhAqBsrXclFxZfuM8,9019
5
+ planelet_sdk/trigger.py,sha256=BXIrMGh6bQSXsMid0hO21ANgAmIpx1eC_wHfcDgodZQ,1412
6
+ planelet_sdk/types.py,sha256=VWVsiakShC0uBNn09GGthHbOk_nUbVblNI1avoWbq-U,1725
7
+ planelet_sdk-0.1.0.dist-info/WHEEL,sha256=f5fWSvWsg5Knq5GWa6t1nJIug0Tqo69GqAWD_9LbBKw,81
8
+ planelet_sdk-0.1.0.dist-info/METADATA,sha256=OE_QLOAtU8pwhRKe3RbgZ7zPz_SKx8JD8T5Vl6zPJ04,1903
9
+ planelet_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.16
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any