maxlath / wikibase-cli-template-collection

A collection of wikibase-cli templates
2 stars 1 forks source link

Japan Road Race Championship template request #2

Open serpico opened 3 years ago

serpico commented 3 years ago

Hi,

I'd like to get started with wikibase-cli mainly to replace/supplement QuickStatements for my WD batch editing.

As an example I tried the following with QS ( I prepared it in a spreadsheet ) to create and populate an item in one "operation" :

CREATE
LAST|Len|"2019 All Japan Road Race Championship Round 6 Okayama"|
LAST|P31|Q106712954|P155|somevalue|P1545|somevalue|P156|somevalue||||||||||||
LAST|P361|Q106725472|P155|somevalue|P1545|somevalue|P156|somevalue||||||||||||
LAST|P17|Q17||||||||||||||||||
LAST|P276|Q173185||||||||||||||||||
LAST|P580|+2019-08-30T00:00:00Z/11||||||||||||||||||
LAST|P582|+2019-09-01T00:00:00Z/11||||||||||||||||||

unfortunately it doesn't work and as a workaround I used :

CREATE
LAST|Len|"2019 All Japan Road Race Championship Round 6 Okayama"|

Then after manually copy/pasting the newly created item's Q number :

Q106725781|P31|Q106712954|P155|somevalue|P1545|somevalue|P156|somevalue||||||||||||
Q106725781|P361|Q106725472|P155|somevalue|P1545|somevalue|P156|somevalue||||||||||||
Q106725781|P17|Q17||||||||||||||||||
Q106725781|P276|Q173185||||||||||||||||||
Q106725781|P580|+2019-08-30T00:00:00Z/11||||||||||||||||||
Q106725781|P582|+2019-09-01T00:00:00Z/11||||||||||||||||||

Woud you have some pointers on how to move in that direction, to familiarize myself so somewhere down the road I'd like to able to write/modify a little script in which I'd have the values for P31, P361, P276 as arguments ( with an/several "if function" example : with arg1 being the year and arg2 being Okayama ( with a list of abt 10 different values for arg2 )

if  arg1=2019 then P361=Q106725472
if  arg1=2018 then P361=Q106726771
if  arg1=2017 then P361=Q106726795

etc....

and

if  arg2=Okayama then P31=Q106712954 ; 
if  arg2=Okayama then P276=Q173185
if  arg2=Motegi then P31=Q106712966
if  arg2=Motegi then p276=Q1420895 

so I can process several seasons and rounds faster, although I can't see a way to change the value for P580 and P582 other than manually...

Thanks in advance for any guidance you'd be willing to provide.

maxlath commented 3 years ago

That could look something like this:

// template.js
const datePattern = /^\d{4}-\d{2}-\d{2}$/
const itemIdPattern = /^Q\d+$/
const ordinalPattern = /^\d+$/

module.exports = (label, year, location, start, end, follows, isFollowedBy, ordinal) => {
  const championship = championships[year]
  const type = types[location]
  const location = locations[location]

  // Validating arguments as it would be easy to get messed up given the number of arguments to pass
  if (label.split(' ').length < 4) throw new Error(`that label looks too short: ${label}`)
  if (!championship) throw new Error(`unknown championship: ${championship}`)
  if (!type) throw new Error(`unknown type: ${type}`)
  if (!location) throw new Error(`unknown location: ${location}`)
  if (!datePattern.test(start)) throw new Error(`invalid start date: ${start}`)
  if (!datePattern.test(end)) throw new Error(`invalid end date: ${end}`)

  // Make those qualifier values optional: pass null to skip
  if (follows === 'null') follows === null
  if (isFollowedBy === 'null') isFollowedBy === null

  if (follows && !itemIdPattern.test(follows)) throw new Error(`invalid follows item id: ${follows}`)
  if (isFollowedBy && !itemIdPattern.test(isFollowedBy)) throw new Error(`invalid isFollowedBy item id: ${isFollowedBy}`)
  if (!ordinalPattern.test(ordinal)) throw new Error(`invalid ordinal number: ${ordinal}`)

  const qualifiers = {
    P156: ordinal
  }

  if (follows) qualifiers.P155 = follows
  if (isFollowedBy) qualifiers.P156 = isFollowedBy

  const newItem = {
    labels: {
      en: label
    },
    claims: {
      P31: { value: type, qualifiers },
      P361: { value: championship, qualifiers },
      P17: 'Q17',
      P276: location,
      P580: start,
      P582: end,
    }
  }

  return newItem
}

const championships = {
  2019: 'Q106725472',
  2018: 'Q106726771',
  2017: 'Q106726795',
}

const types = {
  'Okayama': 'Q106712954',
  'Motegi': 'Q106712966',
}

const locations = {
  'Okayama': 'Q173185',
  'Motegi': 'Q1420895',
}

to be then called from the CLI like that:

wd create-entity ./template.js "2019 All Japan Road Race Championship Round 6 Okayama" 2019 Okayama 2019-08-30 2019-09-01 null Q106713038 12

I haven't tested it so it might require some adjustments ^^

serpico commented 3 years ago

@maxlath Beautiful!! I'll play with it sometimes next week most probably ( long enough so I don't ask silly questions ) and I'll get back here to tell you how it went. Thanks a lot :fire: :rocket:

