@qvac/harness 0.8.0 → 0.8.5
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/CHANGELOG.md +43 -1
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +16 -24
- package/dist-lib/lib/office/extract.d.ts +9 -0
- package/dist-lib/lib/office/extract.js +19 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +74 -19
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
- package/dist-lib/lib/tools/exec.js +9 -20
- package/dist-lib/lib/tools/office/ops.d.ts +13 -0
- package/dist-lib/lib/tools/office/ops.js +100 -0
- package/dist-lib/lib/tools/office.d.ts +12 -0
- package/dist-lib/lib/tools/office.js +486 -0
- package/package.json +10 -6
- package/wheels/manifest.json +1 -160
- package/worker/harness-sidecar.mjs +3 -1
- package/dist-lib/lib/office/pptx.d.ts +0 -1
- package/dist-lib/lib/office/pptx.js +0 -26
- package/dist-lib/lib/office/python.d.ts +0 -8
- package/dist-lib/lib/office/python.js +0 -32
- package/dist-lib/lib/office/xlsx.d.ts +0 -1
- package/dist-lib/lib/office/xlsx.js +0 -26
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
- package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
- package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
- package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
- package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
- package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
- package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
- package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
- package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
- package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
- package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
- package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
- package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
package/wheels/manifest.json
CHANGED
|
@@ -1,170 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pyodide": "314.0.2",
|
|
3
|
-
"packages": {
|
|
4
|
-
"fpdf2": {
|
|
5
|
-
"version": "2.8.8",
|
|
6
|
-
"imports": [
|
|
7
|
-
"fpdf"
|
|
8
|
-
],
|
|
9
|
-
"pypi": [
|
|
10
|
-
"defusedxml-0.7.1-py2.py3-none-any.whl",
|
|
11
|
-
"fpdf2-2.8.8-py3-none-any.whl"
|
|
12
|
-
],
|
|
13
|
-
"pyodide": [
|
|
14
|
-
"pillow",
|
|
15
|
-
"fonttools"
|
|
16
|
-
]
|
|
17
|
-
},
|
|
18
|
-
"pypdf": {
|
|
19
|
-
"version": "6.15.0",
|
|
20
|
-
"imports": [
|
|
21
|
-
"pypdf"
|
|
22
|
-
],
|
|
23
|
-
"pypi": [
|
|
24
|
-
"pypdf-6.15.0-py3-none-any.whl"
|
|
25
|
-
],
|
|
26
|
-
"pyodide": []
|
|
27
|
-
},
|
|
28
|
-
"openpyxl": {
|
|
29
|
-
"version": "3.1.5",
|
|
30
|
-
"imports": [
|
|
31
|
-
"openpyxl"
|
|
32
|
-
],
|
|
33
|
-
"pypi": [
|
|
34
|
-
"et_xmlfile-2.0.0-py3-none-any.whl",
|
|
35
|
-
"openpyxl-3.1.5-py2.py3-none-any.whl"
|
|
36
|
-
],
|
|
37
|
-
"pyodide": []
|
|
38
|
-
},
|
|
39
|
-
"python-docx": {
|
|
40
|
-
"version": "1.2.0",
|
|
41
|
-
"imports": [
|
|
42
|
-
"docx"
|
|
43
|
-
],
|
|
44
|
-
"pypi": [
|
|
45
|
-
"python_docx-1.2.0-py3-none-any.whl"
|
|
46
|
-
],
|
|
47
|
-
"pyodide": [
|
|
48
|
-
"typing-extensions",
|
|
49
|
-
"lxml"
|
|
50
|
-
]
|
|
51
|
-
},
|
|
52
|
-
"python-pptx": {
|
|
53
|
-
"version": "1.0.2",
|
|
54
|
-
"imports": [
|
|
55
|
-
"pptx"
|
|
56
|
-
],
|
|
57
|
-
"pypi": [
|
|
58
|
-
"xlsxwriter-3.2.9-py3-none-any.whl",
|
|
59
|
-
"python_pptx-1.0.2-py3-none-any.whl"
|
|
60
|
-
],
|
|
61
|
-
"pyodide": [
|
|
62
|
-
"typing-extensions",
|
|
63
|
-
"pillow",
|
|
64
|
-
"lxml"
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
"cryptography": {
|
|
68
|
-
"pypi": [],
|
|
69
|
-
"pyodide": [
|
|
70
|
-
"six",
|
|
71
|
-
"pycparser",
|
|
72
|
-
"cffi",
|
|
73
|
-
"cryptography"
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
"pillow": {
|
|
77
|
-
"pypi": [],
|
|
78
|
-
"pyodide": [
|
|
79
|
-
"pillow"
|
|
80
|
-
]
|
|
81
|
-
}
|
|
82
|
-
},
|
|
3
|
+
"packages": {},
|
|
83
4
|
"files": {
|
|
84
5
|
"micropip-0.11.1-py3-none-any.whl": {
|
|
85
6
|
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/micropip-0.11.1-py3-none-any.whl",
|
|
86
7
|
"sha256": "668cd1270f480ac9f93da4a2b4873a252743e4ad3da723d37a7e4c637c4f2314",
|
|
87
8
|
"channel": "pyodide"
|
|
88
|
-
},
|
|
89
|
-
"defusedxml-0.7.1-py2.py3-none-any.whl": {
|
|
90
|
-
"url": "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl",
|
|
91
|
-
"sha256": "a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61",
|
|
92
|
-
"channel": "pypi"
|
|
93
|
-
},
|
|
94
|
-
"fpdf2-2.8.8-py3-none-any.whl": {
|
|
95
|
-
"url": "https://files.pythonhosted.org/packages/f5/be/af012eda9507494f28b99b077423806c43a11573eb6225dd46f19ae2d263/fpdf2-2.8.8-py3-none-any.whl",
|
|
96
|
-
"sha256": "3557a478fc577a929c94aace9666aed4dcc432b5ab6764232e6a59f1ccd75f17",
|
|
97
|
-
"channel": "pypi"
|
|
98
|
-
},
|
|
99
|
-
"pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
|
|
100
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
|
|
101
|
-
"sha256": "a50d83a8a76d646768e99e93ed5bf365d0b83563fd8b597e77562e75c8f575a9",
|
|
102
|
-
"channel": "pyodide"
|
|
103
|
-
},
|
|
104
|
-
"fonttools-4.62.1-py3-none-any.whl": {
|
|
105
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/fonttools-4.62.1-py3-none-any.whl",
|
|
106
|
-
"sha256": "6d8caa09c191f26c47403499d6cff62f3fb9685b978baa047bf791862bcb298a",
|
|
107
|
-
"channel": "pyodide"
|
|
108
|
-
},
|
|
109
|
-
"pypdf-6.15.0-py3-none-any.whl": {
|
|
110
|
-
"url": "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl",
|
|
111
|
-
"sha256": "14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee",
|
|
112
|
-
"channel": "pypi"
|
|
113
|
-
},
|
|
114
|
-
"et_xmlfile-2.0.0-py3-none-any.whl": {
|
|
115
|
-
"url": "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl",
|
|
116
|
-
"sha256": "7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa",
|
|
117
|
-
"channel": "pypi"
|
|
118
|
-
},
|
|
119
|
-
"openpyxl-3.1.5-py2.py3-none-any.whl": {
|
|
120
|
-
"url": "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl",
|
|
121
|
-
"sha256": "5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2",
|
|
122
|
-
"channel": "pypi"
|
|
123
|
-
},
|
|
124
|
-
"python_docx-1.2.0-py3-none-any.whl": {
|
|
125
|
-
"url": "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl",
|
|
126
|
-
"sha256": "3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7",
|
|
127
|
-
"channel": "pypi"
|
|
128
|
-
},
|
|
129
|
-
"typing_extensions-4.15.0-py3-none-any.whl": {
|
|
130
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/typing_extensions-4.15.0-py3-none-any.whl",
|
|
131
|
-
"sha256": "213fc049fb8bbea5f8369b06c7270cdddd634c2c000f721ce445e22473d66fd5",
|
|
132
|
-
"channel": "pyodide"
|
|
133
|
-
},
|
|
134
|
-
"lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
|
|
135
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
|
|
136
|
-
"sha256": "f37f82e09f6d7957aa167440db7ef683439cc3ce1befac1d2ca27e35bd7c95d4",
|
|
137
|
-
"channel": "pyodide"
|
|
138
|
-
},
|
|
139
|
-
"xlsxwriter-3.2.9-py3-none-any.whl": {
|
|
140
|
-
"url": "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl",
|
|
141
|
-
"sha256": "9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3",
|
|
142
|
-
"channel": "pypi"
|
|
143
|
-
},
|
|
144
|
-
"python_pptx-1.0.2-py3-none-any.whl": {
|
|
145
|
-
"url": "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl",
|
|
146
|
-
"sha256": "160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba",
|
|
147
|
-
"channel": "pypi"
|
|
148
|
-
},
|
|
149
|
-
"six-1.17.0-py2.py3-none-any.whl": {
|
|
150
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/six-1.17.0-py2.py3-none-any.whl",
|
|
151
|
-
"sha256": "c6681057bd87fdec57e811b8e4007bad38deea284052817fa400d542c6423e2d",
|
|
152
|
-
"channel": "pyodide"
|
|
153
|
-
},
|
|
154
|
-
"pycparser-3.0-py3-none-any.whl": {
|
|
155
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/pycparser-3.0-py3-none-any.whl",
|
|
156
|
-
"sha256": "fab73d094251fa60eef1b744db416f4f1fae95b18741b207870b65aa2f3479ed",
|
|
157
|
-
"channel": "pyodide"
|
|
158
|
-
},
|
|
159
|
-
"cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
|
|
160
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
|
|
161
|
-
"sha256": "757845812fc8d2c94b0f0fb664d542e7d6e098263fbb5cda3b89dca5d7eff6d3",
|
|
162
|
-
"channel": "pyodide"
|
|
163
|
-
},
|
|
164
|
-
"cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl": {
|
|
165
|
-
"url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl",
|
|
166
|
-
"sha256": "4e0e7e8e47613dd96110c640b15ee525db72a47946b8b06f8bcd0377c403fc5a",
|
|
167
|
-
"channel": "pyodide"
|
|
168
9
|
}
|
|
169
10
|
}
|
|
170
11
|
}
|
|
@@ -20,6 +20,7 @@ import { deniedRootsFor } from '../dist-lib/lib/tools/fs/scope.js'
|
|
|
20
20
|
import { createGrepTool } from '../dist-lib/lib/tools/grep.js'
|
|
21
21
|
import { createHttpRequestTool } from '../dist-lib/lib/tools/http-request.js'
|
|
22
22
|
import { createMcpCallTool } from '../dist-lib/lib/tools/mcp-call.js'
|
|
23
|
+
import { createOfficeTools } from '../dist-lib/lib/tools/office.js'
|
|
23
24
|
import { compileOperationTools } from '../dist-lib/lib/tools/operations/compile.js'
|
|
24
25
|
import { createPdfTool } from '../dist-lib/lib/tools/pdf.js'
|
|
25
26
|
import { createReadFilesTool } from '../dist-lib/lib/tools/read-files.js'
|
|
@@ -134,7 +135,8 @@ export default async function start(ipc, ready) {
|
|
|
134
135
|
createGrepTool({ deniedRoots }),
|
|
135
136
|
createCreateDocumentsTool({ deniedRoots }),
|
|
136
137
|
createEditDocumentsTool({ deniedRoots }),
|
|
137
|
-
createPdfTool()
|
|
138
|
+
createPdfTool(),
|
|
139
|
+
...createOfficeTools()
|
|
138
140
|
]
|
|
139
141
|
if (skillCatalog) {
|
|
140
142
|
tools.push(...compileOperationTools(skillCatalog, { httpRequestTool, mcpCallTool }))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const PPTX_EXTRACT: string;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export const PPTX_EXTRACT = `from pptx import Presentation
|
|
2
|
-
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
def text(shapes):
|
|
6
|
-
for shape in shapes:
|
|
7
|
-
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
|
8
|
-
yield from text(shape.shapes)
|
|
9
|
-
elif shape.has_text_frame:
|
|
10
|
-
if shape.text.strip():
|
|
11
|
-
yield shape.text
|
|
12
|
-
elif shape.has_table:
|
|
13
|
-
for row in shape.table.rows:
|
|
14
|
-
cells = ["" if cell.is_spanned else cell.text.replace("\\n", " ") for cell in row.cells]
|
|
15
|
-
if any(cell.strip() for cell in cells):
|
|
16
|
-
yield " | ".join(cells)
|
|
17
|
-
|
|
18
|
-
with Path("extracted.txt").open("w", encoding="utf-8") as output:
|
|
19
|
-
for number, slide in enumerate(Presentation("input.pptx").slides, 1):
|
|
20
|
-
heading = False
|
|
21
|
-
for block in text(slide.shapes):
|
|
22
|
-
if not heading:
|
|
23
|
-
output.write("Slide " + str(number) + "\\n")
|
|
24
|
-
heading = True
|
|
25
|
-
output.write(block + "\\n")
|
|
26
|
-
`;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { AbortSignal } from 'bare-abort-controller';
|
|
2
|
-
export interface PythonExtractOptions {
|
|
3
|
-
readonly format: string;
|
|
4
|
-
readonly packages: readonly string[];
|
|
5
|
-
readonly source: string;
|
|
6
|
-
readonly signal?: AbortSignal | undefined;
|
|
7
|
-
}
|
|
8
|
-
export declare function extractWithPython(bytes: Uint8Array, fileName: string, { format, packages, source, signal }: PythonExtractOptions): Promise<string>;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import b4a from 'b4a';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { runPython } from '../tools/exec/adapters/python/pyodide.js';
|
|
4
|
-
import { planVendoredInstall, vendoredManifest, wheelsDir } from '../tools/exec/adapters/python/wheels.js';
|
|
5
|
-
const MAX_OFFICE_BYTES = 20 * 1024 * 1024;
|
|
6
|
-
export async function extractWithPython(bytes, fileName, { format, packages, source, signal }) {
|
|
7
|
-
if (bytes.byteLength > MAX_OFFICE_BYTES) {
|
|
8
|
-
throw new Error(`document is too large to extract: ${bytes.byteLength} bytes, limit ${MAX_OFFICE_BYTES}`);
|
|
9
|
-
}
|
|
10
|
-
const dir = wheelsDir();
|
|
11
|
-
const manifest = vendoredManifest(dir);
|
|
12
|
-
const install = planVendoredInstall(packages, { dir, manifest });
|
|
13
|
-
if (!manifest ||
|
|
14
|
-
!install ||
|
|
15
|
-
!['micropip', ...install.packages].every((name) => Object.keys(manifest.files).some((file) => file.replaceAll('_', '-').startsWith(`${name}-`) && fs.existsSync(`${dir}/${file}`)))) {
|
|
16
|
-
throw new Error(`bundled ${format} extraction packages are unavailable`);
|
|
17
|
-
}
|
|
18
|
-
const result = await runPython(source, {
|
|
19
|
-
packages,
|
|
20
|
-
inputs: [{ path: `input.${format.toLowerCase()}`, bytes }],
|
|
21
|
-
outputs: ['extracted.txt'],
|
|
22
|
-
signal
|
|
23
|
-
});
|
|
24
|
-
if (result.exitCode !== 0) {
|
|
25
|
-
throw new Error(`${fileName}: ${result.stderr.trim()}`);
|
|
26
|
-
}
|
|
27
|
-
const output = result.files?.get('extracted.txt');
|
|
28
|
-
if (!output) {
|
|
29
|
-
throw new Error(result.rejected?.[0]?.reason ?? `${format} extraction produced no text file`);
|
|
30
|
-
}
|
|
31
|
-
return b4a.toString(output, 'utf8');
|
|
32
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const XLSX_EXTRACT: string;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export const XLSX_EXTRACT = `from openpyxl import load_workbook
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
formulas = load_workbook("input.xlsx", read_only=True, data_only=False, keep_links=False)
|
|
5
|
-
values = load_workbook("input.xlsx", read_only=True, data_only=True, keep_links=False)
|
|
6
|
-
try:
|
|
7
|
-
with Path("extracted.txt").open("w", encoding="utf-8") as output:
|
|
8
|
-
for sheet in formulas.worksheets:
|
|
9
|
-
heading = False
|
|
10
|
-
cached = values[sheet.title]
|
|
11
|
-
sheet.reset_dimensions()
|
|
12
|
-
cached.reset_dimensions()
|
|
13
|
-
for cells, results in zip(sheet.iter_rows(), cached.iter_rows()):
|
|
14
|
-
row = [result.value if cell.data_type == "f" and result.value is not None else cell.value
|
|
15
|
-
for cell, result in zip(cells, results)]
|
|
16
|
-
if not any(value is not None and str(value).strip() for value in row):
|
|
17
|
-
continue
|
|
18
|
-
if not heading:
|
|
19
|
-
output.write("Sheet: " + sheet.title + "\\n")
|
|
20
|
-
heading = True
|
|
21
|
-
output.write(" | ".join("" if value is None else str(value).replace("\\n", " ")
|
|
22
|
-
for value in row) + "\\n")
|
|
23
|
-
finally:
|
|
24
|
-
formulas.close()
|
|
25
|
-
values.close()
|
|
26
|
-
`;
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { IMAGE_INPUT, OFFICE_READ, PDF_READ, isEscaped, parseInputs, parseOutputs, parsePackages, stripCommentsAndDocstrings, tripleQuoteAt, unwrapPythonSource } from './python-adapter.js';
|
|
2
|
-
const ARTIFACT_OUTPUT = /\.(docx|xlsx|pptx|pdf)$/i;
|
|
3
|
-
const CREATE_PACKAGES = new Map([
|
|
4
|
-
['python-docx', '1.2.0'],
|
|
5
|
-
['openpyxl', '3.1.5'],
|
|
6
|
-
['python-pptx', '1.0.2'],
|
|
7
|
-
['fpdf2', '2.8.8'],
|
|
8
|
-
['pillow', null]
|
|
9
|
-
]);
|
|
10
|
-
const PACKAGE_SPEC = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:==([0-9A-Za-z.]+))?$/;
|
|
11
|
-
const IMPORT_ROOTS = new Set(['docx', 'openpyxl', 'pptx', 'fpdf', 'datetime']);
|
|
12
|
-
const BUILDER_ROOTS = new Set(['docx', 'openpyxl', 'pptx', 'fpdf']);
|
|
13
|
-
const IMPORT_FROM = /^\s*from\s+([A-Za-z]\w*(?:\.[A-Za-z]\w*)*)\s+import\s+[A-Za-z]\w*(?:\s+as\s+[A-Za-z]\w*)?(?:\s*,\s*[A-Za-z]\w*(?:\s+as\s+[A-Za-z]\w*)?)*\s*$/;
|
|
14
|
-
const ALLOWED_BUILTINS = new Set([
|
|
15
|
-
'True',
|
|
16
|
-
'False',
|
|
17
|
-
'None',
|
|
18
|
-
'Ellipsis',
|
|
19
|
-
'NotImplemented',
|
|
20
|
-
'Exception',
|
|
21
|
-
'ValueError',
|
|
22
|
-
'KeyError',
|
|
23
|
-
'IndexError',
|
|
24
|
-
'TypeError',
|
|
25
|
-
'RuntimeError',
|
|
26
|
-
'abs',
|
|
27
|
-
'all',
|
|
28
|
-
'any',
|
|
29
|
-
'bool',
|
|
30
|
-
'dict',
|
|
31
|
-
'divmod',
|
|
32
|
-
'enumerate',
|
|
33
|
-
'filter',
|
|
34
|
-
'float',
|
|
35
|
-
'int',
|
|
36
|
-
'len',
|
|
37
|
-
'list',
|
|
38
|
-
'map',
|
|
39
|
-
'max',
|
|
40
|
-
'min',
|
|
41
|
-
'pow',
|
|
42
|
-
'print',
|
|
43
|
-
'range',
|
|
44
|
-
'reversed',
|
|
45
|
-
'round',
|
|
46
|
-
'set',
|
|
47
|
-
'sorted',
|
|
48
|
-
'str',
|
|
49
|
-
'sum',
|
|
50
|
-
'tuple',
|
|
51
|
-
'zip'
|
|
52
|
-
]);
|
|
53
|
-
const PYTHON_BUILTINS = new Set(`ArithmeticError AssertionError AttributeError BaseException BaseExceptionGroup
|
|
54
|
-
BlockingIOError BrokenPipeError BufferError BytesWarning ChildProcessError
|
|
55
|
-
ConnectionAbortedError ConnectionError ConnectionRefusedError ConnectionResetError
|
|
56
|
-
DeprecationWarning EOFError Ellipsis EncodingWarning EnvironmentError Exception
|
|
57
|
-
ExceptionGroup False FileExistsError FileNotFoundError FloatingPointError
|
|
58
|
-
FutureWarning GeneratorExit IOError ImportError ImportWarning IndentationError
|
|
59
|
-
IndexError InterruptedError IsADirectoryError KeyError KeyboardInterrupt
|
|
60
|
-
LookupError MemoryError ModuleNotFoundError NameError None NotADirectoryError
|
|
61
|
-
NotImplemented NotImplementedError OSError OverflowError PendingDeprecationWarning
|
|
62
|
-
PermissionError ProcessLookupError PythonFinalizationError RecursionError
|
|
63
|
-
ReferenceError ResourceWarning RuntimeError RuntimeWarning StopAsyncIteration
|
|
64
|
-
StopIteration SyntaxError SyntaxWarning SystemError SystemExit TabError
|
|
65
|
-
TimeoutError True TypeError UnboundLocalError UnicodeDecodeError
|
|
66
|
-
UnicodeEncodeError UnicodeError UnicodeTranslateError UnicodeWarning UserWarning
|
|
67
|
-
ValueError Warning ZeroDivisionError abs aiter all anext any ascii bin bool
|
|
68
|
-
breakpoint bytearray bytes callable chr classmethod compile complex copyright
|
|
69
|
-
credits delattr dict dir divmod enumerate eval exec execfile exit filter float
|
|
70
|
-
format frozenset getattr globals hasattr hash help hex id input int isinstance
|
|
71
|
-
issubclass iter len license list locals map max memoryview min next object oct
|
|
72
|
-
open ord pow print property quit range reload repr reversed round set setattr
|
|
73
|
-
slice sorted staticmethod str sum super tuple type unicode vars zip`.split(/\s+/));
|
|
74
|
-
const DENIED_TOKENS = new Set([
|
|
75
|
-
'os',
|
|
76
|
-
'sys',
|
|
77
|
-
'js',
|
|
78
|
-
'pyodide',
|
|
79
|
-
'micropip',
|
|
80
|
-
'builtins',
|
|
81
|
-
'importlib',
|
|
82
|
-
'inspect',
|
|
83
|
-
'ctypes',
|
|
84
|
-
'subprocess',
|
|
85
|
-
'platform',
|
|
86
|
-
'shutil',
|
|
87
|
-
'pathlib',
|
|
88
|
-
'glob',
|
|
89
|
-
'tempfile',
|
|
90
|
-
'gc',
|
|
91
|
-
'socket',
|
|
92
|
-
'requests',
|
|
93
|
-
'urllib',
|
|
94
|
-
'http',
|
|
95
|
-
'ssl',
|
|
96
|
-
'ftplib',
|
|
97
|
-
'smtplib',
|
|
98
|
-
'webbrowser',
|
|
99
|
-
'pickle',
|
|
100
|
-
'marshal',
|
|
101
|
-
'base64',
|
|
102
|
-
'codecs',
|
|
103
|
-
'io',
|
|
104
|
-
'threading',
|
|
105
|
-
'asyncio',
|
|
106
|
-
'async',
|
|
107
|
-
'await',
|
|
108
|
-
'format_map',
|
|
109
|
-
'vformat'
|
|
110
|
-
]);
|
|
111
|
-
const INTROSPECTION_TOKEN = /^(?:gi|cr|ag|f|tb|im|func)_/;
|
|
112
|
-
function isFStringPrefix(code) {
|
|
113
|
-
const prefix = /[A-Za-z]{1,3}$/.exec(code)?.[0] ?? '';
|
|
114
|
-
return prefix.length <= 2 && /f/i.test(prefix);
|
|
115
|
-
}
|
|
116
|
-
export function splitPythonSource(source) {
|
|
117
|
-
let code = '';
|
|
118
|
-
let text = '';
|
|
119
|
-
let i = 0;
|
|
120
|
-
while (i < source.length) {
|
|
121
|
-
const ch = source[i];
|
|
122
|
-
if (ch === '#') {
|
|
123
|
-
while (i < source.length && source[i] !== '\n') {
|
|
124
|
-
text += source[i];
|
|
125
|
-
i++;
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const triple = tripleQuoteAt(source, i);
|
|
130
|
-
const quote = triple ?? (ch === '"' || ch === "'" ? ch : null);
|
|
131
|
-
if (quote) {
|
|
132
|
-
const fstring = isFStringPrefix(code);
|
|
133
|
-
const end = scanLiteral(source, i + quote.length, { quote, fstring });
|
|
134
|
-
if (!end) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
code += '""';
|
|
138
|
-
text += end.text;
|
|
139
|
-
code += end.code;
|
|
140
|
-
i = end.index;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
code += ch;
|
|
144
|
-
i++;
|
|
145
|
-
}
|
|
146
|
-
return { code, text };
|
|
147
|
-
}
|
|
148
|
-
function scanLiteral(source, start, { quote, fstring }) {
|
|
149
|
-
let text = '';
|
|
150
|
-
let code = '';
|
|
151
|
-
let i = start;
|
|
152
|
-
while (i < source.length) {
|
|
153
|
-
if (source.startsWith(quote, i) && !isEscaped(source, i)) {
|
|
154
|
-
return { index: i + quote.length, text, code };
|
|
155
|
-
}
|
|
156
|
-
const ch = source[i];
|
|
157
|
-
if (fstring && (ch === '{' || ch === '}')) {
|
|
158
|
-
if (source[i + 1] === ch) {
|
|
159
|
-
text += ch + ch;
|
|
160
|
-
i += 2;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (ch === '}') {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const interior = scanInterpolation(source, i + 1);
|
|
167
|
-
if (!interior) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
code += ` ${interior.code} `;
|
|
171
|
-
text += interior.text;
|
|
172
|
-
i = interior.index;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
text += ch;
|
|
176
|
-
i++;
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
function scanInterpolation(source, start) {
|
|
181
|
-
let code = '';
|
|
182
|
-
let text = '';
|
|
183
|
-
let depth = 1;
|
|
184
|
-
let nested = null;
|
|
185
|
-
let i = start;
|
|
186
|
-
while (i < source.length) {
|
|
187
|
-
const ch = source[i];
|
|
188
|
-
if (nested) {
|
|
189
|
-
if (ch === nested && !isEscaped(source, i)) {
|
|
190
|
-
nested = null;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
text += ch;
|
|
194
|
-
}
|
|
195
|
-
i++;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (ch === '"' || ch === "'") {
|
|
199
|
-
nested = ch;
|
|
200
|
-
i++;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (ch === '{') {
|
|
204
|
-
depth++;
|
|
205
|
-
}
|
|
206
|
-
if (ch === '}') {
|
|
207
|
-
depth--;
|
|
208
|
-
if (depth === 0) {
|
|
209
|
-
return { index: i + 1, text, code };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
code += ch;
|
|
213
|
-
i++;
|
|
214
|
-
}
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
function isKeywordArgument(code, start, length) {
|
|
218
|
-
const before = code.slice(0, start).trimEnd();
|
|
219
|
-
const last = before[before.length - 1];
|
|
220
|
-
if (last !== '(' && last !== ',') {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
const after = code.slice(start + length).trimStart();
|
|
224
|
-
return after.startsWith('=') && after[1] !== '=';
|
|
225
|
-
}
|
|
226
|
-
function lintCreateSource(source) {
|
|
227
|
-
const halves = splitPythonSource(source);
|
|
228
|
-
if (!halves) {
|
|
229
|
-
return 'write';
|
|
230
|
-
}
|
|
231
|
-
if (halves.text.includes('__')) {
|
|
232
|
-
return 'write';
|
|
233
|
-
}
|
|
234
|
-
if (/\\(?:x[0-9A-Fa-f]|u[0-9A-Fa-f]|U[0-9A-Fa-f]|N\{|[0-7])/.test(halves.text)) {
|
|
235
|
-
return 'write';
|
|
236
|
-
}
|
|
237
|
-
if (/[^\x20-\x7e\t\r\n\f]/.test(halves.code)) {
|
|
238
|
-
return 'write';
|
|
239
|
-
}
|
|
240
|
-
let builder = false;
|
|
241
|
-
for (const line of halves.code.split('\n')) {
|
|
242
|
-
for (const statement of line.split(';')) {
|
|
243
|
-
if (!/\b(?:from|import)\b/.test(statement))
|
|
244
|
-
continue;
|
|
245
|
-
const match = IMPORT_FROM.exec(statement);
|
|
246
|
-
if (!match) {
|
|
247
|
-
return 'write';
|
|
248
|
-
}
|
|
249
|
-
const root = match[1].split('.')[0];
|
|
250
|
-
if (!IMPORT_ROOTS.has(root)) {
|
|
251
|
-
return 'write';
|
|
252
|
-
}
|
|
253
|
-
if (BUILDER_ROOTS.has(root)) {
|
|
254
|
-
builder = true;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (!builder) {
|
|
259
|
-
return 'write';
|
|
260
|
-
}
|
|
261
|
-
for (const match of halves.code.matchAll(/[A-Za-z_]\w*/g)) {
|
|
262
|
-
const token = match[0];
|
|
263
|
-
if (token.includes('__') || DENIED_TOKENS.has(token) || INTROSPECTION_TOKEN.test(token)) {
|
|
264
|
-
return 'write';
|
|
265
|
-
}
|
|
266
|
-
if (isKeywordArgument(halves.code, match.index, token.length)) {
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (PYTHON_BUILTINS.has(token) && !ALLOWED_BUILTINS.has(token)) {
|
|
270
|
-
return 'write';
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const openers = stripCommentsAndDocstrings(source);
|
|
274
|
-
const reads = [new RegExp(OFFICE_READ.source), new RegExp(PDF_READ.source)];
|
|
275
|
-
if (reads.some((pattern) => pattern.test(openers))) {
|
|
276
|
-
return 'write';
|
|
277
|
-
}
|
|
278
|
-
return 'read';
|
|
279
|
-
}
|
|
280
|
-
export function pythonCreateEffect(args) {
|
|
281
|
-
const outputs = parseOutputs(args.outputs);
|
|
282
|
-
if (!Array.isArray(outputs) || outputs.length === 0) {
|
|
283
|
-
return 'write';
|
|
284
|
-
}
|
|
285
|
-
if (!outputs.every((name) => ARTIFACT_OUTPUT.test(name))) {
|
|
286
|
-
return 'write';
|
|
287
|
-
}
|
|
288
|
-
const packages = parsePackages(args.packages);
|
|
289
|
-
if (packages && 'error' in packages) {
|
|
290
|
-
return 'write';
|
|
291
|
-
}
|
|
292
|
-
for (const spec of packages ?? []) {
|
|
293
|
-
const match = PACKAGE_SPEC.exec(spec);
|
|
294
|
-
if (!match) {
|
|
295
|
-
return 'write';
|
|
296
|
-
}
|
|
297
|
-
const name = match[1].toLowerCase().replace(/[_.]+/g, '-');
|
|
298
|
-
if (!CREATE_PACKAGES.has(name)) {
|
|
299
|
-
return 'write';
|
|
300
|
-
}
|
|
301
|
-
if (CREATE_PACKAGES.get(name) !== (match[2] ?? null)) {
|
|
302
|
-
return 'write';
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const inputs = parseInputs(args.inputs);
|
|
306
|
-
if (inputs && 'error' in inputs) {
|
|
307
|
-
return 'write';
|
|
308
|
-
}
|
|
309
|
-
if ((inputs ?? []).some((input) => !IMAGE_INPUT.test(input.path))) {
|
|
310
|
-
return 'write';
|
|
311
|
-
}
|
|
312
|
-
const command = String(args.command ?? '');
|
|
313
|
-
const source = unwrapPythonSource(command) ?? command;
|
|
314
|
-
if (!source.trim()) {
|
|
315
|
-
return 'write';
|
|
316
|
-
}
|
|
317
|
-
return lintCreateSource(source);
|
|
318
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|