datagrok-tools 4.14.69 → 4.14.70

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,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.migrate = migrate;
7
+ exports.toCamelCase = void 0;
8
+ var _tsMorph = require("ts-morph");
9
+ var path = _interopRequireWildcard(require("path"));
10
+ var _const = require("datagrok-api/src/const");
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
+ const FUNC_TYPE_VALUES = new Set(Object.values(_const.FUNC_TYPES));
13
+ const toCamelCase = str => str.replace(/[-_ ]+(\w)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, c => c.toLowerCase());
14
+ exports.toCamelCase = toCamelCase;
15
+ function getProp(obj, name) {
16
+ return obj.getProperties().find(p => {
17
+ if (![_tsMorph.SyntaxKind.PropertyAssignment, _tsMorph.SyntaxKind.ShorthandPropertyAssignment].includes(p.getKind())) return false;
18
+ const n = p.getName?.();
19
+ return !!n && n.replace(/['"`]/g, '') === name;
20
+ });
21
+ }
22
+ const parseRoleString = value => value.split(',').map(v => v.trim()).filter(Boolean);
23
+ function migrate(argv) {
24
+ const FILE_PATH = path.resolve(process.cwd(), 'src/package.ts');
25
+ const project = new _tsMorph.Project({
26
+ manipulationSettings: {
27
+ quoteKind: _tsMorph.QuoteKind.Single,
28
+ indentationText: _tsMorph.IndentationText.TwoSpaces,
29
+ useTrailingCommas: true
30
+ }
31
+ });
32
+ const source = project.addSourceFileAtPath(FILE_PATH);
33
+ source.getDescendantsOfKind(_tsMorph.SyntaxKind.Decorator).forEach(decorator => {
34
+ const call = decorator.getCallExpression();
35
+ if (!call || !call.getExpression().getText().includes('grok.decorators.')) return;
36
+ const arg = call.getArguments()[0]?.asKind(_tsMorph.SyntaxKind.ObjectLiteralExpression);
37
+ if (!arg) return;
38
+ const tagsProp = getProp(arg, 'tags');
39
+ if (!tagsProp) return;
40
+ const tagsArray = tagsProp.getFirstDescendantByKind(_tsMorph.SyntaxKind.ArrayLiteralExpression);
41
+ if (!tagsArray) return;
42
+ const validTags = [];
43
+ const remainingTags = [];
44
+ tagsArray.getElements().forEach(el => {
45
+ const tag = el.getText().replace(/['"`]/g, '');
46
+ FUNC_TYPE_VALUES.has(tag) ? validTags.push(toCamelCase(tag)) : remainingTags.push(tag);
47
+ });
48
+ if (remainingTags.length === 0) tagsProp.remove();else {
49
+ tagsProp.asKindOrThrow(_tsMorph.SyntaxKind.PropertyAssignment).setInitializer(`[${remainingTags.map(t => `'${t}'`).join(', ')}]`);
50
+ }
51
+ if (validTags.length === 0) return;
52
+ const metaProp = getProp(arg, 'meta');
53
+ if (!metaProp) {
54
+ arg.addPropertyAssignment({
55
+ name: 'meta',
56
+ initializer: `{role: '${validTags.join(',')}'}`
57
+ });
58
+ return;
59
+ }
60
+ const metaObj = metaProp.getFirstChildByKind(_tsMorph.SyntaxKind.ObjectLiteralExpression);
61
+ if (!metaObj) return;
62
+ const roleProp = getProp(metaObj, 'role');
63
+ const existingRoles = roleProp ? parseRoleString(roleProp.asKindOrThrow(_tsMorph.SyntaxKind.PropertyAssignment).getInitializer()?.getText().replace(/['"`]/g, '') ?? '') : [];
64
+ const mergedRoles = Array.from(new Set([...existingRoles, ...validTags]));
65
+ const otherProps = metaObj.getProperties().filter(p => p.asKind(_tsMorph.SyntaxKind.PropertyAssignment)?.getName() !== 'role').map(p => p.getText()).join(', ');
66
+ metaProp.asKindOrThrow(_tsMorph.SyntaxKind.PropertyAssignment).setInitializer(writer => {
67
+ writer.write('{');
68
+ if (otherProps) writer.write(otherProps + ', ');
69
+ writer.write(`role: '${mergedRoles.join(',')}'`);
70
+ writer.write('}');
71
+ });
72
+ });
73
+ source.saveSync();
74
+ }
package/bin/grok.js CHANGED
@@ -16,7 +16,8 @@ const commands = {
16
16
  publish: require('./commands/publish').publish,
17
17
  test: require('./commands/test').test,
18
18
  testall: require('./commands/test-all').testAll,
19
- stresstest: require('./commands/stress-tests').stressTests
19
+ stresstest: require('./commands/stress-tests').stressTests,
20
+ migrate: require('./commands/migrate').migrate,
20
21
  };
21
22
 
22
23
  const onPackageCommandNames = ['api', 'check', 'link', 'publish', 'test'];
@@ -136,7 +136,7 @@ function getFuncAnnotation(data, comment = '//', sep = '\n') {
136
136
  }
137
137
  for (const input of data.inputs ?? []) {
138
138
  if (!input) continue;
139
- let type = input?.type?.replaceAll(" ", "");
139
+ let type = input?.type?.replaceAll(' ', '');
140
140
  if (type?.includes(`|undefined`)) {
141
141
  type = type.replaceAll(`|undefined`, '');
142
142
  // modify original payload
@@ -216,7 +216,7 @@ function buildStringOfOptions(input) {
216
216
  const reservedDecorators = exports.reservedDecorators = {
217
217
  viewer: {
218
218
  metadata: {
219
- tags: [FUNC_TYPES.VIEWER],
219
+ role: FUNC_TYPES.VIEWER,
220
220
  inputs: [],
221
221
  outputs: [{
222
222
  name: 'result',
@@ -227,7 +227,7 @@ const reservedDecorators = exports.reservedDecorators = {
227
227
  },
228
228
  filter: {
229
229
  metadata: {
230
- tags: [FUNC_TYPES.FILTER],
230
+ role: FUNC_TYPES.FILTER,
231
231
  inputs: [],
232
232
  outputs: [{
233
233
  name: 'result',
@@ -238,7 +238,7 @@ const reservedDecorators = exports.reservedDecorators = {
238
238
  },
239
239
  cellRenderer: {
240
240
  metadata: {
241
- tags: [FUNC_TYPES.CELL_RENDERER],
241
+ role: FUNC_TYPES.CELL_RENDERER,
242
242
  inputs: [],
243
243
  outputs: [{
244
244
  name: 'renderer',
@@ -264,7 +264,7 @@ const reservedDecorators = exports.reservedDecorators = {
264
264
  },
265
265
  fileExporter: {
266
266
  metadata: {
267
- tags: [FUNC_TYPES.FILE_EXPORTER],
267
+ role: FUNC_TYPES.FILE_EXPORTER,
268
268
  inputs: [],
269
269
  outputs: []
270
270
  },
@@ -272,7 +272,7 @@ const reservedDecorators = exports.reservedDecorators = {
272
272
  },
273
273
  fileHandler: {
274
274
  metadata: {
275
- tags: [FUNC_TYPES.FILE_HANDLER],
275
+ role: FUNC_TYPES.FILE_HANDLER,
276
276
  inputs: [{
277
277
  name: 'content',
278
278
  type: 'string'
@@ -286,7 +286,7 @@ const reservedDecorators = exports.reservedDecorators = {
286
286
  },
287
287
  fileViewer: {
288
288
  metadata: {
289
- tags: [FUNC_TYPES.FILE_VIEWER],
289
+ role: FUNC_TYPES.FILE_VIEWER,
290
290
  inputs: [{
291
291
  name: 'f',
292
292
  type: 'file'
@@ -300,7 +300,7 @@ const reservedDecorators = exports.reservedDecorators = {
300
300
  },
301
301
  settingsEditor: {
302
302
  metadata: {
303
- tags: [FUNC_TYPES.SETTINGS_EDITOR],
303
+ role: FUNC_TYPES.SETTINGS_EDITOR,
304
304
  inputs: [],
305
305
  outputs: [{
306
306
  name: 'result',
@@ -322,7 +322,7 @@ const reservedDecorators = exports.reservedDecorators = {
322
322
  },
323
323
  app: {
324
324
  metadata: {
325
- tags: [FUNC_TYPES.APP],
325
+ role: FUNC_TYPES.APP,
326
326
  inputs: [],
327
327
  outputs: [{
328
328
  name: 'result',
@@ -333,7 +333,7 @@ const reservedDecorators = exports.reservedDecorators = {
333
333
  },
334
334
  autostart: {
335
335
  metadata: {
336
- tags: [FUNC_TYPES.AUTOSTART],
336
+ role: FUNC_TYPES.AUTOSTART,
337
337
  inputs: [],
338
338
  outputs: []
339
339
  },
@@ -341,7 +341,7 @@ const reservedDecorators = exports.reservedDecorators = {
341
341
  },
342
342
  init: {
343
343
  metadata: {
344
- tags: [FUNC_TYPES.INIT],
344
+ role: FUNC_TYPES.INIT,
345
345
  inputs: [],
346
346
  outputs: []
347
347
  },
@@ -349,7 +349,7 @@ const reservedDecorators = exports.reservedDecorators = {
349
349
  },
350
350
  editor: {
351
351
  metadata: {
352
- tags: [FUNC_TYPES.EDITOR],
352
+ role: FUNC_TYPES.EDITOR,
353
353
  inputs: [{
354
354
  name: 'call',
355
355
  type: 'funccall'
@@ -360,7 +360,7 @@ const reservedDecorators = exports.reservedDecorators = {
360
360
  },
361
361
  panel: {
362
362
  metadata: {
363
- tags: [FUNC_TYPES.PANEL],
363
+ role: FUNC_TYPES.PANEL,
364
364
  inputs: [],
365
365
  outputs: [{
366
366
  name: 'result',
@@ -371,7 +371,7 @@ const reservedDecorators = exports.reservedDecorators = {
371
371
  },
372
372
  folderViewer: {
373
373
  metadata: {
374
- tags: [FUNC_TYPES.FOLDER_VIEWER],
374
+ role: FUNC_TYPES.FOLDER_VIEWER,
375
375
  inputs: [{
376
376
  name: 'folder',
377
377
  type: 'file'
@@ -388,7 +388,7 @@ const reservedDecorators = exports.reservedDecorators = {
388
388
  },
389
389
  semTypeDetector: {
390
390
  metadata: {
391
- tags: [FUNC_TYPES.SEM_TYPE_DETECTOR],
391
+ role: FUNC_TYPES.SEM_TYPE_DETECTOR,
392
392
  inputs: [{
393
393
  name: 'col',
394
394
  type: 'column'
@@ -402,7 +402,7 @@ const reservedDecorators = exports.reservedDecorators = {
402
402
  },
403
403
  dashboard: {
404
404
  metadata: {
405
- tags: [FUNC_TYPES.DASHBOARD],
405
+ role: FUNC_TYPES.DASHBOARD,
406
406
  inputs: [],
407
407
  outputs: [{
408
408
  name: 'result',
@@ -413,7 +413,7 @@ const reservedDecorators = exports.reservedDecorators = {
413
413
  },
414
414
  functionAnalysis: {
415
415
  metadata: {
416
- tags: [FUNC_TYPES.FUNCTION_ANALYSIS],
416
+ role: FUNC_TYPES.FUNCTION_ANALYSIS,
417
417
  inputs: [],
418
418
  outputs: [{
419
419
  name: 'result',
@@ -424,7 +424,7 @@ const reservedDecorators = exports.reservedDecorators = {
424
424
  },
425
425
  converter: {
426
426
  metadata: {
427
- tags: [FUNC_TYPES.CONVERTER],
427
+ role: FUNC_TYPES.CONVERTER,
428
428
  inputs: [{
429
429
  name: 'value',
430
430
  type: 'dynamic'
@@ -460,7 +460,7 @@ const reservedDecorators = exports.reservedDecorators = {
460
460
  },
461
461
  model: {
462
462
  metadata: {
463
- tags: [FUNC_TYPES.MODEL],
463
+ role: FUNC_TYPES.MODEL,
464
464
  inputs: [],
465
465
  outputs: []
466
466
  },
@@ -187,10 +187,10 @@ const dgToTsTypeMap = exports.dgToTsTypeMap = {
187
187
  const propertyTypes = exports.propertyTypes = ['bool', 'int', 'double', 'string', 'datetime', 'object', 'column', 'dataframe', 'bitset', 'cell', 'string_list', 'map'];
188
188
  const headerTags = exports.headerTags = ['name', 'description', 'help-url', 'input', 'output', 'tags', 'sample', 'language', 'returns', 'test', 'sidebar', 'condition', 'top-menu', 'environment', 'require', 'editor-for', 'schedule', 'reference', 'editor', 'meta', 'connection', 'friendlyName'];
189
189
  const fileParamRegex = exports.fileParamRegex = {
190
- py: new RegExp(`^\#\\s*([^:]*): *([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?[^\\n]*$`),
191
- ts: new RegExp(`^\/\/\\s*([^:]*): *([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?[^\\n]*$`),
192
- js: new RegExp(`^\/\/\\s*([^:]*): *([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?[^\\n]*$`),
193
- sql: new RegExp(`^--\\s*([^:]*): *([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?[^\\n]*$`)
190
+ py: new RegExp(`^#\\s*(?!\\s*#)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
191
+ ts: new RegExp(`^//\\s*(?!\\s*//)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
192
+ js: new RegExp(`^//\\s*(?!\\s*//)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
193
+ sql: new RegExp(`^--\\s*([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`)
194
194
  };
195
195
  const nameAnnRegex = exports.nameAnnRegex = /\s*(name[^:]*): ([^\n\r\[\{]+)/;
196
196
  const nameRegex = exports.nameRegex = /(?:|(?:static)(?:export )(?:async ))\s+function\s+([a-zA-Z_][a-zA-Z0-9_$]*)\s*\((.*?).*/;
@@ -202,7 +202,7 @@ function getScriptOutputType(script, comment = '#') {
202
202
  let resType = 'void';
203
203
  let firstItemName = '';
204
204
  let wasSecond = false;
205
- for (let match of matches) {
205
+ for (const match of matches) {
206
206
  if (resType === 'void') {
207
207
  resType = dgToTsTypeMap[match[1]] ?? 'any';
208
208
  firstItemName = match[2];
@@ -268,12 +268,8 @@ async function runScript(script, path, verbose = false) {
268
268
  } = await execAsync(script, {
269
269
  cwd: path
270
270
  });
271
- if (stderr && verbose) {
272
- console.error(`Warning/Error: ${stderr}`);
273
- }
274
- if (stdout && verbose) {
275
- console.log(`Output: ${stdout}`);
276
- }
271
+ if (stderr && verbose) console.error(`Warning/Error: ${stderr}`);
272
+ if (stdout && verbose) console.log(`Output: ${stdout}`);
277
273
  } catch (error) {
278
274
  console.error(`Execution failed: ${error.message}`);
279
275
  throw new Error(`Error executing '${script}'. Error message: ${error.message}`);
@@ -316,12 +312,8 @@ function optionsToString(options) {
316
312
  for (const [key, value] of Object.entries(options)) {
317
313
  if (key === '_' || key === 'all') continue;
318
314
  if (typeof value === 'boolean') {
319
- if (value) {
320
- parts.push(`--${key}`);
321
- }
322
- } else if (value !== undefined && value !== null) {
323
- parts.push(`--${key}="${value}"`);
324
- }
315
+ if (value) parts.push(`--${key}`);
316
+ } else if (value !== undefined && value !== null) parts.push(`--${key}="${value}"`);
325
317
  }
326
318
  return parts.join(' ');
327
319
  }
@@ -1,6 +1,6 @@
1
1
 
2
2
  //name: #{NAME}
3
- //tags: app
3
+ //meta.role: app
4
4
  export function #{NAME}() {
5
5
  grok.shell.info('Hello!');
6
6
  }
@@ -1,4 +1,3 @@
1
-
2
- //tags: init
1
+ //meta.role: init
3
2
  export async function #{NAME}() {
4
3
  }
@@ -1,7 +1,7 @@
1
1
 
2
2
  //name: #{NAME}
3
3
  //description: Creates an info panel
4
- //tags: panel
4
+ //meta.role: panel
5
5
  //input: string smiles {semType: Molecule}
6
6
  //output: widget result
7
7
  //condition: true
@@ -1,5 +1,5 @@
1
1
 
2
- //tags: semTypeDetector
2
+ //meta.role: semTypeDetector
3
3
  //input: column col
4
4
  //output: string semType
5
5
  detect#{PACKAGE_DETECTORS_NAME}(col) {
@@ -1,4 +1,4 @@
1
- import { category, expect, test } from '@datagrok-libraries/utils/src/test';
1
+ import {category, expect, test} from '@datagrok-libraries/utils/src/test';
2
2
 
3
3
 
4
4
  category('Examples', () => {
@@ -11,6 +11,6 @@ category('Examples', () => {
11
11
  });
12
12
 
13
13
  test('Skipped', async () => {
14
- expect(1 === 1 , false);
14
+ expect(1 === 1, false);
15
15
  }, {skipReason: 'TASK-ID'});
16
16
  });
@@ -1,7 +1,7 @@
1
1
 
2
2
  //name: #{NAME}
3
3
  //description: Creates #{NAME} view
4
- //tags: view
4
+ //meta.role: view
5
5
  //input: map params
6
6
  //input: string path
7
7
  //output: view result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "4.14.69",
3
+ "version": "4.14.70",
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": {
@@ -11,6 +11,7 @@
11
11
  "@typescript-eslint/visitor-keys": "^8.31.1",
12
12
  "archiver": "^4.0.2",
13
13
  "archiver-promise": "^1.0.0",
14
+ "datagrok-api": "../js-api",
14
15
  "estraverse": "^5.3.0",
15
16
  "fs": "^0.0.1-security",
16
17
  "glob": "^11.0.2",
@@ -24,8 +25,7 @@
24
25
  "papaparse": "^5.4.1",
25
26
  "path": "^0.12.7",
26
27
  "puppeteer": "22.10.0",
27
- "puppeteer-screen-recorder": "3.0.3",
28
- "datagrok-api": "^1.26.0"
28
+ "puppeteer-screen-recorder": "3.0.3"
29
29
  },
30
30
  "scripts": {
31
31
  "link": "npm link",
@@ -75,6 +75,7 @@
75
75
  "@typescript-eslint/parser": "^5.62.0",
76
76
  "eslint": "^8.56.0",
77
77
  "eslint-config-google": "^0.14.0",
78
+ "ts-morph": "^27.0.2",
78
79
  "typescript": "^5.3.3",
79
80
  "webpack": "^5.89.0",
80
81
  "webpack-cli": "^5.1.4"
@@ -15,6 +15,8 @@ const {
15
15
  inputOptionsNames,
16
16
  } = require('../bin/utils/func-generation');
17
17
 
18
+ const {toCamelCase} = require('../bin/commands/migrate');
19
+
18
20
  const {api} = require('../bin/commands/api');
19
21
 
20
22
  const baseImport = 'import * as DG from \'datagrok-api/dg\';\n';
@@ -132,13 +134,17 @@ class FuncGeneratorPlugin {
132
134
  ...(reservedDecorators[name]['metadata']['tags'] ?? []),
133
135
  ...(decoratorOptions.get('tags') ?? []),
134
136
  ]);
135
-
136
- if ((reservedDecorators[name]['metadata']['role']?.length > 0) ) {
137
- if (!decoratorOptions.get('meta'))
138
- decoratorOptions.set('meta', {role: reservedDecorators[name]['metadata']['role']});
139
- else if (!decoratorOptions.get('meta')['role'])
140
- decoratorOptions.get('meta')['role'] = reservedDecorators[name]['metadata']['role'];
141
- delete reservedDecorators[name]['metadata']['role'];
137
+
138
+ const role = reservedDecorators[name]['metadata']['role'];
139
+ if (role?.length > 0) {
140
+ const camelRole = toCamelCase(role);
141
+
142
+ if (!decoratorOptions.has('meta'))
143
+ decoratorOptions.set('meta', {role: camelRole});
144
+ else {
145
+ const meta = decoratorOptions.get('meta');
146
+ meta['role'] = meta['role'] ? `${meta['role']},${camelRole}` : camelRole;
147
+ }
142
148
  }
143
149
 
144
150
  const functionParams =
@@ -150,6 +156,8 @@ class FuncGeneratorPlugin {
150
156
  node?.type === 'MethodDefinition' ? className : identifierName,
151
157
  modifyImportPath(path.dirname(this.options.outputPath), file),
152
158
  );
159
+ const metadataCopy = {...reservedDecorators[name]['metadata']};
160
+ delete metadataCopy.role;
153
161
 
154
162
  imports.add(importString);
155
163
  const funcName = `${
@@ -157,7 +165,7 @@ class FuncGeneratorPlugin {
157
165
  }${identifierName}`;
158
166
  const funcAnnotaionOptions = {
159
167
  ...{name: funcName},
160
- ...reservedDecorators[name]['metadata'],
168
+ ...metadataCopy,
161
169
  ...(annotationByReturnObj ?
162
170
  {outputs: annotationByReturnObj ?? []} :
163
171
  {}),
@@ -1,6 +1,5 @@
1
1
  //name: #{NAME}
2
2
  //description: Hello world script
3
3
  //language: javascript
4
- //tags: template, demo
5
4
 
6
5
  alert('Hello, World!');
@@ -1,7 +1,6 @@
1
1
  #name: #{NAME}
2
2
  #description: Following template calculates number of cells in table
3
3
  #language: julia
4
- #tags: template, demo
5
4
  #sample: cars.csv
6
5
  #input: dataframe table [Data table]
7
6
  #output: int count [Number of cells in table]
@@ -1,7 +1,6 @@
1
1
  //name: #{NAME}
2
2
  //description: Following template calculates number of cells in table
3
3
  //language: nodejs
4
- //tags: template, demo
5
4
  //sample: cars.csv
6
5
  //input: dataframe table [Data table]
7
6
  //output: int count [Number of cells in table]
@@ -1,7 +1,6 @@
1
1
  #name: #{NAME}
2
2
  #description: Following template calculates number of cells in table
3
3
  #language: octave
4
- #tags: template, demo
5
4
  #sample: cars.csv
6
5
  #input: dataframe table [Data table]
7
6
  #output: int count [Number of cells in table]
@@ -1,7 +1,6 @@
1
1
  #name: #{NAME}
2
2
  #description: Following template calculates number of cells in table
3
3
  #language: python
4
- #tags: template, demo
5
4
  #sample: cars.csv
6
5
  #input: dataframe table [Data table]
7
6
  #output: int count [Number of cells in table]
@@ -1,7 +1,6 @@
1
1
  #name: #{NAME}
2
2
  #description: Following template calculates number of cells in table
3
3
  #language: r
4
- #tags: template, demo
5
4
  #sample: cars.csv
6
5
  #input: dataframe table [Data table]
7
6
  #output: int count [Number of cells in table]