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.
@@ -19,6 +19,15 @@ const confTemplate = _jsYaml.default.load(_fs.default.readFileSync(confTemplateD
19
19
  }));
20
20
  const grokDir = _path.default.join(_os.default.homedir(), '.grok');
21
21
  const confPath = _path.default.join(grokDir, 'config.yaml');
22
+ function defaultRegistry(url) {
23
+ try {
24
+ const hostname = new URL(url).hostname;
25
+ if (hostname === 'localhost' || /^[\d.]+$/.test(hostname) || hostname === '::1') return '';
26
+ return `registry.${hostname}`;
27
+ } catch (e) {
28
+ return '';
29
+ }
30
+ }
22
31
  function validateKey(key) {
23
32
  if (!key || /^([A-Za-z\d-])+$/.test(key)) return true;else return 'Developer key may only include letters, numbers, or hyphens';
24
33
  }
@@ -33,7 +42,7 @@ function generateKeyQ(server, url) {
33
42
  if (server.startsWith('local')) question.message = `Developer key for ${origin} (press ENTER to skip):`;
34
43
  return question;
35
44
  }
36
- async function addNewServer(config) {
45
+ async function addNewServer(config, askRegistry = false) {
37
46
  while (true) {
38
47
  const addServer = (await _inquirer.default.prompt({
39
48
  name: 'add-server',
@@ -53,17 +62,29 @@ async function addNewServer(config) {
53
62
  message: 'Enter a URL:'
54
63
  }))['server-url'];
55
64
  const key = (await _inquirer.default.prompt(generateKeyQ(name, url)))[name];
56
- config.servers[name] = {
65
+ const entry = {
57
66
  url,
58
67
  key
59
68
  };
69
+ if (askRegistry) {
70
+ const regDefault = defaultRegistry(url);
71
+ const reg = (await _inquirer.default.prompt({
72
+ name: 'registry',
73
+ type: 'input',
74
+ message: `Docker registry for ${name}:`,
75
+ default: regDefault || undefined
76
+ }))['registry'] || '';
77
+ if (reg) entry.registry = reg;
78
+ }
79
+ config.servers[name] = entry;
60
80
  } else break;
61
81
  }
62
82
  }
63
83
  function config(args) {
64
84
  const nOptions = Object.keys(args).length - 1;
65
- const interactiveMode = args['_'].length === 1 && (nOptions < 1 || nOptions === 1 && args.reset);
66
- const hasAddServerCommand = args['_'].length === 2 && args['_'][1] === 'add' && args.server && args.key && args.k && args.alias && (nOptions === 4 || nOptions === 5 && args.default);
85
+ const askRegistry = args.registry != null;
86
+ const interactiveMode = args['_'].length === 1 && (nOptions < 1 || nOptions === 1 && (args.reset || askRegistry) || nOptions === 2 && args.reset && askRegistry);
87
+ const hasAddServerCommand = args['_'].length === 2 && args['_'][1] === 'add' && args.server && args.key && args.k && args.alias && nOptions >= 4 && nOptions <= 6;
67
88
  if (!interactiveMode && !hasAddServerCommand) return false;
68
89
  if (!_fs.default.existsSync(grokDir)) _fs.default.mkdirSync(grokDir);
69
90
  if (!_fs.default.existsSync(confPath) || args.reset) _fs.default.writeFileSync(confPath, _jsYaml.default.dump(confTemplate));
@@ -77,10 +98,15 @@ function config(args) {
77
98
  color.error('URL parsing error. Please, provide a valid server URL.');
78
99
  return false;
79
100
  }
80
- config.servers[args.alias] = {
101
+ const server = {
81
102
  url: args.server,
82
103
  key: args.key
83
104
  };
105
+ if (args.registry != null) {
106
+ const registry = typeof args.registry === 'string' ? args.registry : defaultRegistry(args.server);
107
+ if (registry) server.registry = registry;
108
+ }
109
+ config.servers[args.alias] = server;
84
110
  color.success(`Successfully added the server to ${confPath}.`);
85
111
  console.log(`Use this command to deploy packages: grok publish ${args.alias}`);
86
112
  if (args.default) config.default = args.alias;
@@ -110,8 +136,18 @@ function config(args) {
110
136
  question.default = config['servers'][server]['key'];
111
137
  const devKey = await _inquirer.default.prompt(question);
112
138
  config['servers'][server]['key'] = devKey[server];
139
+ if (askRegistry) {
140
+ const regDefault = config['servers'][server]['registry'] || defaultRegistry(url);
141
+ const reg = (await _inquirer.default.prompt({
142
+ name: 'registry',
143
+ type: 'input',
144
+ message: `Docker registry for ${server}:`,
145
+ default: regDefault || undefined
146
+ }))['registry'] || '';
147
+ if (reg) config['servers'][server]['registry'] = reg;else delete config['servers'][server]['registry'];
148
+ }
113
149
  }
114
- await addNewServer(config);
150
+ await addNewServer(config, askRegistry);
115
151
  const defaultServer = await _inquirer.default.prompt({
116
152
  name: 'default-server',
117
153
  type: 'input',
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.dockerGen = dockerGen;
7
+ var _pythonCeleryGen = require("../utils/python-celery-gen");
8
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
9
+ 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); }
10
+ async function dockerGen(args) {
11
+ color.setVerbose(args.verbose || args.v || false);
12
+ const packageDir = process.cwd();
13
+ const generated = (0, _pythonCeleryGen.generateCeleryArtifacts)(packageDir);
14
+ if (generated) color.success('Generated Celery Docker artifacts');else console.log('No python/ directory with annotated functions found');
15
+ return true;
16
+ }
@@ -10,19 +10,20 @@ Usage: grok <command>
10
10
  Datagrok's package management tool
11
11
 
12
12
  Commands:
13
- add Add an object template
14
- api Create wrapper functions
15
- build Build a package or multiple packages
16
- check Check package content (function signatures, etc.)
17
- claude Launch Claude Code in a Datagrok dev container
18
- config Create and manage config files
19
- create Create a package
20
- init Modify a package template
21
- link Link \`datagrok-api\` and libraries for local development
22
- publish Upload a package
23
- test Run package tests
24
- testall Run packages tests
25
- migrate Migrate legacy tags to meta.role
13
+ add Add an object template
14
+ api Create wrapper functions
15
+ build Build a package or multiple packages
16
+ check Check package content (function signatures, etc.)
17
+ claude Launch Claude Code in a Datagrok dev container
18
+ config Create and manage config files
19
+ create Create a package
20
+ docker-gen Generate Celery Docker artifacts from Python functions
21
+ init Modify a package template
22
+ link Link \`datagrok-api\` and libraries for local development
23
+ publish Upload a package
24
+ test Run package tests
25
+ testall Run packages tests
26
+ migrate Migrate legacy tags to meta.role
26
27
 
27
28
  To get help on a particular command, use:
28
29
  grok <command> --help
@@ -122,13 +123,14 @@ Usage: grok config
122
123
  Create or update a configuration file
123
124
 
124
125
  Options:
125
- [--reset] [--server] [--alias] [-k | --key]
126
+ [--reset] [--server] [--alias] [-k | --key] [--registry]
126
127
 
127
128
  --reset Restore the default config file template
128
129
  --server Use to add a server to the config (\`grok config add --alias alias --server url --key key\`)
129
130
  --alias Use in conjunction with the \`server\` option to set the server name
130
131
  --key Use in conjunction with the \`server\` option to set the developer key
131
132
  --default Use in conjunction with the \`server\` option to set the added server as default
133
+ --registry Docker registry URL (default: registry.{server hostname})
132
134
  `;
133
135
  const HELP_CREATE = `
134
136
  Usage: grok create [name]
@@ -267,6 +269,17 @@ Options:
267
269
  --verbose Prints detailed information about linked packages
268
270
  --all Links all available packages(run in packages directory)
269
271
  `;
272
+ const HELP_DOCKER_GEN = `
273
+ Usage: grok docker-gen
274
+
275
+ Generate Celery Docker artifacts from annotated Python functions in the python/ directory.
276
+ Produces Dockerfile, tasks.yaml, and Celery entry point in dockerfiles/<name>/.
277
+
278
+ Options:
279
+ [-v | --verbose]
280
+
281
+ --verbose Print detailed output
282
+ `;
270
283
  const HELP_MIGRATE = `
271
284
  Usage: grok migrate
272
285
 
@@ -315,6 +328,7 @@ const help = exports.help = {
315
328
  claude: HELP_CLAUDE,
316
329
  config: HELP_CONFIG,
317
330
  create: HELP_CREATE,
331
+ 'docker-gen': HELP_DOCKER_GEN,
318
332
  init: HELP_INIT,
319
333
  link: HELP_LINK,
320
334
  publish: HELP_PUBLISH,
@@ -17,6 +17,7 @@ var _testUtils = require("../utils/test-utils");
17
17
  var utils = _interopRequireWildcard(require("../utils/utils"));
18
18
  var color = _interopRequireWildcard(require("../utils/color-utils"));
19
19
  var _check = require("./check");
20
+ var _pythonCeleryGen = require("../utils/python-celery-gen");
20
21
  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); }
21
22
  // @ts-ignore
22
23
 
@@ -37,7 +38,7 @@ const packDir = _path.default.join(curDir, 'package.json');
37
38
  const packageFiles = ['src/package.ts', 'src/detectors.ts', 'src/package.js', 'src/detectors.js', 'src/package-test.ts', 'src/package-test.js', 'package.js', 'detectors.js'];
38
39
  let config;
39
40
  const BLEEDING_EDGE_TAG = 'bleeding-edge';
40
- function discoverDockerfiles(packageName, version) {
41
+ function discoverDockerfiles(packageName, version, debug) {
41
42
  const dockerfilesDir = _path.default.join(curDir, 'dockerfiles');
42
43
  if (!_fs.default.existsSync(dockerfilesDir)) return [];
43
44
  const results = [];
@@ -48,13 +49,14 @@ function discoverDockerfiles(packageName, version) {
48
49
  if (!entry.isDirectory()) continue;
49
50
  const dockerfilePath = _path.default.join(dockerfilesDir, entry.name, 'Dockerfile');
50
51
  if (!_fs.default.existsSync(dockerfilePath)) continue;
51
- const cleanName = utils.removeScope(packageName).toLowerCase();
52
+ const cleanName = utils.removeScope(packageName).replace(/-/g, '').toLowerCase();
52
53
  const imageName = `${cleanName}-${entry.name.toLowerCase()}`;
54
+ const imageTag = debug ? version : `${version}.X`;
53
55
  results.push({
54
56
  imageName,
55
- imageTag: `${version}.X`,
57
+ imageTag,
56
58
  dirName: entry.name,
57
- fullLocalName: `${imageName}:${version}.X`
59
+ fullLocalName: `${imageName}:${imageTag}`
58
60
  });
59
61
  }
60
62
  return results;
@@ -105,7 +107,7 @@ function dockerBuild(imageName, dockerfilePath, context) {
105
107
  color.log(`Building Docker image ${imageName}...`);
106
108
  execSync(`docker build -t ${imageName} -f ${dockerfilePath} ${context}`, {
107
109
  encoding: 'utf-8',
108
- stdio: ['pipe', 'pipe', 'pipe']
110
+ stdio: 'inherit'
109
111
  });
110
112
  return true;
111
113
  } catch (e) {
@@ -113,29 +115,69 @@ function dockerBuild(imageName, dockerfilePath, context) {
113
115
  return false;
114
116
  }
115
117
  }
116
- async function resolveLatestCompatible(host, devKey, dockerName) {
118
+ async function getUserLogin(host, devKey) {
119
+ let loginResp;
117
120
  try {
118
- // Login with dev key to get a session token
119
- const loginResp = await (0, _nodeFetch.default)(`${host}/users/login/dev/${devKey}`, {
121
+ loginResp = await (0, _nodeFetch.default)(`${host}/users/login/dev/${devKey}`, {
120
122
  method: 'POST'
121
123
  });
122
- if (loginResp.status !== 200) return null;
123
- const loginData = await loginResp.json();
124
- const token = loginData.token;
125
- const url = `${host}/docker/images/${encodeURIComponent(dockerName)}/latest-compatible`;
126
- const resp = await (0, _nodeFetch.default)(url, {
124
+ } catch (e) {
125
+ color.warn(`Cannot reach server ${host}: ${e.message || e}`);
126
+ return null;
127
+ }
128
+ if (loginResp.status !== 200) return null;
129
+ const loginData = await loginResp.json();
130
+ const token = loginData.token;
131
+ try {
132
+ const userResp = await (0, _nodeFetch.default)(`${host}/users/current`, {
127
133
  headers: {
128
134
  'Authorization': token
129
135
  }
130
136
  });
131
- if (resp.status === 200) return await resp.json();
132
- return null;
133
- } catch {
137
+ if (userResp.status !== 200) return null;
138
+ const userData = await userResp.json();
139
+ return {
140
+ login: userData.login,
141
+ token
142
+ };
143
+ } catch (e) {
134
144
  return null;
135
145
  }
136
146
  }
137
- async function processDockerImages(packageName, version, registry, devKey, host, rebuildDocker, zip, localTimestamps) {
138
- const dockerImages = discoverDockerfiles(packageName, version);
147
+ async function resolveLatestCompatible(host, devKey, dockerName) {
148
+ const userInfo = await getUserLogin(host, devKey);
149
+ if (!userInfo) return {
150
+ found: null,
151
+ serverError: `Authentication failed. Check your developer key.`
152
+ };
153
+ try {
154
+ const url = `${host}/docker/images/${encodeURIComponent(dockerName)}/latest-compatible`;
155
+ const resp = await (0, _nodeFetch.default)(url, {
156
+ headers: {
157
+ 'Authorization': userInfo.token
158
+ }
159
+ });
160
+ if (resp.status === 200) return {
161
+ found: await resp.json(),
162
+ serverError: null
163
+ };
164
+ if (resp.status === 404) return {
165
+ found: null,
166
+ serverError: null
167
+ };
168
+ return {
169
+ found: null,
170
+ serverError: `Unexpected response (HTTP ${resp.status}) from latest-compatible endpoint`
171
+ };
172
+ } catch (e) {
173
+ return {
174
+ found: null,
175
+ serverError: `Failed to query latest-compatible: ${e.message || e}`
176
+ };
177
+ }
178
+ }
179
+ async function processDockerImages(packageName, version, registry, devKey, host, rebuildDocker, zip, localTimestamps, debug) {
180
+ const dockerImages = discoverDockerfiles(packageName, version, debug);
139
181
  if (dockerImages.length === 0) return;
140
182
  color.log(`Found ${dockerImages.length} Dockerfile(s)`);
141
183
  if (registry) dockerLogin(registry, devKey);
@@ -145,18 +187,45 @@ async function processDockerImages(packageName, version, registry, devKey, host,
145
187
  const dockerfileDir = _path.default.join('dockerfiles', img.dirName);
146
188
  const dockerfilePath = _path.default.join(dockerfileDir, 'Dockerfile');
147
189
  if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
148
- result = pushImage(img, registry, version);
190
+ result = pushImage(img, registry);
149
191
  color.success(`Built and tagged ${img.fullLocalName}`);
150
192
  } else {
151
193
  result = await fallbackImage(img, host, devKey, registry);
152
194
  }
153
195
  } else if (localImageExists(img.fullLocalName)) {
154
- result = pushImage(img, registry, version);
196
+ result = pushImage(img, registry);
155
197
  color.success(`Found local image ${img.fullLocalName}`);
156
198
  } else {
157
- result = await fallbackImage(img, host, devKey, registry);
158
- color.warn(`Local image not found. Expected: ${img.fullLocalName}` + (result.image ? `. Falling back to ${result.image}` : ''));
199
+ color.warn(`Local image not found. Expected: ${img.fullLocalName}`);
159
200
  color.log(` Build it with: docker build -t ${img.fullLocalName} -f dockerfiles/${img.dirName}/Dockerfile dockerfiles/${img.dirName}`);
201
+ const fallback = await fallbackImage(img, host, devKey, registry);
202
+ if (fallback.serverError) {
203
+ color.error(`Cannot resolve fallback: ${fallback.serverError}`);
204
+ result = {
205
+ image: null,
206
+ fallback: true,
207
+ requestedVersion: img.imageTag
208
+ };
209
+ } else if (fallback.image) {
210
+ result = fallback;
211
+ color.warn(`Falling back to ${fallback.image}`);
212
+ } else {
213
+ // No fallback and no local image — must build
214
+ color.warn(`No fallback available. Building ${img.fullLocalName}...`);
215
+ const dockerfileDir = _path.default.join('dockerfiles', img.dirName);
216
+ const dockerfilePath = _path.default.join(dockerfileDir, 'Dockerfile');
217
+ if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
218
+ result = pushImage(img, registry);
219
+ color.success(`Built and tagged ${img.fullLocalName}`);
220
+ } else {
221
+ result = {
222
+ image: null,
223
+ fallback: true,
224
+ requestedVersion: img.imageTag
225
+ };
226
+ color.error(`Failed to build ${img.fullLocalName}. No container will be available.`);
227
+ }
228
+ }
160
229
  }
161
230
  const imageJsonPath = _path.default.join('dockerfiles', img.dirName, 'image.json');
162
231
  zip.append(JSON.stringify(result, null, 2), {
@@ -166,34 +235,43 @@ async function processDockerImages(packageName, version, registry, devKey, host,
166
235
  color.log(`Added ${imageJsonPath}`);
167
236
  }
168
237
  }
169
- function pushImage(img, registry, version) {
238
+ function pushImage(img, registry) {
239
+ const canonicalImage = `datagrok/${img.fullLocalName}`;
170
240
  if (registry) {
171
- const remoteTag = `${registry}/${img.imageName}:${version}`;
241
+ const remoteTag = `${registry}/${canonicalImage}`;
172
242
  dockerTag(img.fullLocalName, remoteTag);
173
243
  if (dockerPush(remoteTag)) return {
174
- image: remoteTag,
244
+ image: canonicalImage,
175
245
  fallback: false
176
246
  };
177
247
  color.warn(`Push failed, image tagged locally only: ${remoteTag}`);
178
248
  return {
179
- image: remoteTag,
249
+ image: canonicalImage,
180
250
  fallback: false
181
251
  };
182
252
  }
183
253
  color.warn('No registry configured. Image tagged locally only.');
184
254
  return {
185
- image: img.fullLocalName,
255
+ image: canonicalImage,
186
256
  fallback: false
187
257
  };
188
258
  }
189
259
  async function fallbackImage(img, host, devKey, registry) {
190
- const latest = await resolveLatestCompatible(host, devKey, img.imageName);
191
- if (latest) return {
192
- image: latest.image,
260
+ const {
261
+ found,
262
+ serverError
263
+ } = await resolveLatestCompatible(host, devKey, img.imageName);
264
+ if (serverError) return {
265
+ image: null,
266
+ fallback: true,
267
+ requestedVersion: img.imageTag,
268
+ serverError
269
+ };
270
+ if (found) return {
271
+ image: found.image,
193
272
  fallback: true,
194
273
  requestedVersion: img.imageTag
195
274
  };
196
- color.warn('No previous version available. Container will not be available until an image is built.');
197
275
  return {
198
276
  image: null,
199
277
  fallback: true,
@@ -201,21 +279,21 @@ async function fallbackImage(img, host, devKey, registry) {
201
279
  };
202
280
  }
203
281
  async function processPackage(debug, rebuild, host, devKey, packageName, dropDb, suffix, hostAlias, registry, rebuildDocker) {
204
- // Get the server timestamps
282
+ // Validate server connectivity and dev key
205
283
  let timestamps = {};
206
284
  let url = `${host}/packages/dev/${devKey}/${packageName}`;
207
- if (debug) {
208
- try {
209
- timestamps = await (await (0, _nodeFetch.default)(url + '/timestamps')).json();
210
- if (timestamps['#type'] === 'ApiError') {
211
- color.error(timestamps.message);
212
- return 1;
213
- }
214
- } catch (error) {
215
- if (utils.isConnectivityError(error)) color.error(`Server is possibly offline: ${host}`);
216
- if (color.isVerbose()) console.error(error);
285
+ try {
286
+ const checkResp = await (0, _nodeFetch.default)(url + '/timestamps');
287
+ const checkData = await checkResp.json();
288
+ if (checkData['#type'] === 'ApiError') {
289
+ color.error(checkData.message);
217
290
  return 1;
218
291
  }
292
+ if (debug) timestamps = checkData;
293
+ } catch (error) {
294
+ if (utils.isConnectivityError(error)) color.error(`Server is possibly offline: ${host}`);
295
+ if (color.isVerbose()) console.error(error);
296
+ return 1;
219
297
  }
220
298
  const zip = (0, _archiverPromise.default)('zip', {
221
299
  store: false
@@ -313,9 +391,16 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
313
391
  return 1;
314
392
  }
315
393
 
394
+ // Generate Celery Docker artifacts from python/ if present
395
+ (0, _pythonCeleryGen.generateCeleryArtifacts)(curDir);
396
+
316
397
  // Process Docker images and inject image.json into the ZIP
317
- const packageVersion = json.version;
318
- await processDockerImages(packageName, packageVersion, registry, devKey, host, rebuildDocker ?? false, zip, localTimestamps);
398
+ let dockerVersion = json.version;
399
+ if (debug) {
400
+ const userInfo = await getUserLogin(host, devKey);
401
+ if (userInfo) dockerVersion = userInfo.login;
402
+ }
403
+ await processDockerImages(packageName, dockerVersion, registry, devKey, host, rebuildDocker ?? false, zip, localTimestamps, debug);
319
404
  zip.append(JSON.stringify(localTimestamps), {
320
405
  name: 'timestamps.json'
321
406
  });