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.
@@ -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
+ }
@@ -114,7 +114,11 @@ function getDevKey(hostKey) {
114
114
  key
115
115
  };
116
116
  }
117
- async function getBrowserPage(puppeteer, params = defaultLaunchParameters) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.0",
3
+ "version": "6.1.1",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {