wikibase-cli 18.1.0 → 18.2.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/wb-badges.js CHANGED
@@ -5,6 +5,8 @@ import { exitOnMissingInstance } from '#lib/exit_on_missing'
5
5
  import program from '#lib/program'
6
6
  import { get } from '#lib/request'
7
7
 
8
+ program.canHaveZeroArguments = true
9
+
8
10
  await program.process('badges')
9
11
 
10
12
  const { instance } = program
package/bin/wb-config.js CHANGED
@@ -5,6 +5,8 @@ import { configurateCredentials } from '#lib/config/credentials_config'
5
5
  import fileOps from '#lib/config/file_operations'
6
6
  import program from '#lib/program'
7
7
 
8
+ program.canHaveZeroArguments = true
9
+
8
10
  await program.process('config')
9
11
 
10
12
  const { args, json } = program
package/bin/wb-convert.js CHANGED
@@ -5,6 +5,8 @@ import errors_ from '#lib/errors'
5
5
  import { getStdinInput } from '#lib/get_stdin_input'
6
6
  import program from '#lib/program'
7
7
 
8
+ program.canHaveZeroArguments = true
9
+
8
10
  await program
9
11
  .option('-s, --subjects [subjects...]', 'set a subject')
10
12
  .option('-p, --property <property>', 'set a property')
package/bin/wb-data.js CHANGED
@@ -4,6 +4,8 @@ import { parseGuid } from '#lib/parse_command_utils'
4
4
  import program from '#lib/program'
5
5
  import { tolerantIdParserFactory } from '#lib/tolerant_id_parser'
6
6
 
7
+ program.acceptsArgsOnStdin = true
8
+
7
9
  await program
8
10
  .option('-s, --simplify', 'get simplified entities data')
9
11
  .option('-k, --keep <props>', 'data to keep when simplifying claims (ids,richvalues,types,references,qualifiers,hashes,nontruthy,nondeprecated,ranks)')
@@ -9,10 +9,12 @@ import { readIdsFromStdin } from '#lib/read_ids_from_stdin'
9
9
  import { tolerantIdParserFactory } from '#lib/tolerant_id_parser'
10
10
  import { getWbk } from '#lib/wbk'
11
11
 
12
+ program.acceptsArgsOnStdin = true
13
+
12
14
  await program
13
15
  .option('-p, --props <props>', 'request only certain properties (info, sitelinks, aliases, labels, descriptions, claims, datatype)')
14
16
  .option('-r, --revision <id>', 'request a specific revision')
15
- .option('-f, --format <format>', 'Default: js when fetching a single entity, json otherwise. Note that the -j, --json option is equivalent to "--format json"')
17
+ .option('-f, --format <format>', 'Options: js, json, mjs. Default: js when fetching a single entity, json otherwise. Note that the -j, --json option is equivalent to "--format json"')
16
18
  .option('-m, --create-mode', 'optimize for creating an entity from a previously existing one, namely dropping ids from the existing entity used as template')
17
19
  .option('-z, --no-minimize', 'disable claims minimization, making the output format more predictable; i.e. single claims will still be in arrays')
18
20
  .process('generate-template')