serpico commented 3 years ago

I completed the const lists at the bottom as follow :

const championships = {
  2021: 'Q106735845',
  2020: 'Q106707657',
  2019: 'Q106725472',
  2018: 'Q106726771',
  2017: 'Q106726795',   
  2016: 'Q106726816',
  2015: 'Q106726831',   
}

const types = {
  'Autopolis': 'Q106705839',
  'Autopolis 2&4' : 'Q106763105',
  'MFJ GP' : 'Q106712980',
  'Motegi': 'Q106712966', 
  'Motegi 2&4' : 'Q106763118',
  'Okayama': 'Q106712954',
  'Sugo' : 'Q106697764',
  'Suzuka' : 'Q106737001',
  'Suzuka 2&4' : 'Q106737001',
  'Tsukuba' : 'Q106725499',
}

const locations = {
  'Autopolis' : 'Q1748785',
  'Motegi': 'Q1420895',
  'Okayama': 'Q173185',
  'Sugo' : 'Q917398',
  'Suzuka' : 'Q174170',
  'Tsukuba' : 'Q2097263',
}

Tried :

wd create-entity ./template.js "2018 All Japan Road Race Championship Round 1 Motegi" 2018 Motegi 2018-04-05 2018-04-08 null null 1

Got this output :

/home/arno/wikibase-cli-templates/create/template.js:9
  const location = locations[location]
        ^

SyntaxError: Identifier 'location' has already been declared
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:616:28)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)
    at getDataFromJsModule (/usr/local/lib/node_modules/wikibase-cli/lib/object_arg_parser.js:57:20)

Replaced location by locatione in line 9 ; line 15 (!locatione) ; line 42 Re-run

wd create-entity ./template.js "2018 All Japan Road Race Championship Round 1 Motegi" 2018 Motegi 2018-04-05 2018-04-08 null null 1

Got :

Error: invalid follows item id: null
    at module.exports (/home/arno/wikibase-cli-templates/create/template.js:23:54)
    at getDataFromJsModule (/usr/local/lib/node_modules/wikibase-cli/lib/object_arg_parser.js:61:12)
    at getData (/usr/local/lib/node_modules/wikibase-cli/lib/object_arg_parser.js:36:14)
    at Command.module.exports.args [as customArgsParser] (/usr/local/lib/node_modules/wikibase-cli/lib/object_arg_parser.js:8:16)
    at runOnce (/usr/local/lib/node_modules/wikibase-cli/lib/edit/edit_command.js:59:20)
    at module.exports (/usr/local/lib/node_modules/wikibase-cli/lib/edit/edit_command.js:41:13)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:695:11)
    at startup (bootstrap_node.js:188:16)

Tried :

wd create-entity ./template.js "2018 All Japan Road Race Championship Round 1 Motegi" 2018 Motegi 2018-04-05 2018-04-08 Q106713038 Q106713038 1

as a dummy edit, to avoid coming up against the null error above again

Got :


