@primate/python 0.2.0 → 0.4.0
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/lib/private/Default.js +50 -79
- package/lib/private/pyodide.d.ts +2 -1
- package/lib/private/pyodide.js +4 -1
- package/lib/private/to-request.d.ts +1 -1
- package/lib/private/to-request.js +24 -28
- package/lib/private/wrapper.d.ts +2 -0
- package/lib/private/wrapper.js +92 -0
- package/lib/public/wrapper.d.ts +2 -0
- package/lib/public/wrapper.js +2 -0
- package/package.json +7 -6
package/lib/private/Default.js
CHANGED
|
@@ -1,85 +1,46 @@
|
|
|
1
1
|
import Runtime from "#Runtime";
|
|
2
|
-
import
|
|
2
|
+
import TAG from "@primate/core/backend/TAG";
|
|
3
|
+
import fail from "@primate/core/fail";
|
|
4
|
+
import log from "@primate/core/log";
|
|
3
5
|
import assert from "@rcompat/assert";
|
|
4
6
|
import FileRef from "@rcompat/fs/FileRef";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const python = await pyodide();
|
|
42
|
-
const messageCallback = () => {};
|
|
43
|
-
|
|
44
|
-
await python.loadPackage("micropip", { messageCallback });
|
|
45
|
-
const micropip = python.pyimport("micropip");
|
|
46
|
-
await micropip.install("primate-run", { messageCallback });
|
|
47
|
-
${packages.map(p => `await micropip.install("${p}", { messageCallback });`).join("\n")}
|
|
48
|
-
|
|
49
|
-
await python.runPython(\`${user_code}\`);
|
|
50
|
-
|
|
51
|
-
// Get the registry of registered route function names
|
|
52
|
-
const registry = python.runPython("Route.registry()").toJs();
|
|
53
|
-
|
|
54
|
-
// Create route handler functions
|
|
55
|
-
await python.runPython(\`
|
|
56
|
-
\${Object.keys(registry).map(route => \`
|
|
57
|
-
def run_\${route.toUpperCase()}(js_request, helpers_obj, session_obj):
|
|
58
|
-
Route.set_session(session_obj, helpers_obj)
|
|
59
|
-
request = Route.Request(js_request, helpers_obj)
|
|
60
|
-
return Route.call_route("\${route.toUpperCase()}", request)
|
|
61
|
-
\`).join("\\n")}
|
|
62
|
-
\`);
|
|
63
|
-
|
|
64
|
-
// Create route handlers for each registered route
|
|
65
|
-
for (const [verb, func_name] of Object.entries(registry)) {
|
|
66
|
-
const route_fn = python.globals.get(\`run_\${verb.toUpperCase()}\`);
|
|
67
|
-
|
|
68
|
-
route[verb.toLowerCase()](async request => {
|
|
69
|
-
try {
|
|
70
|
-
const converted_request = await to_request(request);
|
|
71
|
-
const result = await route_fn(converted_request, helpers, wrapped_session);
|
|
72
|
-
return to_response(result);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
console.error(\`python error (\${verb.toLowerCase()})\`, e);
|
|
75
|
-
return { status: 500, body: "Python execution error: " + e.message };
|
|
76
|
-
}
|
|
77
|
-
});
|
|
7
|
+
import execute from "@rcompat/stdio/execute";
|
|
8
|
+
const PACKAGE = "primate-run";
|
|
9
|
+
const [MAJOR, MINOR] = TAG.split(".").map(Number);
|
|
10
|
+
function package_not_found() {
|
|
11
|
+
return fail("package not found - run 'pip install {0}~={1}.0'", PACKAGE, TAG);
|
|
12
|
+
}
|
|
13
|
+
function pkg_mismatch(major, minor) {
|
|
14
|
+
return fail("installed {0} package version {1}.{2}.x not in range ~> {3}.x", PACKAGE, major, minor, TAG);
|
|
15
|
+
}
|
|
16
|
+
async function show_package() {
|
|
17
|
+
const str0 = () => "";
|
|
18
|
+
let out = await execute(`pip show ${PACKAGE} 2>/dev/null`).catch(str0);
|
|
19
|
+
if (out.trim())
|
|
20
|
+
return out;
|
|
21
|
+
out = await execute(`uv pip show ${PACKAGE} 2>/dev/null`).catch(str0);
|
|
22
|
+
if (out.trim())
|
|
23
|
+
return out;
|
|
24
|
+
out = await execute(`python -m pip show ${PACKAGE} 2>/dev/null`).catch(str0);
|
|
25
|
+
if (out.trim())
|
|
26
|
+
return out;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
async function check_version() {
|
|
30
|
+
const output = await show_package();
|
|
31
|
+
if (!output)
|
|
32
|
+
throw package_not_found();
|
|
33
|
+
const version_match = output.match(/Version:\s*(\d+)\.(\d+)\.(\d+)/);
|
|
34
|
+
if (!version_match)
|
|
35
|
+
throw package_not_found();
|
|
36
|
+
const [major, minor] = version_match.slice(1).map(Number);
|
|
37
|
+
if (major !== MAJOR || minor !== MINOR)
|
|
38
|
+
throw pkg_mismatch(major, minor);
|
|
39
|
+
log.info("using {0} package {1}.{2}.x", PACKAGE, major, minor);
|
|
78
40
|
}
|
|
79
|
-
`;
|
|
80
|
-
};
|
|
81
41
|
export default class Default extends Runtime {
|
|
82
42
|
async build(app, next) {
|
|
43
|
+
await check_version();
|
|
83
44
|
const requirements_txt = app.root.join("requirements.txt");
|
|
84
45
|
let packages = [];
|
|
85
46
|
if (await requirements_txt.exists()) {
|
|
@@ -89,10 +50,20 @@ export default class Default extends Runtime {
|
|
|
89
50
|
.filter(line => line.trim() && !line.startsWith("#"))
|
|
90
51
|
.map(p => p.trim());
|
|
91
52
|
}
|
|
92
|
-
|
|
53
|
+
const packages_str = JSON.stringify(packages);
|
|
54
|
+
app.bind(this.fileExtension, async (file, { context }) => {
|
|
93
55
|
assert(context === "routes", "python: only route files are supported");
|
|
94
|
-
const
|
|
95
|
-
await
|
|
56
|
+
const relative = file.debase(app.path.routes).path.replace(/^\//, "");
|
|
57
|
+
const source = await file.text();
|
|
58
|
+
return `
|
|
59
|
+
import wrapper from "@primate/python/wrapper";
|
|
60
|
+
await wrapper(
|
|
61
|
+
${JSON.stringify(source)},
|
|
62
|
+
${packages_str},
|
|
63
|
+
"${PACKAGE}~=${TAG}.0",
|
|
64
|
+
"${relative}"
|
|
65
|
+
);
|
|
66
|
+
`;
|
|
96
67
|
});
|
|
97
68
|
return next(app);
|
|
98
69
|
}
|
package/lib/private/pyodide.d.ts
CHANGED
package/lib/private/pyodide.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { loadPyodide } from "pyodide";
|
|
2
|
+
let pyodide = null;
|
|
2
3
|
export default async function () {
|
|
3
|
-
|
|
4
|
+
if (!pyodide)
|
|
5
|
+
pyodide = await loadPyodide({ indexURL: "./node_modules/pyodide" });
|
|
6
|
+
return pyodide;
|
|
4
7
|
}
|
|
5
8
|
//# sourceMappingURL=pyodide.js.map
|
|
@@ -24,7 +24,7 @@ export default function to_request(request: RequestFacade): Promise<{
|
|
|
24
24
|
body: {
|
|
25
25
|
text: () => string;
|
|
26
26
|
json: () => string;
|
|
27
|
-
|
|
27
|
+
form: () => Dict<string>;
|
|
28
28
|
files: () => FileEntry[];
|
|
29
29
|
binary: () => {
|
|
30
30
|
buffer: Uint8Array<ArrayBuffer>;
|
|
@@ -28,41 +28,37 @@ function toDict(obj) {
|
|
|
28
28
|
}
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
|
-
async function
|
|
32
|
-
const
|
|
33
|
-
const plain = Object.create(null);
|
|
31
|
+
async function bridge_form(body) {
|
|
32
|
+
const form = Object.create(null);
|
|
34
33
|
const files = [];
|
|
35
34
|
const pending = [];
|
|
36
|
-
for (const [k, v] of Object.entries(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
35
|
+
for (const [k, v] of Object.entries(body.form())) {
|
|
36
|
+
form[k] = v;
|
|
37
|
+
}
|
|
38
|
+
for (const [k, v] of Object.entries(body.files())) {
|
|
39
|
+
const name = v.name;
|
|
40
|
+
const type = v.type;
|
|
41
|
+
const size = v.size;
|
|
42
|
+
pending.push(v.arrayBuffer().then(buffer => {
|
|
43
|
+
files.push({
|
|
44
|
+
bytes: new Uint8Array(buffer),
|
|
45
|
+
field: k,
|
|
46
|
+
name,
|
|
47
|
+
size,
|
|
48
|
+
type,
|
|
49
|
+
});
|
|
50
|
+
}));
|
|
55
51
|
}
|
|
56
52
|
await Promise.all(pending);
|
|
57
53
|
return {
|
|
58
|
-
|
|
54
|
+
form: () => form,
|
|
59
55
|
files: () => files,
|
|
60
56
|
};
|
|
61
57
|
}
|
|
62
58
|
async function bridgeBody(body) {
|
|
63
|
-
let
|
|
64
|
-
if (body.type === "
|
|
65
|
-
|
|
59
|
+
let form, buffer, blob;
|
|
60
|
+
if (body.type === "form") {
|
|
61
|
+
form = await bridge_form(body);
|
|
66
62
|
}
|
|
67
63
|
if (body.type === "binary") {
|
|
68
64
|
blob = body.binary();
|
|
@@ -73,8 +69,8 @@ async function bridgeBody(body) {
|
|
|
73
69
|
json: () => {
|
|
74
70
|
return JSON.stringify(body.json());
|
|
75
71
|
},
|
|
76
|
-
|
|
77
|
-
files: () =>
|
|
72
|
+
form: () => form.form(),
|
|
73
|
+
files: () => form.files(),
|
|
78
74
|
binary: () => {
|
|
79
75
|
const mime = blob.type || "application/octet-stream";
|
|
80
76
|
return {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import borrow from "@primate/python/borrow";
|
|
2
|
+
import helpers from "@primate/python/helpers";
|
|
3
|
+
import pyodide from "@primate/python/pyodide";
|
|
4
|
+
import to_request from "@primate/python/to-request";
|
|
5
|
+
import to_response from "@primate/python/to-response";
|
|
6
|
+
import session from "primate/config/session";
|
|
7
|
+
import route from "primate/route";
|
|
8
|
+
const messageCallback = () => { };
|
|
9
|
+
let _micropip = null;
|
|
10
|
+
const installed = new Set();
|
|
11
|
+
async function load_micropip(python) {
|
|
12
|
+
if (!_micropip) {
|
|
13
|
+
_micropip = (async () => {
|
|
14
|
+
await python.loadPackage("micropip", { messageCallback });
|
|
15
|
+
return python.pyimport("micropip");
|
|
16
|
+
})();
|
|
17
|
+
}
|
|
18
|
+
return _micropip;
|
|
19
|
+
}
|
|
20
|
+
async function load_package(micropip, pkg) {
|
|
21
|
+
if (installed.has(pkg))
|
|
22
|
+
return;
|
|
23
|
+
await micropip.install(pkg, { messageCallback });
|
|
24
|
+
installed.add(pkg);
|
|
25
|
+
}
|
|
26
|
+
export default async function wrapper(source, packages, primate_run, id) {
|
|
27
|
+
const python = await pyodide();
|
|
28
|
+
const micropip = await load_micropip(python);
|
|
29
|
+
const route_id = JSON.stringify(id);
|
|
30
|
+
await load_package(micropip, primate_run);
|
|
31
|
+
for (const pkg of packages) {
|
|
32
|
+
await load_package(micropip, pkg);
|
|
33
|
+
}
|
|
34
|
+
await python.runPython(`
|
|
35
|
+
from primate import Route
|
|
36
|
+
Route.clear(${route_id})
|
|
37
|
+
Route.scope(${route_id})
|
|
38
|
+
`);
|
|
39
|
+
await python.runPython(source);
|
|
40
|
+
const verbs = python.runPython(`
|
|
41
|
+
from primate import Route
|
|
42
|
+
list(Route.registry(${route_id}).keys())
|
|
43
|
+
`).toJs();
|
|
44
|
+
const wrapped_session = {
|
|
45
|
+
get id() {
|
|
46
|
+
return session().id;
|
|
47
|
+
},
|
|
48
|
+
get exists() {
|
|
49
|
+
return session().exists;
|
|
50
|
+
},
|
|
51
|
+
create(initial) {
|
|
52
|
+
session().create(borrow(initial));
|
|
53
|
+
},
|
|
54
|
+
get() {
|
|
55
|
+
return session().get();
|
|
56
|
+
},
|
|
57
|
+
try() {
|
|
58
|
+
return session().get();
|
|
59
|
+
},
|
|
60
|
+
set(data) {
|
|
61
|
+
session().set(borrow(data));
|
|
62
|
+
},
|
|
63
|
+
destroy() {
|
|
64
|
+
session().destroy();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
for (const verb of verbs) {
|
|
68
|
+
route[verb.toLowerCase()](async (request) => {
|
|
69
|
+
python.globals.set("js_req", await to_request(request));
|
|
70
|
+
python.globals.set("helpers_obj", helpers);
|
|
71
|
+
python.globals.set("session_obj", wrapped_session);
|
|
72
|
+
const verb_str = JSON.stringify(verb);
|
|
73
|
+
try {
|
|
74
|
+
const result = await python.runPythonAsync(`
|
|
75
|
+
from primate import Route
|
|
76
|
+
await Route.call_js(${route_id}, ${verb_str}, js_req, helpers_obj, session_obj)
|
|
77
|
+
`);
|
|
78
|
+
return to_response(result);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.error(`python error (${verb.toLowerCase()})`, e);
|
|
82
|
+
return { status: 500, body: "Python execution error: " + e.message };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
python.globals.delete("js_req");
|
|
86
|
+
python.globals.delete("helpers_obj");
|
|
87
|
+
python.globals.delete("session_obj");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=wrapper.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primate/python",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Primate Python backend",
|
|
5
5
|
"homepage": "https://primate.run/docs/backend/python",
|
|
6
6
|
"bugs": "https://github.com/primate-run/primate/issues",
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@rcompat/assert": "^0.3.1",
|
|
20
|
-
"@rcompat/fs": "^0.
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
20
|
+
"@rcompat/fs": "^0.22.3",
|
|
21
|
+
"@rcompat/stdio": "^0.12.2",
|
|
22
|
+
"pyodide": "^0.29.0",
|
|
23
|
+
"@primate/core": "^0.4.0",
|
|
24
|
+
"pema": "^0.4.0"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
|
-
"primate": "^0.
|
|
27
|
+
"primate": "^0.35.0"
|
|
27
28
|
},
|
|
28
29
|
"type": "module",
|
|
29
30
|
"imports": {
|