@@ -47,7 +49,7 @@ const handleIds = async ids => {
47
49
 
48
50
  const batchMode = ids.length > 1
49
51
 
50
- if (batchMode && format === 'js') {
52
+ if (batchMode && format.endsWith('js')) {
51
53
  throw new Error("js format can't be used when several entities are requested")
52
54
  }
53
55
 
package/bin/wb-props.js CHANGED
@@ -7,6 +7,8 @@ import getPatternFilter from '#lib/get_pattern_filter'
7
7
  import program from '#lib/program'
8
8
  import resetProperties from '#lib/reset_properties'
9
9
 
10
+ program.canHaveZeroArguments = true
11
+
10
12
  await program
11
13
  .option('-d, --details', 'include properties labels, types, descriptions, and aliases')
12
14
  .option('-t, --type [type]', 'include properties types, or if a type is specified, keep only properties with that type')
package/bin/wb-query.js CHANGED
@@ -7,6 +7,9 @@ import { outputFactory } from '#lib/output'
7
7
  import program from '#lib/program'
8
8
  import { sparqlQueryCommand } from '#lib/sparql_query_command'
9
9
 
10
+ // All arguments are passed as options values making program.args.length === 0 likely
11
+ program.canHaveZeroArguments = true
12
+
10
13
  await program
11
14
  .option('-s, --subject <subject>', 'set a subject')
12
15
  .option('-p, --property <property>', 'set a property')
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { values } from 'lodash-es'
2
+ import { chunk, values } from 'lodash-es'
3
3
  import { isEntityId } from 'wikibase-sdk'
4
4
  import errors_ from '#lib/errors'
5
5
  import { exitOnMissingInstance } from '#lib/exit_on_missing'
@@ -9,58 +9,82 @@ import { get } from '#lib/request'
9
9
  import { isPositiveIntegerString } from '#lib/types'
10
10
  import { getWbk } from '#lib/wbk'
11
11
 
12
+ program.acceptsArgsOnStdin = true
13
+
12
14
  await program
13
15
  .option('-s, --start <date>', 'start date')
14
16
  .option('-e, --end <date>', 'end date')
15
17
  .option('-n, --limit <num>', 'maximum number of revisions')
18
+ .option('-p, --props <props>', 'requested props, separated by a comma. Available props: https://www.mediawiki.org/wiki/API:Revisions#query+revisions:rvprop')
16
19
  .process('revisions')
17
20
 
18
21
  exitOnMissingInstance(program.instance)
19
22
 
20
23
  const { getRevisions } = getWbk(program)
21
24
 
22
- // Not parsing the ids with ../lib/tolerant_id_parser as that would
23
- // remove prefixes which are required for entities out of the main namespace
24
- // Ex: Property:P570
25
- const ids = program.args
26
- if (!(ids && ids.length > 0)) program.helpAndExit(0)
27
-
28
- ids.forEach(id => {
29
- let [ prefix, entityId ] = id.split(':')
30
- if (entityId) {
31
- if (prefix !== 'Property' && prefix !== 'Item') {
32
- throw new Error(`invalid entity prefix: ${prefix}`)
25
+ async function fetchAndLogRevisions (ids) {
26
+ ids.forEach(id => {
27
+ let [ prefix, entityId ] = id.split(':')
28
+ if (entityId) {
29
+ if (prefix !== 'Property' && prefix !== 'Item') {
30
+ throw new Error(`invalid entity prefix: ${prefix}`)
31
+ }
32
+ } else {
33
+ entityId = prefix
33
34
  }
34
- } else {
35
- entityId = prefix
36
- }
37
- if (!isEntityId(entityId)) throw new Error(`invalid entity id: ${id}`)
38
- })
35
+ if (!isEntityId(entityId)) throw new Error(`invalid entity id: ${id}`)
36
+ })
37
+
38
+ const query = {}
39
+ let { start, end, limit, props, verbose } = program
40
+ if (isPositiveIntegerString(start)) start = parseInt(start)
41
+ if (isPositiveIntegerString(end)) end = parseInt(end)
39
42
 
40
- const query = {}
41
- let { start, end, limit, verbose } = program
42
- if (isPositiveIntegerString(start)) start = parseInt(start)
43
- if (isPositiveIntegerString(end)) end = parseInt(end)
43
+ if (start != null) query.start = start
44
+ if (end != null) query.end = end
45
+ if (limit != null) query.limit = limit
46
+ if (props != null) query.prop = props?.split(/[,|]/)
44
47
 
45
- query.start = start
46
- query.end = end
47
- query.limit = limit
48
+ // Prevent error "titles, pageids or a generator was used to supply multiple pages, but the rvlimit, rvstartid, rvendid, rvdir=newer, rvuser, rvexcludeuser, rvstart, and rvend parameters may only be used on a single page. "
49
+ const usesSinglePageParam = limit != null || start != null || end != null
48
50
 
49
- const getAndLogRevisions = id => {
50
- const url = getRevisions({ ids: [ id ], ...query })
51
- if (verbose) console.log(`revision query: ${id}`, url)
52
- return get(url)
53
- .then(body => values(body.query.pages)[0])
51
+ async function getAndLogRevisions (ids) {
52
+ const url = getRevisions({ ids, ...query })
53
+ if (verbose) console.log(`revision query: ${ids}`, url)
54
+ const body = await get(url)
55
+ return values(body.query.pages)
56
+ }
57
+
58
+ if (usesSinglePageParam) {
59
+ const idsBatches = chunk(ids, 10)
60
+ for (const batch of idsBatches) {
61
+ // Getting revisisions data individually to be able to pass parameters
62
+ // cf https://github.com/maxlath/wikibase-sdk/blob/master/docs/get_revisions.md
63
+ await Promise.all(batch.map(getAndLogRevisions))
64
+ .then(logNdjson)
65
+ .catch(errors_.exit)
66
+ }
67
+ } else {
68
+ const idsBatches = chunk(ids, 50)
69
+ for (const batch of idsBatches) {
70
+ await getAndLogRevisions(batch)
71
+ .then(logNdjson)
72
+ .catch(errors_.exit)
73
+ }
74
+ }
54
75
  }
55
76
 
56
- if (ids.length === 1) {
57
- getAndLogRevisions(ids[0])
58
- .then(data => console.log(JSON.stringify(data)))
59
- .catch(errors_.exit)
77
+ // process.stdin.isTTY will be undefined if the process is receiving
78
+ // its stdin from another process
79
+ if (program.args.length === 0 && process.stdin.isTTY) {
80
+ program.helpAndExit(0)
81
+ } else if (program.args.length > 0) {
82
+ // Not parsing the ids with ../lib/tolerant_id_parser as that would
83
+ // remove prefixes which are required for entities out of the main namespace
84
+ // Ex: Property:P570
85
+ const ids = program.args
86
+ fetchAndLogRevisions(ids)
60
87
  } else {
61
- // Getting revisisions data individually to be able to pass parameters
62
- // cf https://github.com/maxlath/wikibase-sdk/blob/master/docs/get_revisions.md
63
- Promise.all(ids.map(getAndLogRevisions))
64
- .then(logNdjson)
65
- .catch(errors_.exit)
88
+ const { readIdsFromStdin } = await import('#lib/read_ids_from_stdin')
89
+ readIdsFromStdin(fetchAndLogRevisions)
66
90
  }
@@ -106,11 +106,10 @@ function getWbEditConfig () {
106
106
  const { editGroup } = program
107
107
  let { summary, baserevid, maxlag } = program
108
108
 
109
+ let summarySuffix
109
110
  if (editGroup) {
110
111
  const compactEditGroupUrl = getEditGroupUrl(':toollabs:', editGroup)
111
- summary = summary || ''
112
- summary += ` ([[${compactEditGroupUrl}|details]])`
113
- summary = summary.trim()
112
+ summarySuffix = `([[${compactEditGroupUrl}|details]])`
114
113
  }
115
114
 
116
115
  const { instance, credentials } = config
@@ -132,6 +131,7 @@ function getWbEditConfig () {
132
131
  instance,
133
132
  credentials: { oauth, username, password },
134
133
  summary,
134
+ summarySuffix,
135
135
  baserevid,
136
136
  tags,
137
137
  bot: config.bot,
package/lib/log_ndjson.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Log as newline-delimited JSON: one obj JSON per line. No comma.
2
2
 
3
3
  export default array => {
4
- for (const obj of array) {
4
+ for (const obj of array.flat()) {
5
5
  process.stdout.write(JSON.stringify(obj))
6
6
  process.stdout.write('\n')
7
7
  }
@@ -1,6 +1,7 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { red } from '#lib/chalk'
2
3
  import { readJsonFile } from '#lib/json'
3
- import { isFilePathSync, isJsonString, getAbsoluteFilePath, validateTemplateCommand } from '#lib/utils'
4
+ import { isJsonString, getAbsoluteFileUrl, validateTemplateCommand } from '#lib/utils'
4
5
  import { parseGuid } from './parse_command_utils.js'
5
6
  import program from './program.js'
6
7
  import validateFunctionArgs from './validate_function_args.js'
@@ -20,22 +21,22 @@ const getData = args => {
20
21
  return JSON.parse(arg)
21
22
  }
22
23
 
23
- const filepath = getAbsoluteFilePath(arg)
24
+ const fileUrl = getAbsoluteFileUrl(arg)
24
25
 
25
- if (!isFilePathSync(filepath)) {
26
+ if (!existsSync(fileUrl)) {
26
27
  console.error(red('the argument should be a valid JSON or a JSON file path or a JS function file path'))
27
28
  console.error("- it doesn't look like inline JSON")
28
- console.error(`couldn't parse arguments: ${filepath} is not the path to an existing file`)
29
+ console.error(`couldn't parse arguments: ${fileUrl} is not the path to an existing file`)
29
30
  process.exit(1)
30
31
  }
31
32
 
32
33
  try {
33
34
  // Try to parse it as a JSON file
34
- return readJsonFile(filepath)
35
+ return readJsonFile(fileUrl)
35
36
  } catch (err1) {
36
37
  // Try to parse it as a JS module
37
38
  try {
38
- return getDataFromJsModule(filepath, args)
39
+ return getDataFromJsModule(fileUrl, args)
39
40
  } catch (err2) {
40
41
  if (err2 === 'SyntaxError') {
41
42
  console.error(red('the argument should be a valid JSON or a JSON file path or a JS function file path'))
@@ -50,8 +51,8 @@ const getData = args => {
50
51
  }
51
52
  }
52
53
 
53
- async function getDataFromJsModule (filepath, args) {
54
- const { default: jsModule } = await import(filepath)
54
+ async function getDataFromJsModule (fileUrl, args) {
55
+ const { default: jsModule } = await import(fileUrl)
55
56
  if (typeof jsModule === 'function') {
56
57
  const inputArgs = args.slice(1)
57
58
  validateFunctionArgs(jsModule, inputArgs, jsModule)
@@ -10,8 +10,8 @@ export function outputTemplatesFactory ({ batchMode, format, propsToPick, reques
10
10
  const formatEntity = FormatEntity(batchMode, propsToPick, requestedPropsAndSubProps, minimize)
11
11
  return async function outputTemplates (entities) {
12
12
  entities = entities.map(formatEntity)
13
- if (format === 'js') {
14
- const jsFile = await stringifyAsJsFunction(entities[0], program.lang)
13
+ if (format.endsWith('js')) {
14
+ const jsFile = await stringifyAsJsFunction(entities[0], program.lang, format)
15
15
  console.log(jsFile)
16
16
  } else {
17
17
  const newLines = entities.map(entity => JSON.stringify(entity)).join('\n')
package/lib/program.js CHANGED
@@ -27,9 +27,9 @@ program.process = async command => {
27
27
  } else if (showHelp) {
28
28
  program.helpAndExit(0)
29
29
  } else if (program.args.length === 0 && !program.batch) {
30
- if (commandsTakingArgsOnStdin.includes(command)) {
30
+ if (program.acceptsArgsOnStdin) {
31
31
  if (process.stdin.isTTY) program.helpAndExit(0)
32
- } else if (!commandsAcceptingZeroArguments.includes(command)) {
32
+ } else if (!program.canHaveZeroArguments) {
33
33
  program.helpAndExit(0)
34
34
  }
35
35
  }
@@ -48,23 +48,6 @@ function getOptionsKeyValues (options) {
48
48
  return Object.fromEntries(optionsEntries)
49
49
  }
50
50
 
51
- const commandsAcceptingZeroArguments = [
52
- // Can be called without argument
53
- 'props',
54
- 'badges',
55
- // All arguments are passed as options values
56
- // making program.args.length === 0 likely
57
- 'query',
58
- 'convert',
59
- // Needs to also log the current config
60
- 'config',
61
- ]
62
-
63
- const commandsTakingArgsOnStdin = [
64
- 'data',
65
- 'generate-template',
66
- ]
67
-
68
51
  const parseArgv = (argv, isCommandsWithCustomHelpMenu) => {
69
52
  // Make a copy to be able to mutate the array without affecting other operations
70
53
  // that might rely on that array being intact
@@ -1,12 +1,15 @@
1
1
  import split from 'split'
2
2
  import through from 'through'
3
+ import program from '#lib/program'
3
4
 
4
5
  let ids = []
5
6
 
6
7
  export function readIdsFromStdin (handleIdsBatch) {
7
- const write = function (id) {
8
+ let count = 0
9
+ async function write (id) {
8
10
  id = id.trim()
9
11
  if (id === '') return
12
+ count++
10
13
  ids.push(id)
11
14
  if (ids.length < 50) return
12
15
 
@@ -15,18 +18,23 @@ export function readIdsFromStdin (handleIdsBatch) {
15
18
  const batch = ids
16
19
  ids = []
17
20
 
18
- handleIdsBatch(batch)
19
- .then(() => this.resume())
21
+ await handleIdsBatch(batch)
22
+ this.resume()
20
23
  }
21
24
 
22
- const end = function () {
25
+ async function end () {
26
+ if (count === 0) program.helpAndExit(0)
23
27
  if (ids.length === 0) return this.emit('end')
24
- handleIdsBatch(ids)
25
- .then(this.emit.bind(this, 'end'))
28
+ await handleIdsBatch(ids)
29
+ this.emit('end')
26
30
  }
27
31
 
28
32
  process.stdin
29
33
  .pipe(split(/\s/))
30
34
  .pipe(through(write, end))
31
35
  .on('error', console.error)
36
+
37
+ setTimeout(() => {
38
+ if (count === 0) program.helpAndExit(0)
39
+ }, 500)
32
40
  }
@@ -70,11 +70,12 @@ const getPatternComment = (line, pattern, entitiesLabels) => {
70
70
  }
71
71
  }
72
72
 
73
- export default async (entity, lang) => {
73
+ export default async (entity, lang, format) => {
74
74
  const json = JSON.stringify(entity, null, 2)
75
75
  // Use the classic syntax function to avoid the implicit return syntax
76
76
  // which might be confusion to users unfamiliar with JS
77
- const file = `module.exports = function () {
77
+ const moduleExport = format === 'mjs' ? 'export default function' : 'module.exports = function'
78
+ const file = `${moduleExport} () {
78
79
  return ${formatJsObj(json)}
79
80
  }`
80
81
  const entitiesIds = collectEntitiesIds(json)
package/lib/utils.js CHANGED
@@ -1,29 +1,30 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
3
4
  import errors_ from './errors.js'
4
5
 
5
6
  export const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
6
7
 
7
8
  export const sum = (a, b) => a + b
8
9
 
9
- export const average = values => {
10
+ export function average (values) {
10
11
  if (values.length > 0) return values.reduce(sum, 0) / values.length
11
12
  else return 0
12
13
  }
13
14
 
14
- export const getAbsoluteFilePath = filepath => path.resolve(process.cwd(), filepath)
15
+ export const getAbsoluteFileUrl = filepath => pathToFileURL(path.resolve(process.cwd(), filepath))
15
16
 
16
- export const isFilePathSync = arg => {
17
- const possibleFilePath = getAbsoluteFilePath(arg)
18
- return existsSync(possibleFilePath)
17
+ export function isFilePathSync (arg) {
18
+ const possibleFileUrl = getAbsoluteFileUrl(arg)
19
+ return existsSync(possibleFileUrl)
19
20
  }
20
21
 
21
- export const isJsonString = str => {
22
+ export function isJsonString (str) {
22
23
  if (typeof str !== 'string') return false
23
24
  else return (str.trim()[0] === '{' || str.trim()[0] === '[')
24
25
  }
25
26
 
26
- export const validateTemplateCommand = ({ commandName, validCommands }) => {
27
+ export function validateTemplateCommand ({ commandName, validCommands }) {
27
28
  if (!validCommands) return
28
29
  if (validCommands && !validCommands.includes(commandName)) {
29
30
  throw errors_.exitMessage('wrong command for this template', { commandName, validCommands })
package/metadata/data.js CHANGED
@@ -16,7 +16,7 @@ export default {
16
16
  { args: '--simplify Q123', comment: 'fetch Q123 simplified data' },
17
17
  { args: '--simplify --keep ids,references,qualifiers,hashes,nontruthy Q123', comment: 'simplified Q123 data, but keep some attributes' },
18
18
  { args: '--simplify --keep all Q123', comment: 'same as above' },
19
- { args: 'Q1496 | jd labels.pt', comment: "take advantage of the raw data being output as JSON\n # to pass it to a JSON parsers (here jsondepth a.k.a. jd)\n # and get only the piece of data you're looking for" },
20
- { args: '--simplify --keep ids Q123 | jd claims.P138 -j', comment: 'get Q123 P138 claims ids' },
19
+ { args: 'Q1496 | jq .labels.pt', comment: "take advantage of the raw data being output as JSON\n # to pass it to jq (a JSON parsers https://jqlang.github.io/jq/)\n # and get only the piece of data you're looking for" },
20
+ { args: '--simplify --keep ids Q123 | jq .claims.P138 -j', comment: 'get Q123 P138 claims ids' },
21
21
  ],
22
22
  }
package/metadata/query.js CHANGED
@@ -15,7 +15,7 @@ export default {
15
15
  { args: '--property P921 --object Q44559 --labels', comment: 'find out which works have exoplanets (Q44559) for main subject (P921)\n # and include labels' },
16
16
  { args: '--property P7476 --count', comment: 'get a list of all the claims using the property (P7476)' },
17
17
  { args: '-p P921 -o Q44559 -a', comment: 'as always, you can also use the short options syntax' },
18
- { args: '-p P921 -o Q44559 -a | jd 2', comment: 'the output is valid JSON, so you can pipe it to any JSON parser you like\n # (here jsondepth a.k.a. jd)' },
18
+ { args: "-p P921 -o Q44559 -a | jq '.[]' -cr", comment: 'the output is valid JSON, so you can pipe it to any JSON parser you like\n # (here jq https://jqlang.github.io/jq/)' },
19
19
  { args: '-s Q15228 -p P674 -a', comment: 'find out which are the characters (P674) in The Lord of The Ring (Q15228)' },
20
20
  { args: '-s Q15228 -o Q177499 -a', comment: 'find out which property link The Lord of The Ring (Q15228) and Gandalf (Q177499)' },
21
21
  { args: '-s Q177499 -a', comment: 'get the properties and objects for all the triples (subject, property, object) having Gandalf (Q177499) as subject' },
@@ -12,6 +12,7 @@ export default {
12
12
  },
13
13
  examples: [
14
14
  { args: 'Q3548931', comment: "fetch Q3548931's revisions data" },
15
+ { args: 'Q3548931 -p content,user,ids', comment: "fetch only certain attributes from Q3548931's revisions data" },
15
16
  { args: 'Property:P31', comment: "fetch P31's revisions data" },
16
17
  ],
17
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikibase-cli",
3
- "version": "18.1.0",
3
+ "version": "18.2.1",
4
4
  "description": "A command-line interface to Wikibase",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,7 +16,7 @@
16
16
  "lint-staged": "./scripts/lint_staged",
17
17
  "prepublishOnly": "export MOCHA_OPTIONS='--bail'; npm run lint && npm test",
18
18
  "postpublish": "npm run docker:publish && git push --tags",
19
- "test": "mocha --exit --timeout 20000 $MOCHA_OPTIONS",
19
+ "test": "export FORCE_COLOR=false; mocha --exit --timeout 20000 $MOCHA_OPTIONS",
20
20
  "update-toc": "./scripts/update_toc"
21
21
  },
22
22
  "repository": {
@@ -57,7 +57,7 @@
57
57
  "shell-quote": "^1.8.1",
58
58
  "split": "^1.0.1",
59
59
  "through": "^2.3.8",
60
- "wikibase-edit": "^7.1.0",
60
+ "wikibase-edit": "^7.1.2",
61
61
  "wikibase-sdk": "^10.1.0",
62
62
  "wikidata-lang": "^2.0.11"
63
63
  },