{ permissiondenied: permissiondenied: You do not have the permissions needed to carry out this action.
    at requestError (/usr/local/lib/node_modules/wikibase-cli/node_modules/wikibase-edit/lib/request/parse_response_body.js:18:15)
    at module.exports (/usr/local/lib/node_modules/wikibase-cli/node_modules/wikibase-edit/lib/request/parse_response_body.js:11:33)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
url: https://www.wikidata.org/w/api.php?action=wbeditentity&format=json
response status: 200
response body: {"error":{"code":"permissiondenied","info":"You do not have the permissions needed to carry out this action.","messages":[{"name":"wikibase-api-permissiondenied","parameters":[],"html":{"*":"You do not have the permissions needed to carry out this action."}},{"name":"badaccess-groups","parameters":["*, [[Wikidata:Users|Users]]",2],"html":{"*":"The action you have requested is limited to users in one of the groups: *, <a href=\"/wiki/Wikidata:Users\" title=\"Wikidata:Users\">Users</a>."}}],"*":"See https://www.wikidata.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."},"servedby":"mw1378"}

At the beginning I remember getting the credential set up prompt, I followed the instructions, choose OAuth, copy/pasted the 4 "keys" from the meta.wikimedia webpage into the terminal ( but I didn't "tick" any of the boxes on the webpage, didn't change anything, just went to the bottom of webpage and clicked on some kind of validation button (from memory) ).

Now I'm wondering if I should have ticked some of those boxes to be able to get the proper permission...

I tried

wb config credentials https://www.wikidata.org

The 4 "keys" show up

In the "Manage connected applications" webpage of meta.wikimedia.org, for wikibase-cli-myusername I only have "revoke access" not "manage access" ( I was hoping to be able to activate the necessary permission which I might have missed at the "ticking boxes" step...

Let me know if you need me to try something else. Thanks again

maxlath commented 3 years ago

So I did a few mistakes in the template >< which I could have easily tested by running the command with the --dry flag, which prevents to actually run the edit. Now that it's done, I corrected the template to this:

// template.js
const datePattern = /^\d{4}-\d{2}-\d{2}$/
const itemIdPattern = /^Q\d+$/
const ordinalPattern = /^\d+$/

module.exports = (label, year, locationName, start, end, follows, isFollowedBy, ordinal) => {
  console.error({ label, year, locationName, start, end, follows, isFollowedBy, ordinal })
  const championship = championships[year]
  const type = types[locationName]
  const location = locations[locationName]

  // Validating arguments as it would be easy to get messed up given the number of arguments to pass
  if (label.split(' ').length < 4) throw new Error(`that label looks too short: ${label}`)
  if (!championship) throw new Error(`unknown championship: ${championship}`)
  if (!type) throw new Error(`unknown type: ${type}`)
  if (!location) throw new Error(`unknown location: ${location}`)
  if (!datePattern.test(start)) throw new Error(`invalid start date: ${start}`)
  if (!datePattern.test(end)) throw new Error(`invalid end date: ${end}`)

  // Make those qualifier values optional: pass null to skip
  if (follows === 'null') follows = null
  if (isFollowedBy === 'null') isFollowedBy = null

  if (follows && !itemIdPattern.test(follows)) throw new Error(`invalid follows item id: ${follows}`)
  if (isFollowedBy && !itemIdPattern.test(isFollowedBy)) throw new Error(`invalid isFollowedBy item id: ${isFollowedBy}`)
  if (!ordinalPattern.test(ordinal)) throw new Error(`invalid ordinal number: ${ordinal}`)

  const qualifiers = {
    P156: ordinal
  }

  if (follows) qualifiers.P155 = follows
  if (isFollowedBy) qualifiers.P156 = isFollowedBy

  const newItem = {
    labels: {
      en: label
    },
    claims: {
      P31: { value: type, qualifiers },
      P361: { value: championship, qualifiers },
      P17: 'Q17',
      P276: location,
      P580: start,
      P582: end,
    }
  }

  return newItem
}

const championships = {
  2021: 'Q106735845',
  2020: 'Q106707657',
  2019: 'Q106725472',
  2018: 'Q106726771',
  2017: 'Q106726795',
  2016: 'Q106726816',
  2015: 'Q106726831',
}

const types = {
  'Autopolis': 'Q106705839',
  'Autopolis 2&4' : 'Q106763105',
  'MFJ GP' : 'Q106712980',
  'Motegi': 'Q106712966',
  'Motegi 2&4' : 'Q106763118',
  'Okayama': 'Q106712954',
  'Sugo' : 'Q106697764',
  'Suzuka' : 'Q106737001',
  'Suzuka 2&4' : 'Q106737001',
  'Tsukuba' : 'Q106725499',
}

const locations = {
  'Autopolis' : 'Q1748785',
  'Motegi': 'Q1420895',
  'Okayama': 'Q173185',
  'Sugo' : 'Q917398',
  'Suzuka' : 'Q174170',
  'Tsukuba' : 'Q2097263',
}

As for the permissions, I would recommand to delete the previous tokens and run:

wb config credentials https://www.wikidata.org reset

and select the following authorizations:

serpico commented 3 years ago

There was a tiny typo line 29 ( P156 instead of P1545 ), once changed the item was created successfully !!! :congratulations:

I tried to make the ordinal argument optional as well by mimicking line 21 and 22 but I couldn't ( among other things >< ) figure out how to modify the check in line 4 accordingly

Would it be possible :

  1. To have 2 distinct parameters/arguments for ordinal, one applying to P31, another applying to P361 ? as they're never the same, the same goes for follows and isFollowedBy, with the CLI command ordered as follow ( maybe following the pattern as they appears on WD ( although I know it's cosmetic, it doesn't matter how they appear/are ordered, just when one's does manual edit/comparison, for clarity/readability's sake ) :

wd create-entity ./template.js "2019 All Japan Road Race Championship Round 6 Okayama" 2019 Okayama followsP31 ordinalP31 isFollowedP31 followsP361 ordinalP361 isFollowedP361 2019-08-30 2019-09-01

Test case with the current syntax :

wd create-entity ./template.js "2018 All Japan Road Race Championship Round 2 Suzuka 2&4" 2018 Suzuka 2018-04-20 2018-04-22 Q106766005 null 2

Allowed me to create Q106794957, the value, Q106766005, is correct for P361 but for P31 I haven't created the item for 2017 All Japan Road Race Championship Round x Suzuka 2&4 yet so even if I had a specific/different parameter in the syntax/command to add it to the CLI command, I'd be nice to have 'somevalue' as allowed value ( see pt. 2 below ) so I can update ( manually ) as soon as I'll create it ( or more realistically somewhere down the road :-) ... )

  1. To have followsP31/isFollowedP31/followsP361/isFollowedP361 as well as ordinalP31/ordinalP361 accepting 'null' ( not created - current behavior ) but also 'somevalue' ( created - appears as unknown value ) as value ( so it's faster to modify the qualifier value manually once all the necessary items are created.

Following the proposed pattern, the ideal ( at least in my mind, at this point...) syntax would resemble something like :

wd create-entity ./template.js "2018 All Japan Road Race Championship Round 3 Autopolis 2&4" 2018 Autopolis somevalue somevalue Q106725510 Q106794957 3 somevalue 2018-05-11 2018-05-13

And the result should be as it it appears for Q106799195 ( created manually for the demo purpose )

I'm not sure my explanations are clear enough, so I could create the items manually as they are expected to appear for each parameter combination/CLI syntax if you feel it's necessary, it wouldn't a problem at all for me, feel free to ask if you think it may save you some time.

Thanks again for your help

maxlath commented 3 years ago

could be something like this?

// template.js
const datePattern = /^\d{4}-\d{2}-\d{2}$/
const itemIdPattern = /^Q\d+$/
const ordinalPattern = /^\d+$/

module.exports = (label, year, locationName, followsP31, ordinalP31, isFollowedP31, followsP361, ordinalP361, isFollowedP361, start, end ) => {
  console.error({ label, year, locationName, followsP31, ordinalP31, isFollowedP31, followsP361, ordinalP361, isFollowedP361, start, end })
  const championship = championships[year]
  const type = types[locationName]
  const location = locations[locationName]

  // Validating arguments as it would be easy to get messed up given the number of arguments to pass
  if (label.split(' ').length < 4) throw new Error(`that label looks too short: ${label}`)
  if (!championship) throw new Error(`unknown championship: ${championship}`)
  if (!type) throw new Error(`unknown type: ${type}`)
  if (!location) throw new Error(`unknown location: ${location}`)
  if (!datePattern.test(start)) throw new Error(`invalid start date: ${start}`)
  if (!datePattern.test(end)) throw new Error(`invalid end date: ${end}`)

  // Make those qualifier values optional: pass null to skip
  if (followsP31 === 'null') followsP31 = null
  if (isFollowedByP31 === 'null') isFollowedByP31 = null
  if (ordinalP31 === 'null') ordinalP31 = null
  if (followsP31 === 'somevalue') followsP31 = { snaktype: 'somevalue' }
  if (isFollowedByP31 === 'somevalue') isFollowedByP31 = { snaktype: 'somevalue' }
  if (ordinalP31 === 'somevalue') ordinalP31 = { snaktype: 'somevalue' }

  if (isNormalSnaktypeValue(followsP31) && !itemIdPattern.test(followsP31)) throw new Error(`invalid followsP31 item id: ${followsP31}`)
  if (isNormalSnaktypeValue(isFollowedByP31) && !itemIdPattern.test(isFollowedByP31)) throw new Error(`invalid isFollowedByP31 item id: ${isFollowedByP31}`)
  if (isNormalSnaktypeValue(ordinalP31) && !ordinalP31Pattern.test(ordinalP31)) throw new Error(`invalid ordinalP31 number: ${ordinalP31}`)

  const qualifiersP31 = {}
  if (followsP31) qualifiersP31.P155 = followsP31
  if (isFollowedByP31) qualifiersP31.P156 = isFollowedByP31
  if (ordinalP31) qualifiersP31.P1545 = ordinalP31

  if (followsP361 === 'null') followsP361 = null
  if (isFollowedByP361 === 'null') isFollowedByP361 = null
  if (ordinalP361 === 'null') ordinalP361 = null
  if (followsP361 === 'somevalue') followsP361 = { snaktype: 'somevalue' }
  if (isFollowedByP361 === 'somevalue') isFollowedByP361 = { snaktype: 'somevalue' }
  if (ordinalP361 === 'somevalue') ordinalP361 = { snaktype: 'somevalue' }

  if (isNormalSnaktypeValue(followsP361) && !itemIdPattern.test(followsP361)) throw new Error(`invalid followsP361 item id: ${followsP361}`)
  if (isNormalSnaktypeValue(isFollowedByP361) && !itemIdPattern.test(isFollowedByP361)) throw new Error(`invalid isFollowedByP361 item id: ${isFollowedByP361}`)
  if (isNormalSnaktypeValue(ordinalP361) && !ordinalP361Pattern.test(ordinalP361)) throw new Error(`invalid ordinalP361 number: ${ordinalP361}`)

  const qualifiersP361 = {}
  if (followsP361) qualifiersP361.P155 = followsP361
  if (isFollowedByP361) qualifiersP361.P156 = isFollowedByP361
  if (ordinalP361) qualifiersP361.P1545 = ordinalP361

  const newItem = {
    labels: {
      en: label
    },
    claims: {
      P31: { value: type, qualifiers: qualifiersP31 },
      P361: { value: championship, qualifiers: qualifiersP361 },
      P17: 'Q17',
      P276: location,
      P580: start,
      P582: end,
    }
  }

  return newItem
}

const championships = {
  2021: 'Q106735845',
  2020: 'Q106707657',
  2019: 'Q106725472',
  2018: 'Q106726771',
  2017: 'Q106726795',
  2016: 'Q106726816',
  2015: 'Q106726831',
}

const types = {
  'Autopolis': 'Q106705839',
  'Autopolis 2&4' : 'Q106763105',
  'MFJ GP' : 'Q106712980',
  'Motegi': 'Q106712966',
  'Motegi 2&4' : 'Q106763118',
  'Okayama': 'Q106712954',
  'Sugo' : 'Q106697764',
  'Suzuka' : 'Q106737001',
  'Suzuka 2&4' : 'Q106737001',
  'Tsukuba' : 'Q106725499',
}

const locations = {
  'Autopolis' : 'Q1748785',
  'Motegi': 'Q1420895',
  'Okayama': 'Q173185',
  'Sugo' : 'Q917398',
  'Suzuka' : 'Q174170',
  'Tsukuba' : 'Q2097263',
}

const isNormalSnaktypeValue = value => typeof value === 'string'
serpico commented 3 years ago

Below is the one I used, I just had to define ordinalP31Pattern and ordinalP361Pattern by copying what you did for ordinalPattern and replace isFollowedBy by isFollowed and it worked splendidly !!!

There's just the qualifiers orders but it's cosmetic so don't bother it'll do like this.

Once again, thanks for bestowing your magic on this, for kicks I'll try to do a whole or even half a season manually and the same using your code, just to fully realize how much time you saved me, and if I do I'll report the numbers.

// template.js
const datePattern = /^\d{4}-\d{2}-\d{2}$/
const itemIdPattern = /^Q\d+$/
const ordinalPattern = /^\d+$/
const ordinalP31Pattern = /^\d+$/
const ordinalP361Pattern = /^\d+$/

module.exports = (label, year, locationName, followsP31, ordinalP31, isFollowedP31, followsP361, ordinalP361, isFollowedP361, start, end ) => {
  console.error({ label, year, locationName, followsP31, ordinalP31, isFollowedP31, followsP361, ordinalP361, isFollowedP361, start, end })
  const championship = championships[year]
  const type = types[locationName]
  const location = locations[locationName]

  // Validating arguments as it would be easy to get messed up given the number of arguments to pass
  if (label.split(' ').length < 4) throw new Error(`that label looks too short: ${label}`)
  if (!championship) throw new Error(`unknown championship: ${championship}`)
  if (!type) throw new Error(`unknown type: ${type}`)
  if (!location) throw new Error(`unknown location: ${location}`)
  if (!datePattern.test(start)) throw new Error(`invalid start date: ${start}`)
  if (!datePattern.test(end)) throw new Error(`invalid end date: ${end}`)

  // Make those qualifier values optional: pass null to skip
  if (followsP31 === 'null') followsP31 = null
  if (isFollowedP31 === 'null') isFollowedP31 = null
  if (ordinalP31 === 'null') ordinalP31 = null
  if (followsP31 === 'somevalue') followsP31 = { snaktype: 'somevalue' }
  if (isFollowedP31 === 'somevalue') isFollowedP31 = { snaktype: 'somevalue' }
  if (ordinalP31 === 'somevalue') ordinalP31 = { snaktype: 'somevalue' }

  if (isNormalSnaktypeValue(followsP31) && !itemIdPattern.test(followsP31)) throw new Error(`invalid followsP31 item id: ${followsP31}`)
  if (isNormalSnaktypeValue(isFollowedP31) && !itemIdPattern.test(isFollowedP31)) throw new Error(`invalid isFollowedP31 item id: ${isFollowedP31}`)
  if (isNormalSnaktypeValue(ordinalP31) && !ordinalP31Pattern.test(ordinalP31)) throw new Error(`invalid ordinalP31 number: ${ordinalP31}`)

  const qualifiersP31 = {}
  if (followsP31) qualifiersP31.P155 = followsP31
  if (isFollowedP31) qualifiersP31.P156 = isFollowedP31
  if (ordinalP31) qualifiersP31.P1545 = ordinalP31

  if (followsP361 === 'null') followsP361 = null
  if (isFollowedP361 === 'null') isFollowedP361 = null
  if (ordinalP361 === 'null') ordinalP361 = null
  if (followsP361 === 'somevalue') followsP361 = { snaktype: 'somevalue' }
  if (isFollowedP361 === 'somevalue') isFollowedP361 = { snaktype: 'somevalue' }
  if (ordinalP361 === 'somevalue') ordinalP361 = { snaktype: 'somevalue' }

  if (isNormalSnaktypeValue(followsP361) && !itemIdPattern.test(followsP361)) throw new Error(`invalid followsP361 item id: ${followsP361}`)
  if (isNormalSnaktypeValue(isFollowedP361) && !itemIdPattern.test(isFollowedP361)) throw new Error(`invalid isFollowedP361 item id: ${isFollowedP361}`)
  if (isNormalSnaktypeValue(ordinalP361) && !ordinalP361Pattern.test(ordinalP361)) throw new Error(`invalid ordinalP361 number: ${ordinalP361}`)

  const qualifiersP361 = {}
  if (followsP361) qualifiersP361.P155 = followsP361
  if (isFollowedP361) qualifiersP361.P156 = isFollowedP361
  if (ordinalP361) qualifiersP361.P1545 = ordinalP361

  const newItem = {
    labels: {
      en: label
    },
    claims: {
      P31: { value: type, qualifiers: qualifiersP31 },
      P361: { value: championship, qualifiers: qualifiersP361 },
      P17: 'Q17',
      P276: location,
      P580: start,
      P582: end,
    }
  }

  return newItem
}

const championships = {
  2021: 'Q106735845',
  2020: 'Q106707657',
  2019: 'Q106725472',
  2018: 'Q106726771',
  2017: 'Q106726795',
  2016: 'Q106726816',
  2015: 'Q106726831',
}

const types = {
  'Autopolis': 'Q106705839',
  'Autopolis 2&4' : 'Q106763105',
  'MFJ GP' : 'Q106712980',
  'Motegi': 'Q106712966',
  'Motegi 2&4' : 'Q106763118',
  'Okayama': 'Q106712954',
  'Sugo' : 'Q106697764',
  'Suzuka' : 'Q106737001',
  'Suzuka 2&4' : 'Q106737001',
  'Tsukuba' : 'Q106725499',
}

const locations = {
  'Autopolis' : 'Q1748785',
  'Motegi': 'Q1420895',
  'Okayama': 'Q173185',
  'Sugo' : 'Q917398',
  'Suzuka' : 'Q174170',
  'Tsukuba' : 'Q2097263',
}

const isNormalSnaktypeValue = value => typeof value === 'string'