datagrok-tools 6.1.0 → 6.1.1
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/bin/commands/config.js +42 -6
- package/bin/commands/docker-gen.js +16 -0
- package/bin/commands/help.js +28 -14
- package/bin/commands/publish.js +129 -44
- package/bin/commands/test.js +540 -5
- package/bin/grok.js +2 -0
- package/bin/utils/python-celery-gen.js +247 -0
- package/bin/utils/test-utils.js +8 -4
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.generateCeleryArtifacts = generateCeleryArtifacts;
|
|
8
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
9
|
+
var _path = _interopRequireDefault(require("path"));
|
|
10
|
+
var color = _interopRequireWildcard(require("./color-utils"));
|
|
11
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
12
|
+
// Header tags recognized in Python function metadata comments (ported from ScriptParser.headerTags)
|
|
13
|
+
const headerTags = ['name', 'description', 'help-url', 'input', 'output', 'tags', 'sample', 'language', 'endpoint', 'requiresServer', 'param-csrfmiddlewaretoken', 'returns', 'test', 'sidebar', 'condition', 'top-menu', 'environment', 'require', 'editor-for', 'schedule', 'schedule.runAs', 'reference', 'editor'];
|
|
14
|
+
const paramRegex = new RegExp(`(#\\s*(${headerTags.join('|')}|meta\\.[^:]*):)\\s*(\\S+)\\s?(\\S+)?`);
|
|
15
|
+
const defRegex = /^\s*def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/;
|
|
16
|
+
const TASKS_FILE = 'tasks.yaml';
|
|
17
|
+
const TEMPLATE_PIP_DOCKER = `FROM python:3.11-slim-bookworm
|
|
18
|
+
|
|
19
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
20
|
+
gcc \\
|
|
21
|
+
libc-dev \\
|
|
22
|
+
&& apt-get clean \\
|
|
23
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24
|
+
|
|
25
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
26
|
+
|
|
27
|
+
RUN useradd -m datagrok
|
|
28
|
+
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
COPY . /app
|
|
31
|
+
ENV PYTHONPATH="/app:\${PYTHONPATH}"
|
|
32
|
+
RUN chown -R datagrok:datagrok /app
|
|
33
|
+
|
|
34
|
+
# Install Python dependencies using uv
|
|
35
|
+
RUN uv pip install --system celery datagrok-celery-task pyyaml && \\
|
|
36
|
+
if [ -f requirements.in ]; then uv pip install --system --no-cache -r requirements.in; \\
|
|
37
|
+
elif [ -f requirements.txt ]; then uv pip install --system --no-cache -r requirements.txt; fi
|
|
38
|
+
|
|
39
|
+
USER datagrok
|
|
40
|
+
|
|
41
|
+
EXPOSE 8000
|
|
42
|
+
|
|
43
|
+
CMD celery -A \$DATAGROK_CELERY_NAME worker \\
|
|
44
|
+
--loglevel=info \\
|
|
45
|
+
--hostname=\$CELERY_HOSTNAME \\
|
|
46
|
+
--concurrency=\$WORKERS_PROCESSES \\
|
|
47
|
+
-Q \$TASK_QUEUE_NAME
|
|
48
|
+
`;
|
|
49
|
+
const TEMPLATE_CONDA_DOCKER = `FROM mambaorg/micromamba:latest as builder
|
|
50
|
+
|
|
51
|
+
USER root
|
|
52
|
+
|
|
53
|
+
ENV MAMBA_ROOT_PREFIX=/opt/conda \\
|
|
54
|
+
MAMBA_DOCKERFILE_ACTIVATE=1 \\
|
|
55
|
+
DEFAULT_ENV_NAME=myenv
|
|
56
|
+
|
|
57
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
58
|
+
|
|
59
|
+
WORKDIR /tmp/app
|
|
60
|
+
|
|
61
|
+
COPY environment.yaml environment.yml* ./
|
|
62
|
+
|
|
63
|
+
RUN if [ -f environment.yaml ]; then sed -i 's/\\r\$//' environment.yaml; fi && \\
|
|
64
|
+
if [ -f environment.yml ]; then sed -i 's/\\r\$//' environment.yml; fi
|
|
65
|
+
|
|
66
|
+
RUN ENV_FILE=\$( [ -f environment.yaml ] && echo environment.yaml || echo environment.yml ) && \\
|
|
67
|
+
ENV_NAME=\$(grep '^name:' \$ENV_FILE | cut -d' ' -f2 || echo "myenv") && \\
|
|
68
|
+
micromamba create -y -n \$ENV_NAME -f \$ENV_FILE && \\
|
|
69
|
+
uv pip install --python /opt/conda/envs/\$ENV_NAME/bin/python celery datagrok-celery-task pyyaml && \\
|
|
70
|
+
echo "\$ENV_NAME" > /tmp/env_name.txt
|
|
71
|
+
|
|
72
|
+
RUN micromamba clean --all --yes
|
|
73
|
+
|
|
74
|
+
FROM mambaorg/micromamba:latest
|
|
75
|
+
|
|
76
|
+
USER root
|
|
77
|
+
|
|
78
|
+
ENV MAMBA_ROOT_PREFIX=/opt/conda \\
|
|
79
|
+
MAMBA_DOCKERFILE_ACTIVATE=1
|
|
80
|
+
|
|
81
|
+
# Create non-root user
|
|
82
|
+
RUN useradd -m -s /bin/bash grok
|
|
83
|
+
|
|
84
|
+
WORKDIR /app
|
|
85
|
+
|
|
86
|
+
COPY --from=builder /opt/conda /opt/conda
|
|
87
|
+
COPY --from=builder /tmp/env_name.txt /opt/env_name.txt
|
|
88
|
+
|
|
89
|
+
COPY . /app
|
|
90
|
+
|
|
91
|
+
RUN chown -R grok:grok /opt/conda /app /opt/env_name.txt
|
|
92
|
+
EXPOSE 8000
|
|
93
|
+
USER grok
|
|
94
|
+
SHELL ["/bin/bash", "-c"]
|
|
95
|
+
|
|
96
|
+
CMD micromamba run -n \$(cat /opt/env_name.txt) \\
|
|
97
|
+
celery -A \$DATAGROK_CELERY_NAME worker \\
|
|
98
|
+
--loglevel=info \\
|
|
99
|
+
--hostname=\$CELERY_HOSTNAME \\
|
|
100
|
+
--concurrency=\$WORKERS_PROCESSES \\
|
|
101
|
+
-Q \$TASK_QUEUE_NAME
|
|
102
|
+
`;
|
|
103
|
+
const TEMPLATE_PYTHON_ENTRY = `import os
|
|
104
|
+
import importlib
|
|
105
|
+
import logging
|
|
106
|
+
|
|
107
|
+
import yaml
|
|
108
|
+
from celery import Celery
|
|
109
|
+
from datagrok_celery_task import DatagrokTask, Settings
|
|
110
|
+
|
|
111
|
+
import sys
|
|
112
|
+
sys.path.insert(0, os.getcwd())
|
|
113
|
+
|
|
114
|
+
settings = Settings(log_level=logging.DEBUG)
|
|
115
|
+
app = Celery(settings.celery_name, broker=settings.broker_url)
|
|
116
|
+
|
|
117
|
+
with open("${TASKS_FILE}") as f:
|
|
118
|
+
tasks = yaml.safe_load(f)
|
|
119
|
+
|
|
120
|
+
if not isinstance(tasks, dict) or not isinstance(tasks['tasks'], list):
|
|
121
|
+
raise RuntimeError('Incorrect format for tasks.yaml. It should contain tasks field.')
|
|
122
|
+
|
|
123
|
+
for task_def in tasks['tasks']:
|
|
124
|
+
mod = importlib.import_module(task_def["module"])
|
|
125
|
+
fn = getattr(mod, task_def["name"])
|
|
126
|
+
app.task(name=task_def["name"], base=DatagrokTask)(fn)
|
|
127
|
+
`;
|
|
128
|
+
function getAllPyFiles(dir) {
|
|
129
|
+
const results = [];
|
|
130
|
+
const entries = _fs.default.readdirSync(dir, {
|
|
131
|
+
withFileTypes: true
|
|
132
|
+
});
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const fullPath = _path.default.join(dir, entry.name);
|
|
135
|
+
if (entry.isDirectory()) results.push(...getAllPyFiles(fullPath));else if (entry.name.endsWith('.py')) results.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
function scanPythonFunctions(pythonDir, root) {
|
|
140
|
+
const pyFiles = getAllPyFiles(pythonDir);
|
|
141
|
+
const tasks = [];
|
|
142
|
+
for (const pyFile of pyFiles) {
|
|
143
|
+
const content = _fs.default.readFileSync(pyFile, 'utf-8');
|
|
144
|
+
const lines = content.split('\n');
|
|
145
|
+
let header = [];
|
|
146
|
+
let isHeader = false;
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (line.length > 0 && paramRegex.test(line)) {
|
|
149
|
+
if (!isHeader) isHeader = true;
|
|
150
|
+
header.push(line);
|
|
151
|
+
} else {
|
|
152
|
+
if (isHeader && defRegex.test(line)) {
|
|
153
|
+
const match = defRegex.exec(line);
|
|
154
|
+
const name = match[1];
|
|
155
|
+
let module = _path.default.basename(pyFile, '.py');
|
|
156
|
+
const fileDir = _path.default.dirname(pyFile);
|
|
157
|
+
if (fileDir !== root) {
|
|
158
|
+
const relDir = _path.default.relative(root, fileDir).replace(/[\\/]+/g, '.');
|
|
159
|
+
if (relDir && relDir !== '.') module = relDir + '.' + module;
|
|
160
|
+
}
|
|
161
|
+
tasks.push({
|
|
162
|
+
name,
|
|
163
|
+
module
|
|
164
|
+
});
|
|
165
|
+
isHeader = false;
|
|
166
|
+
header = [];
|
|
167
|
+
} else if (isHeader) {
|
|
168
|
+
isHeader = false;
|
|
169
|
+
header = [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return tasks;
|
|
175
|
+
}
|
|
176
|
+
function copyDirContents(src, dest) {
|
|
177
|
+
const entries = _fs.default.readdirSync(src, {
|
|
178
|
+
withFileTypes: true
|
|
179
|
+
});
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const srcPath = _path.default.join(src, entry.name);
|
|
182
|
+
const destPath = _path.default.join(dest, entry.name);
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
_fs.default.mkdirSync(destPath, {
|
|
185
|
+
recursive: true
|
|
186
|
+
});
|
|
187
|
+
copyDirContents(srcPath, destPath);
|
|
188
|
+
} else _fs.default.copyFileSync(srcPath, destPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function useConda(dir) {
|
|
192
|
+
return _fs.default.existsSync(_path.default.join(dir, 'environment.yaml')) || _fs.default.existsSync(_path.default.join(dir, 'environment.yml'));
|
|
193
|
+
}
|
|
194
|
+
function deployFolder(packageDir, folderPath, dirName) {
|
|
195
|
+
const tasks = scanPythonFunctions(folderPath, folderPath);
|
|
196
|
+
if (tasks.length === 0) {
|
|
197
|
+
color.log(`No annotated Python functions found in ${dirName}`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const dockerfilesDir = _path.default.join(packageDir, 'dockerfiles', dirName);
|
|
201
|
+
_fs.default.mkdirSync(dockerfilesDir, {
|
|
202
|
+
recursive: true
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Generate Dockerfile
|
|
206
|
+
const dockerfile = useConda(folderPath) ? TEMPLATE_CONDA_DOCKER : TEMPLATE_PIP_DOCKER;
|
|
207
|
+
_fs.default.writeFileSync(_path.default.join(dockerfilesDir, 'Dockerfile'), dockerfile);
|
|
208
|
+
|
|
209
|
+
// Generate tasks.yaml (JSON-encoded, matching server behavior)
|
|
210
|
+
_fs.default.writeFileSync(_path.default.join(dockerfilesDir, TASKS_FILE), JSON.stringify({
|
|
211
|
+
tasks
|
|
212
|
+
}, null, 2));
|
|
213
|
+
|
|
214
|
+
// Generate Celery entry point
|
|
215
|
+
const celeryName = dirName.replace(/-/g, '_');
|
|
216
|
+
_fs.default.writeFileSync(_path.default.join(dockerfilesDir, celeryName + '.py'), TEMPLATE_PYTHON_ENTRY);
|
|
217
|
+
|
|
218
|
+
// Copy Python source files
|
|
219
|
+
copyDirContents(folderPath, dockerfilesDir);
|
|
220
|
+
|
|
221
|
+
// Copy container.json if present
|
|
222
|
+
const containerJsonSrc = _path.default.join(folderPath, 'container.json');
|
|
223
|
+
const containerJsonDest = _path.default.join(dockerfilesDir, 'container.json');
|
|
224
|
+
if (_fs.default.existsSync(containerJsonSrc) && !_fs.default.existsSync(containerJsonDest)) _fs.default.copyFileSync(containerJsonSrc, containerJsonDest);
|
|
225
|
+
color.log(`Generated Celery Docker artifacts in dockerfiles/${dirName}/`);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
function generateCeleryArtifacts(packageDir) {
|
|
229
|
+
const pythonDir = _path.default.join(packageDir, 'python');
|
|
230
|
+
if (!_fs.default.existsSync(pythonDir)) return false;
|
|
231
|
+
const entries = _fs.default.readdirSync(pythonDir, {
|
|
232
|
+
withFileTypes: true
|
|
233
|
+
});
|
|
234
|
+
const isNested = entries.length > 0 && entries.every(e => e.isDirectory);
|
|
235
|
+
let generated = false;
|
|
236
|
+
if (isNested) {
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (!entry.isDirectory()) continue;
|
|
239
|
+
const folderPath = _path.default.join(pythonDir, entry.name);
|
|
240
|
+
if (deployFolder(packageDir, folderPath, entry.name)) generated = true;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
const dirName = _path.default.basename(packageDir).toLowerCase();
|
|
244
|
+
if (deployFolder(packageDir, pythonDir, dirName)) generated = true;
|
|
245
|
+
}
|
|
246
|
+
return generated;
|
|
247
|
+
}
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -114,7 +114,11 @@ function getDevKey(hostKey) {
|
|
|
114
114
|
key
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
function appendUrlParams(url, extraParams) {
|
|
118
|
+
if (!extraParams) return url;
|
|
119
|
+
return url + (url.includes('?') ? '&' : '?') + extraParams;
|
|
120
|
+
}
|
|
121
|
+
async function getBrowserPage(puppeteer, params = defaultLaunchParameters, urlParams) {
|
|
118
122
|
let url = process.env.HOST ?? '';
|
|
119
123
|
const cfg = getDevKey(url);
|
|
120
124
|
url = cfg.url;
|
|
@@ -153,7 +157,7 @@ async function getBrowserPage(puppeteer, params = defaultLaunchParameters) {
|
|
|
153
157
|
await page.evaluate(token => {
|
|
154
158
|
window.localStorage.setItem('auth', token);
|
|
155
159
|
}, token);
|
|
156
|
-
await page.goto(url);
|
|
160
|
+
await page.goto(appendUrlParams(url, urlParams));
|
|
157
161
|
try {
|
|
158
162
|
// await page.waitForSelector('.grok-preloader', { timeout: 1800000 });
|
|
159
163
|
await page.waitForFunction(() => document.querySelector('.grok-preloader') == null, {
|
|
@@ -625,7 +629,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
625
629
|
|
|
626
630
|
// Remove old console listeners before refresh to avoid stale state references
|
|
627
631
|
page.removeAllListeners('console');
|
|
628
|
-
await page.goto(webUrl);
|
|
632
|
+
await page.goto(appendUrlParams(webUrl, browserOptions.urlParams));
|
|
629
633
|
try {
|
|
630
634
|
await page.waitForFunction(() => document.querySelector('.grok-preloader') == null, {
|
|
631
635
|
timeout: 3600000
|
|
@@ -639,7 +643,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
639
643
|
devtools: browserOptions.debug
|
|
640
644
|
}, defaultLaunchParameters);
|
|
641
645
|
if (browserOptions.gui) params['headless'] = false;
|
|
642
|
-
const out = await getBrowserPage(_puppeteer.default, params);
|
|
646
|
+
const out = await getBrowserPage(_puppeteer.default, params, browserOptions.urlParams);
|
|
643
647
|
browser = out.browser;
|
|
644
648
|
page = out.page;
|
|
645
649
|
webUrl = await getWebUrlFromPage(page);
|
package/package.json
CHANGED