@superblocksteam/sdk-api 2.0.105-next.0 → 2.0.105-next.2
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 +202 -86
- package/dist/api/definition.d.ts +11 -6
- package/dist/api/definition.d.ts.map +1 -1
- package/dist/api/definition.js +19 -12
- package/dist/api/definition.js.map +1 -1
- package/dist/api/definition.test.js +39 -15
- package/dist/api/definition.test.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/base/index.d.ts +2 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/index.js +1 -0
- package/dist/integrations/base/index.js.map +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts +48 -0
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -0
- package/dist/integrations/base/rest-api-client-base.js +98 -0
- package/dist/integrations/base/rest-api-client-base.js.map +1 -0
- package/dist/integrations/base/rest-api-integration-client.d.ts +10 -20
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +10 -65
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/box/types.d.ts +1 -1
- package/dist/integrations/declarations.d.ts +5 -63
- package/dist/integrations/declarations.d.ts.map +1 -1
- package/dist/integrations/declarations.js +5 -59
- package/dist/integrations/declarations.js.map +1 -1
- package/dist/integrations/documentation.test.js +0 -2
- package/dist/integrations/documentation.test.js.map +1 -1
- package/dist/integrations/googledrive/types.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -9
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -6
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/registry.d.ts +1 -10
- package/dist/integrations/registry.d.ts.map +1 -1
- package/dist/integrations/registry.js +0 -25
- package/dist/integrations/registry.js.map +1 -1
- package/dist/integrations/slack/client.d.ts +13 -9
- package/dist/integrations/slack/client.d.ts.map +1 -1
- package/dist/integrations/slack/client.js +60 -8
- package/dist/integrations/slack/client.js.map +1 -1
- package/dist/integrations/slack/client.test.d.ts +11 -0
- package/dist/integrations/slack/client.test.d.ts.map +1 -0
- package/dist/integrations/slack/client.test.js +368 -0
- package/dist/integrations/slack/client.test.js.map +1 -0
- package/dist/integrations/slack/index.d.ts +2 -1
- package/dist/integrations/slack/index.d.ts.map +1 -1
- package/dist/integrations/slack/index.js +1 -0
- package/dist/integrations/slack/index.js.map +1 -1
- package/dist/integrations/slack/types.d.ts +127 -28
- package/dist/integrations/slack/types.d.ts.map +1 -1
- package/dist/integrations/slack/types.js +27 -1
- package/dist/integrations/slack/types.js.map +1 -1
- package/dist/integrations/snowflake/client.d.ts +2 -2
- package/dist/integrations/snowflake/client.js +2 -2
- package/dist/runtime/context.d.ts +1 -1
- package/dist/runtime/executor.d.ts +2 -2
- package/dist/types.d.ts +14 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/definition.test.ts +40 -15
- package/src/api/definition.ts +19 -12
- package/src/errors.ts +1 -1
- package/src/index.ts +13 -30
- package/src/integrations/asana/README.md +12 -12
- package/src/integrations/base/index.ts +2 -1
- package/src/integrations/base/rest-api-client-base.ts +134 -0
- package/src/integrations/base/rest-api-integration-client.ts +12 -89
- package/src/integrations/bitbucket/README.md +19 -19
- package/src/integrations/box/README.md +24 -24
- package/src/integrations/box/types.ts +1 -1
- package/src/integrations/circleci/README.md +18 -18
- package/src/integrations/declarations.ts +5 -91
- package/src/integrations/documentation.test.ts +0 -2
- package/src/integrations/googledrive/README.md +25 -22
- package/src/integrations/googledrive/types.ts +1 -1
- package/src/integrations/graphql/README.md +2 -2
- package/src/integrations/index.ts +0 -45
- package/src/integrations/registry.ts +1 -34
- package/src/integrations/salesforce/README.md +11 -9
- package/src/integrations/slack/README.md +62 -19
- package/src/integrations/slack/client.test.ts +553 -0
- package/src/integrations/slack/client.ts +92 -12
- package/src/integrations/slack/index.ts +6 -1
- package/src/integrations/slack/types.ts +142 -29
- package/src/integrations/snowflake/client.ts +2 -2
- package/src/integrations/zoom/README.md +15 -15
- package/src/runtime/context.ts +1 -1
- package/src/runtime/executor.ts +2 -2
- package/src/types.ts +14 -5
- package/dist/integrations/couchbase/client.d.ts +0 -36
- package/dist/integrations/couchbase/client.d.ts.map +0 -1
- package/dist/integrations/couchbase/client.js +0 -148
- package/dist/integrations/couchbase/client.js.map +0 -1
- package/dist/integrations/couchbase/index.d.ts +0 -8
- package/dist/integrations/couchbase/index.d.ts.map +0 -1
- package/dist/integrations/couchbase/index.js +0 -7
- package/dist/integrations/couchbase/index.js.map +0 -1
- package/dist/integrations/couchbase/types.d.ts +0 -100
- package/dist/integrations/couchbase/types.d.ts.map +0 -1
- package/dist/integrations/couchbase/types.js +0 -5
- package/dist/integrations/couchbase/types.js.map +0 -1
- package/dist/integrations/kafka/client.d.ts +0 -25
- package/dist/integrations/kafka/client.d.ts.map +0 -1
- package/dist/integrations/kafka/client.js +0 -124
- package/dist/integrations/kafka/client.js.map +0 -1
- package/dist/integrations/kafka/index.d.ts +0 -8
- package/dist/integrations/kafka/index.d.ts.map +0 -1
- package/dist/integrations/kafka/index.js +0 -7
- package/dist/integrations/kafka/index.js.map +0 -1
- package/dist/integrations/kafka/types.d.ts +0 -113
- package/dist/integrations/kafka/types.d.ts.map +0 -1
- package/dist/integrations/kafka/types.js +0 -5
- package/dist/integrations/kafka/types.js.map +0 -1
- package/dist/integrations/kinesis/client.d.ts +0 -31
- package/dist/integrations/kinesis/client.d.ts.map +0 -1
- package/dist/integrations/kinesis/client.js +0 -101
- package/dist/integrations/kinesis/client.js.map +0 -1
- package/dist/integrations/kinesis/index.d.ts +0 -8
- package/dist/integrations/kinesis/index.d.ts.map +0 -1
- package/dist/integrations/kinesis/index.js +0 -7
- package/dist/integrations/kinesis/index.js.map +0 -1
- package/dist/integrations/kinesis/types.d.ts +0 -97
- package/dist/integrations/kinesis/types.d.ts.map +0 -1
- package/dist/integrations/kinesis/types.js +0 -7
- package/dist/integrations/kinesis/types.js.map +0 -1
- package/dist/integrations/python/client.d.ts +0 -42
- package/dist/integrations/python/client.d.ts.map +0 -1
- package/dist/integrations/python/client.js +0 -89
- package/dist/integrations/python/client.js.map +0 -1
- package/dist/integrations/python/client.test.d.ts +0 -5
- package/dist/integrations/python/client.test.d.ts.map +0 -1
- package/dist/integrations/python/client.test.js +0 -214
- package/dist/integrations/python/client.test.js.map +0 -1
- package/dist/integrations/python/index.d.ts +0 -6
- package/dist/integrations/python/index.d.ts.map +0 -1
- package/dist/integrations/python/index.js +0 -5
- package/dist/integrations/python/index.js.map +0 -1
- package/dist/integrations/python/types.d.ts +0 -85
- package/dist/integrations/python/types.d.ts.map +0 -1
- package/dist/integrations/python/types.js +0 -5
- package/dist/integrations/python/types.js.map +0 -1
- package/src/integrations/couchbase/README.md +0 -138
- package/src/integrations/couchbase/client.ts +0 -225
- package/src/integrations/couchbase/index.ts +0 -8
- package/src/integrations/couchbase/types.ts +0 -126
- package/src/integrations/kafka/README.md +0 -144
- package/src/integrations/kafka/client.ts +0 -216
- package/src/integrations/kafka/index.ts +0 -14
- package/src/integrations/kafka/types.ts +0 -128
- package/src/integrations/kinesis/README.md +0 -153
- package/src/integrations/kinesis/client.ts +0 -146
- package/src/integrations/kinesis/index.ts +0 -14
- package/src/integrations/kinesis/types.ts +0 -114
- package/src/integrations/python/README.md +0 -566
- package/src/integrations/python/client.test.ts +0 -341
- package/src/integrations/python/client.ts +0 -136
- package/src/integrations/python/index.ts +0 -6
- package/src/integrations/python/types.ts +0 -92
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Python client binding injection.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from "vitest";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
|
|
8
|
-
import type { QueryExecutor } from "../registry.js";
|
|
9
|
-
import type { IntegrationConfig } from "../types.js";
|
|
10
|
-
import { PythonClientImpl } from "./client.js";
|
|
11
|
-
|
|
12
|
-
/** Helper to compute expected base64-encoded json.loads preamble. */
|
|
13
|
-
function expectedBindingsLine(bindings: Record<string, unknown>): string {
|
|
14
|
-
const b64 = Buffer.from(JSON.stringify(bindings)).toString("base64");
|
|
15
|
-
return `_bindings = json.loads(base64.b64decode('${b64}').decode())`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("PythonClientImpl", () => {
|
|
19
|
-
const createMockConfig = (): IntegrationConfig => ({
|
|
20
|
-
id: "test-python-id",
|
|
21
|
-
name: "Test Python",
|
|
22
|
-
pluginId: "python",
|
|
23
|
-
configuration: {},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const createMockExecutor = (mockResult: unknown): QueryExecutor => {
|
|
27
|
-
return vi.fn().mockResolvedValue(mockResult);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
describe("binding injection", () => {
|
|
31
|
-
it("generates code without bindings when none provided", async () => {
|
|
32
|
-
const mockExecutor = vi.fn().mockResolvedValue({ result: 42 });
|
|
33
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
34
|
-
|
|
35
|
-
await client.run(
|
|
36
|
-
'return {"result": 42}',
|
|
37
|
-
z.object({ result: z.number() }),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
expect(mockExecutor).toHaveBeenCalledWith(
|
|
41
|
-
{ body: 'return {"result": 42}' },
|
|
42
|
-
undefined,
|
|
43
|
-
undefined,
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("injects string bindings as Python variables", async () => {
|
|
48
|
-
const mockExecutor = vi
|
|
49
|
-
.fn()
|
|
50
|
-
.mockResolvedValue({ message: "Hello World" });
|
|
51
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
52
|
-
|
|
53
|
-
const bindings = { greeting: "Hello World" };
|
|
54
|
-
await client.run(
|
|
55
|
-
'return {"message": greeting}',
|
|
56
|
-
z.object({ message: z.string() }),
|
|
57
|
-
bindings,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
61
|
-
expect(request.body).toContain("import json, base64");
|
|
62
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
63
|
-
expect(request.body).toContain("greeting = _bindings['greeting']");
|
|
64
|
-
expect(request.body).toContain('return {"message": greeting}');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("injects number bindings as Python variables", async () => {
|
|
68
|
-
const mockExecutor = vi.fn().mockResolvedValue({ result: 14 });
|
|
69
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
70
|
-
|
|
71
|
-
const bindings = { number: 10 };
|
|
72
|
-
await client.run(
|
|
73
|
-
'return {"result": number + 4}',
|
|
74
|
-
z.object({ result: z.number() }),
|
|
75
|
-
bindings,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
79
|
-
expect(request.body).toContain("import json, base64");
|
|
80
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
81
|
-
expect(request.body).toContain("number = _bindings['number']");
|
|
82
|
-
expect(request.body).toContain('return {"result": number + 4}');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("injects boolean bindings as Python variables", async () => {
|
|
86
|
-
const mockExecutor = vi.fn().mockResolvedValue({ active: true });
|
|
87
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
88
|
-
|
|
89
|
-
const bindings = { isActive: true };
|
|
90
|
-
await client.run(
|
|
91
|
-
'return {"active": isActive}',
|
|
92
|
-
z.object({ active: z.boolean() }),
|
|
93
|
-
bindings,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
97
|
-
expect(request.body).toContain("import json, base64");
|
|
98
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
99
|
-
expect(request.body).toContain("isActive = _bindings['isActive']");
|
|
100
|
-
expect(request.body).toContain('return {"active": isActive}');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("injects null bindings as Python variables", async () => {
|
|
104
|
-
const mockExecutor = vi.fn().mockResolvedValue({ value: null });
|
|
105
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
106
|
-
|
|
107
|
-
const bindings = { nullValue: null };
|
|
108
|
-
await client.run(
|
|
109
|
-
'return {"value": nullValue}',
|
|
110
|
-
z.object({ value: z.null() }),
|
|
111
|
-
bindings,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
115
|
-
expect(request.body).toContain("import json, base64");
|
|
116
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
117
|
-
expect(request.body).toContain("nullValue = _bindings['nullValue']");
|
|
118
|
-
expect(request.body).toContain('return {"value": nullValue}');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("injects array bindings as Python variables", async () => {
|
|
122
|
-
const mockExecutor = vi.fn().mockResolvedValue({ count: 3 });
|
|
123
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
124
|
-
|
|
125
|
-
const bindings = { items: [1, 2, 3] };
|
|
126
|
-
await client.run(
|
|
127
|
-
'return {"count": len(items)}',
|
|
128
|
-
z.object({ count: z.number() }),
|
|
129
|
-
bindings,
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
133
|
-
expect(request.body).toContain("import json, base64");
|
|
134
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
135
|
-
expect(request.body).toContain("items = _bindings['items']");
|
|
136
|
-
expect(request.body).toContain('return {"count": len(items)}');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("injects object bindings as Python variables", async () => {
|
|
140
|
-
const mockExecutor = vi.fn().mockResolvedValue({ name: "John" });
|
|
141
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
142
|
-
|
|
143
|
-
const bindings = { user: { name: "John", age: 30 } };
|
|
144
|
-
await client.run(
|
|
145
|
-
'return {"name": user["name"]}',
|
|
146
|
-
z.object({ name: z.string() }),
|
|
147
|
-
bindings,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
151
|
-
expect(request.body).toContain("import json, base64");
|
|
152
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
153
|
-
expect(request.body).toContain("user = _bindings['user']");
|
|
154
|
-
expect(request.body).toContain('return {"name": user["name"]}');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("injects multiple bindings as Python variables", async () => {
|
|
158
|
-
const mockExecutor = vi
|
|
159
|
-
.fn()
|
|
160
|
-
.mockResolvedValue({ message: "Alice is 25 years old" });
|
|
161
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
162
|
-
|
|
163
|
-
const bindings = { userName: "Alice", userAge: 25 };
|
|
164
|
-
await client.run(
|
|
165
|
-
'return {"message": f"{userName} is {userAge} years old"}',
|
|
166
|
-
z.object({ message: z.string() }),
|
|
167
|
-
bindings,
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
171
|
-
expect(request.body).toContain("import json, base64");
|
|
172
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
173
|
-
expect(request.body).toContain("userName = _bindings['userName']");
|
|
174
|
-
expect(request.body).toContain("userAge = _bindings['userAge']");
|
|
175
|
-
expect(request.body).toContain(
|
|
176
|
-
'return {"message": f"{userName} is {userAge} years old"}',
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("handles strings with special characters", async () => {
|
|
181
|
-
const mockExecutor = vi
|
|
182
|
-
.fn()
|
|
183
|
-
.mockResolvedValue({ text: 'It\'s a "test"' });
|
|
184
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
185
|
-
|
|
186
|
-
const bindings = { text: 'It\'s a "test"' };
|
|
187
|
-
await client.run(
|
|
188
|
-
'return {"text": text}',
|
|
189
|
-
z.object({ text: z.string() }),
|
|
190
|
-
bindings,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
194
|
-
expect(request.body).toContain("import json, base64");
|
|
195
|
-
// Base64 encoding avoids all string escaping issues
|
|
196
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
197
|
-
expect(request.body).toContain("text = _bindings['text']");
|
|
198
|
-
expect(request.body).toContain('return {"text": text}');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("handles strings with newlines and tabs", async () => {
|
|
202
|
-
const mockExecutor = vi
|
|
203
|
-
.fn()
|
|
204
|
-
.mockResolvedValue({ text: "line1\nline2\ttab" });
|
|
205
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
206
|
-
|
|
207
|
-
const bindings = { text: "line1\nline2\ttab" };
|
|
208
|
-
await client.run(
|
|
209
|
-
'return {"text": text}',
|
|
210
|
-
z.object({ text: z.string() }),
|
|
211
|
-
bindings,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
215
|
-
// Base64 encoding handles newlines/tabs safely
|
|
216
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
217
|
-
expect(request.body).toContain("text = _bindings['text']");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("handles strings with backslashes", async () => {
|
|
221
|
-
const mockExecutor = vi
|
|
222
|
-
.fn()
|
|
223
|
-
.mockResolvedValue({ path: "C:\\Users\\test" });
|
|
224
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
225
|
-
|
|
226
|
-
const bindings = { path: "C:\\Users\\test" };
|
|
227
|
-
await client.run(
|
|
228
|
-
'return {"path": path}',
|
|
229
|
-
z.object({ path: z.string() }),
|
|
230
|
-
bindings,
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
234
|
-
// Base64 encoding handles backslashes safely
|
|
235
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
236
|
-
expect(request.body).toContain("path = _bindings['path']");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("preserves code structure with blank line between preamble and user code", async () => {
|
|
240
|
-
const mockExecutor = vi.fn().mockResolvedValue({ result: 42 });
|
|
241
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
242
|
-
|
|
243
|
-
const bindings = { number: 21 };
|
|
244
|
-
await client.run(
|
|
245
|
-
'result = number * 2\nreturn {"result": result}',
|
|
246
|
-
z.object({ result: z.number() }),
|
|
247
|
-
bindings,
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
251
|
-
const lines = (request.body as string).split("\n");
|
|
252
|
-
|
|
253
|
-
// Should have: import, _bindings assignment, variable extraction, blank line, then user code
|
|
254
|
-
expect(lines[0]).toBe("import json, base64");
|
|
255
|
-
expect(lines[1]).toBe(expectedBindingsLine(bindings));
|
|
256
|
-
expect(lines[2]).toBe("number = _bindings['number']");
|
|
257
|
-
expect(lines[3]).toBe("");
|
|
258
|
-
expect(lines[4]).toBe("result = number * 2");
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe("validation", () => {
|
|
263
|
-
it("validates result against schema", async () => {
|
|
264
|
-
const mockExecutor = createMockExecutor({ result: 42 });
|
|
265
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
266
|
-
|
|
267
|
-
const result = await client.run(
|
|
268
|
-
'return {"result": 42}',
|
|
269
|
-
z.object({ result: z.number() }),
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
expect(result).toEqual({ result: 42 });
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("throws CodeExecutionError when validation fails", async () => {
|
|
276
|
-
const mockExecutor = createMockExecutor({ result: "not a number" });
|
|
277
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
278
|
-
|
|
279
|
-
await expect(
|
|
280
|
-
client.run(
|
|
281
|
-
'return {"result": "not a number"}',
|
|
282
|
-
z.object({ result: z.number() }),
|
|
283
|
-
),
|
|
284
|
-
).rejects.toThrow("Python execution result validation failed");
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("throws CodeExecutionError when execution fails", async () => {
|
|
288
|
-
const mockExecutor = vi
|
|
289
|
-
.fn()
|
|
290
|
-
.mockRejectedValue(new Error("Execution error"));
|
|
291
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
292
|
-
|
|
293
|
-
await expect(
|
|
294
|
-
client.run('return {"result": 42}', z.object({ result: z.number() })),
|
|
295
|
-
).rejects.toThrow("Python execution failed");
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe("complex bindings", () => {
|
|
300
|
-
it("handles nested objects", async () => {
|
|
301
|
-
const mockExecutor = vi.fn().mockResolvedValue({ name: "John Doe" });
|
|
302
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
303
|
-
|
|
304
|
-
const bindings = {
|
|
305
|
-
person: { first: "John", last: "Doe", address: { city: "NYC" } },
|
|
306
|
-
};
|
|
307
|
-
await client.run(
|
|
308
|
-
"return {\"name\": f\"{person['first']} {person['last']}\"}",
|
|
309
|
-
z.object({ name: z.string() }),
|
|
310
|
-
bindings,
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
314
|
-
expect(request.body).toContain("import json, base64");
|
|
315
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
316
|
-
expect(request.body).toContain("person = _bindings['person']");
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("handles arrays of objects", async () => {
|
|
320
|
-
const mockExecutor = vi.fn().mockResolvedValue({ count: 2 });
|
|
321
|
-
const client = new PythonClientImpl(createMockConfig(), mockExecutor);
|
|
322
|
-
|
|
323
|
-
const bindings = {
|
|
324
|
-
users: [
|
|
325
|
-
{ id: 1, name: "Alice" },
|
|
326
|
-
{ id: 2, name: "Bob" },
|
|
327
|
-
],
|
|
328
|
-
};
|
|
329
|
-
await client.run(
|
|
330
|
-
'return {"count": len(users)}',
|
|
331
|
-
z.object({ count: z.number() }),
|
|
332
|
-
bindings,
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
const request = mockExecutor.mock.calls[0][0];
|
|
336
|
-
expect(request.body).toContain("import json, base64");
|
|
337
|
-
expect(request.body).toContain(expectedBindingsLine(bindings));
|
|
338
|
-
expect(request.body).toContain("users = _bindings['users']");
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python client implementation.
|
|
3
|
-
*
|
|
4
|
-
* Executes Python code via the orchestrator with type-safe bindings.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { PartialMessage } from "@bufbuild/protobuf";
|
|
8
|
-
import type { z } from "zod";
|
|
9
|
-
|
|
10
|
-
import type { Plugin as PythonPlugin } from "@superblocksteam/types/dist/src/plugins/python/v1/plugin_pb";
|
|
11
|
-
|
|
12
|
-
import { CodeExecutionError } from "../../errors.js";
|
|
13
|
-
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
14
|
-
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
15
|
-
import type { PythonClient } from "./types.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Python plugin request type derived from proto definition.
|
|
19
|
-
* Using PartialMessage allows optional fields.
|
|
20
|
-
*/
|
|
21
|
-
export type PythonRequest = PartialMessage<PythonPlugin>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Internal implementation of PythonClient.
|
|
25
|
-
*
|
|
26
|
-
* This implementation communicates with the orchestrator to execute Python code.
|
|
27
|
-
* The orchestrator handles {{}} binding resolution and script execution.
|
|
28
|
-
*/
|
|
29
|
-
export class PythonClientImpl implements PythonClient, IntegrationClientImpl {
|
|
30
|
-
readonly name: string;
|
|
31
|
-
readonly pluginId: string;
|
|
32
|
-
readonly config: IntegrationConfig;
|
|
33
|
-
|
|
34
|
-
private readonly executeQuery: QueryExecutor;
|
|
35
|
-
|
|
36
|
-
constructor(config: IntegrationConfig, executeQuery: QueryExecutor) {
|
|
37
|
-
this.name = config.name;
|
|
38
|
-
this.pluginId = config.pluginId;
|
|
39
|
-
this.config = config;
|
|
40
|
-
this.executeQuery = executeQuery;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build Python plugin request with injected bindings.
|
|
45
|
-
*
|
|
46
|
-
* Bindings are injected as Python variables in a preamble before the user code.
|
|
47
|
-
* This allows the user code to reference binding keys directly as Python variables.
|
|
48
|
-
*
|
|
49
|
-
* @param code - User's Python code string
|
|
50
|
-
* @param bindings - Bindings to inject as Python variables
|
|
51
|
-
* @returns Request object matching the proto schema with bindings injected
|
|
52
|
-
*/
|
|
53
|
-
private buildRequest(
|
|
54
|
-
code: string,
|
|
55
|
-
bindings?: Record<string, unknown>,
|
|
56
|
-
): PythonRequest {
|
|
57
|
-
// If no bindings, just return the code as-is
|
|
58
|
-
if (!bindings || Object.keys(bindings).length === 0) {
|
|
59
|
-
return {
|
|
60
|
-
body: code,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Inject bindings as Python variables using base64 to avoid string escaping issues.
|
|
65
|
-
// Format: import json,base64\n_bindings = json.loads(base64.b64decode('...').decode())\nvar1 = _bindings['var1']\n...\n\n<user code>
|
|
66
|
-
const bindingLines: string[] = ["import json, base64"];
|
|
67
|
-
|
|
68
|
-
// Base64-encode the JSON to avoid issues with special characters (\n, \t, \\, quotes)
|
|
69
|
-
// that JSON.stringify produces and Python's string parser would interpret
|
|
70
|
-
const bindingsB64 = Buffer.from(JSON.stringify(bindings)).toString(
|
|
71
|
-
"base64",
|
|
72
|
-
);
|
|
73
|
-
bindingLines.push(
|
|
74
|
-
`_bindings = json.loads(base64.b64decode('${bindingsB64}').decode())`,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Extract each binding as a local variable
|
|
78
|
-
for (const key of Object.keys(bindings)) {
|
|
79
|
-
bindingLines.push(`${key} = _bindings['${key}']`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Combine binding preamble with user code
|
|
83
|
-
const fullCode = `${bindingLines.join("\n")}\n\n${code}`;
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
body: fullCode,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async run<T>(
|
|
91
|
-
code: string,
|
|
92
|
-
schema: z.ZodSchema<T>,
|
|
93
|
-
bindings?: Record<string, unknown>,
|
|
94
|
-
metadata?: TraceMetadata,
|
|
95
|
-
): Promise<T> {
|
|
96
|
-
// Build request with bindings injected as Python variables
|
|
97
|
-
const request = this.buildRequest(code, bindings);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
// Execute the Python code with injected bindings
|
|
101
|
-
// No need to pass bindings as input since they're already in the code
|
|
102
|
-
const result = await this.executeQuery(
|
|
103
|
-
request as unknown as Record<string, unknown>,
|
|
104
|
-
undefined,
|
|
105
|
-
metadata,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Validate result against schema
|
|
109
|
-
const parseResult = schema.safeParse(result);
|
|
110
|
-
|
|
111
|
-
if (!parseResult.success) {
|
|
112
|
-
throw new CodeExecutionError(
|
|
113
|
-
`Python execution result validation failed: ${parseResult.error.message}`,
|
|
114
|
-
{
|
|
115
|
-
code,
|
|
116
|
-
bindings,
|
|
117
|
-
result,
|
|
118
|
-
zodError: parseResult.error,
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return parseResult.data;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
if (error instanceof CodeExecutionError) {
|
|
126
|
-
throw error;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
throw new CodeExecutionError("Python execution failed", {
|
|
130
|
-
code,
|
|
131
|
-
bindings,
|
|
132
|
-
originalError: error,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python code execution client types.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { z } from "zod";
|
|
6
|
-
|
|
7
|
-
import type { BaseIntegrationClient } from "../../types.js";
|
|
8
|
-
import type { TraceMetadata } from "../registry.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Python code execution client.
|
|
12
|
-
*
|
|
13
|
-
* Executes Python scripts with type-safe inputs and outputs.
|
|
14
|
-
* Bindings are passed as a map and referenced in code using {{key}} syntax.
|
|
15
|
-
* The orchestrator resolves bindings before execution.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* // Declare in api(): integrations: { runtime: python(INTEGRATION_ID) }
|
|
20
|
-
* // In run(), access via: ctx.integrations.runtime
|
|
21
|
-
* const result = await ctx.integrations.runtime.run(
|
|
22
|
-
* `
|
|
23
|
-
* import time
|
|
24
|
-
*
|
|
25
|
-
* user_id = {{userId}}
|
|
26
|
-
* current_time = int(time.time())
|
|
27
|
-
*
|
|
28
|
-
* return {
|
|
29
|
-
* "greeting": f"Hello user {user_id}!",
|
|
30
|
-
* "timestamp": current_time
|
|
31
|
-
* }
|
|
32
|
-
* `,
|
|
33
|
-
* z.object({
|
|
34
|
-
* greeting: z.string(),
|
|
35
|
-
* timestamp: z.number(),
|
|
36
|
-
* }),
|
|
37
|
-
* {
|
|
38
|
-
* userId,
|
|
39
|
-
* }
|
|
40
|
-
* );
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export interface PythonClient extends BaseIntegrationClient {
|
|
44
|
-
/**
|
|
45
|
-
* Execute Python code with validated output.
|
|
46
|
-
*
|
|
47
|
-
* The Python code must return a value using a `return` statement.
|
|
48
|
-
* Bindings are referenced using {{key}} syntax and resolved by the
|
|
49
|
-
* orchestrator before execution.
|
|
50
|
-
*
|
|
51
|
-
* **Important:** The parameter order is `(code, schema, bindings?, metadata?)`.
|
|
52
|
-
* Because `bindings` is `Record<string, unknown>`, it is structurally
|
|
53
|
-
* compatible with `TraceMetadata`. Take care not to pass a metadata object
|
|
54
|
-
* as the third argument -- if you only need metadata and no bindings, pass
|
|
55
|
-
* `undefined` for `bindings` explicitly:
|
|
56
|
-
*
|
|
57
|
-
* ```typescript
|
|
58
|
-
* // Correct: skip bindings, pass metadata
|
|
59
|
-
* await python.run(code, schema, undefined, { label: "my label" });
|
|
60
|
-
*
|
|
61
|
-
* // Wrong: metadata is accidentally treated as bindings
|
|
62
|
-
* await python.run(code, schema, { label: "my label" });
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @param code - Python script to execute (use {{key}} for bindings)
|
|
66
|
-
* @param schema - Zod schema for validating the return value
|
|
67
|
-
* @param bindings - Optional key/value pairs accessible via {{key}} in code.
|
|
68
|
-
* Do not pass `TraceMetadata` here; use the `metadata` parameter instead.
|
|
69
|
-
* @param metadata - Optional trace metadata for diagnostics labeling
|
|
70
|
-
* @returns Validated result matching the schema
|
|
71
|
-
*
|
|
72
|
-
* @throws {CodeExecutionError} If execution fails or validation fails
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```typescript
|
|
76
|
-
* const result = await python.run(
|
|
77
|
-
* `
|
|
78
|
-
* user_id = {{userId}}
|
|
79
|
-
* return {"message": f"Hello {user_id}"}
|
|
80
|
-
* `,
|
|
81
|
-
* z.object({ message: z.string() }),
|
|
82
|
-
* { userId: "123" }
|
|
83
|
-
* );
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
run<T>(
|
|
87
|
-
code: string,
|
|
88
|
-
schema: z.ZodSchema<T>,
|
|
89
|
-
bindings?: Record<string, unknown>,
|
|
90
|
-
metadata?: TraceMetadata,
|
|
91
|
-
): Promise<T>;
|
|
92
|
-
}
|