diff --git a/data-workflow/v1/conditormetrie.cfg b/data-workflow/v1/conditormetrie.cfg new file mode 100644 index 0000000..401058a --- /dev/null +++ b/data-workflow/v1/conditormetrie.cfg @@ -0,0 +1,394 @@ +[use] +plugin = basics +plugin = analytics + +[env] +path = number2labelDR +value = fix({"01": "DR01 Ile-de-France Villejuif","02": "DR02 Paris-Centre","04": "DR04 Ile-de-France Gif-sur-Yvette","05": "DR05 Ile-de-France Meudon","16": "DR16 Paris-Normandie","06": "DR06 Centre Est","10": "DR10 Alsace","08": "DR08 Centre Limousin Poitou Charente","17": "DR17 Bretagne et Pays de la Loire","18": "DR18 Hauts-de-France","07": "DR07 Rhône Auvergne","11": "DR11 Alpes","12": "DR12 Provence et Corse","20": "DR20 Côte d'Azur","13": "DR13 Occitanie Est","14": "DR14 Occitanie Ouest","15": "DR15 Aquitaine"}) + +[assign] +# Récupère electronicPublicationDate et publicationDate +# Prend la plus ancienne (= la plus petite) +# Ne garde que l'année +path = ApilPublicationDate +value = get("host.electronicPublicationDate", "9999") \ + .castArray() \ + .concat(_.get(self, "host.publicationDate", "9999")) \ + .min().toString() \ + .thru(str => str.substring(0,4)) + +[assign] +path = ApilFinancement +value = get('funders').castArray().filter(Boolean).thru(arr => Boolean(arr.length)) + +# Quand les RNSR ne sont pas fournis dans authors.*.affiliations.*.rnsr +# on utilise les enrichissements et on les met au même niveau dans ApilRnsr +[map] +path = authors + +[map/map] +path = affiliations + +[map/map/assign] +path = ApilRnsr1 +value = get("rnsr") + +[map/map/swing] +test = get("ApilRnsr1").isEmpty() +[map/map/swing/assign] +path = ApilRnsr +value = get("enrichments.rnsr", []) \ + .filter(rnsr => !["200612821P", "200018571R", "199812965F", "201523784S"].includes(rnsr)) + +# On rassemble tous les RNSR au niveau de la notice (ceux en provenance de +# authors.*.rnsr et ceux en provenance de authors.*.affiliations.*) +# dans allAuthorsRnsr +[assign] +path=ApilRnsr2 +value= get("authors").map("rnsr").flatten() + +[assign] +path = allApilRnsr1 +; value= get("authors").map("affiliations").map("ApilRnsr1") +value= get("authors").flatMap("affiliations").flatMap("ApilRnsr1") + +[assign] +path= ApilRnsr +value= get("allApilRnsr1").concat(self.ApilRnsr2).compact().uniq() + +# Garde un identifiant +[assign] +path = sourceUidChain +value = get("business.sourceUidChain") + +#On ajoute un objet pour attribuer 'OA - Inconnu' aux null ou undefined +path = enrichments.openAccess.unpaywall.oaLocations +value = get("enrichments.openAccess.unpaywall.oaLocations",[{"hostType":"OA - Inconnu"}]) + +# Supprime les champs inutiles pour les études bibliométriques +[exchange] +value = omit(['business','origins','technical','allApilRnsr1','ApilRnsr2']) +append = pack + + +# Quand les RNSR ne sont pas fournis dans authors.*.affiliations.*.rnsr +# on utilise le Web Service qui les met au même niveau dans wsRnsr + +# Récupère les infos Loterre 2XK +[assign] +path = ws.loterre2xk +value = get("ApilRnsr").castArray().map((itemApilRnsr, indice) => ({indice, itemApilRnsr, codeRNSR: itemApilRnsr, institut: itemApilRnsr, publicationDate: self.ApilPublicationDate })) + +[expand] +path = ws.loterre2xk + +[expand/exploding] + +[expand/expand] +path = value.itemApilRnsr +size = 100 +cacheName = 04-2xk-identify + +[expand/expand/URLConnect] +url = https://loterre-resolvers.services.istex.fr/v1/2XK/identify +timeout = 120000 +noerror = true + +[expand/assign] +path = value.institut +value = get('value.institut').append(`|${self.value.publicationDate}`) + +[expand/expand] +path = value.institut +size = 100 +cacheName = 04-rnsr-year-instituts-cnrs + +[expand/expand/URLConnect] +url = https://mapping-tools.services.istex.fr/v1/rnsr-year/instituts-cnrs +timeout = 90002 +noerror = true + +[expand/assign] +path = value.label +value = get('value.itemApilRnsr.prefLabel@fr', 'n/a') + +path = value.labelNormalized +value = get('value.itemApilRnsr.prefLabel@fr', 'n/a').thru(item => String(item).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toUpperCase()) + +path = value.dr +value = get("value.itemApilRnsr.delegationRegionale_dep").castArray().compact().map(n => _.get(env("number2labelDR"), n, `unknow ${n}` )) + +[expand/aggregate] + +[assign] +path = ApilRnsr +value = get("ws.loterre2xk").castArray().map('codeRNSR') + +path = ApilWsLaboIntitule +value = get("ws.loterre2xk").castArray().map('label') + +path = ApilWsDr +value = get("ws.loterre2xk").castArray().map('dr') + +path = ApilWsInstitutCnrs +value = get("ws.loterre2xk").castArray().map('institut') + +# S'il y a au moins un institut, il y a au moins une affiliation CNRS +path = ApilWsIsCnrs +value = get("ws.loterre2xk").castArray().map('institut').thru(array => Boolean(array.length)) + +; [debug] +; path = ApilRnsr +; path = ApilWsLaboIntitule +; path = ApilWsDr +; path = ApilWsInstitutCnrs +; path = ApilWsIsCnrs + +############################################################### + +#interrogation d'Openalex depuis le champs doi https://biblio-tools.services.istex.fr/v1/openalex/works/expand +#Pas d'omit sur ce champs, les valeurs apc peuvent eventuellement servir pour d'autres cas +[assign] +path=ApilWsOpenalex +value = get("doi") + +[expand] +path = ApilWsOpenalex +size = 100 + +[expand/URLConnect] +url = https://biblio-tools.services.istex.fr/v1/openalex/works/expand +timeout = 90007 +noerror = true + +# Données Open Access host type modifiées à partir d'un champ fulltext, si hal est présent +#Transformer des données inconnues de 'HostType' en repository si absence d'un DOI mais présence de Hal dans 'fulltext' +[assign] +path=ApilOaLocationsHal +value=get("enrichments.openAccess.unpaywall.oaLocations").map("hostType").concat([self.fulltextUrl].map((value)=>value && value.replace(/^((?!hal).)*$/,"@@@@").replace(/.*hal.*/,"repository"))).uniq().filter((value, index, collection)=>{if(!(value === "OA - Inconnu" && collection[index+1] === "repository" )){return true}}).filter(value=>value!=="@@@@").compact() + +#Transformer des données inconnues en "green" si absence d'un DOI mais présence de "repository" dans 'ApilOaLocationsHal' +[assign] +path=ApilOaStatusHal +value=get("enrichments.openAccess.unpaywall.oaStatus").replace(/^$/,"OA - Inconnu").castArray().concat(self.ApilOaLocationsHal).compact().join(",").replace(/OA - Inconnu,repository|OA - Non,repository|closed,repository/g,"green").split(",").head().capitalize().replace("Oa - inconnu","OA - Inconnu") + +##Transformer des données inconnues en OA-Oui si absence d'un DOI mais présence de "green" dans 'ApiloaStatusHal' +[assign] +path=ApilIsOaHal +value=get("ApilOaStatusHal","OA - Inconnu").replace("closed","OA - Non").replace(/^((?!OA).)*$/,"OA - Oui") + +#On traduit les voies d'acces. Sort pour placer "publisher" avant "repository", replace puis si les 2 valeurs sont présentes, on remplace par "commun" +[assign] +path=ApilTypeDaccesHal +value=get("ApilOaLocationsHal").sort().replace("repository","Archive seule").replace("publisher","Editeur seul").replace("Editeur seul,Archive seule","Commun") + +#on crée un nouveau champ où l'on récupère les valeurs de "apc_list/value". Si value = 0 alors la publi est diamant. On remplace donc "0" par "diamond" et efface tout le reste. On concatène ensuite avec enrichments/openAccess/unpaywall/oaStatus qui donne les couleurs de l'OA. Puis on retire "gold" lorsqu'il est associé à "diamond" +[assign] +path=ApilOaStatusDiamond +value=get("ApilWsOpenalex").castArray().map("apc_list/value").replace(/^(?!0$).*$/,"").replace(/^0$/,"diamond").concat(_.get(self,"enrichments.openAccess.unpaywall.oaStatus")).filter((value, index, collection)=>{if(!(value === "gold" && collection[index-1] === "diamond" )){return true}}).last().capitalize().replace(/^$/,"OA - Inconnu") + + +#on crée un nouveau champ où l'on cumule les nouvelles données de 'ApilOaStatusDiamond' et 'ApilOaStatusHal' +[assign] +path=ApilOaStatusDiamondHal +value=get("ApilOaStatusDiamond").concat(self.ApilOaStatusHal).uniq().filter((value, index, collection)=>{if(!(value === "OA - Inconnu" && collection[index+1] === "Green" )){return true}}).filter((value, index, collection)=>{if(!(value === "Gold" && collection[index-1] === "Diamond" )){return true}}).toString() + +#Transformations spécifiques pour créer des valuers compatibles avec VEga-lite pour la création de graphiques +[assign] +path=ApilGraphSourceEditeurIsOa +value=get("enrichments.openAccess.unpaywall.isOa").replace(/^((?!Oui).)*$/,"null").prepend("OA=").append((";TypeAcces="+self.ApilTypeDaccesHal).replace(/Commun|Editeur seul/g,"Editeur").replace(/OA - Non|Archive seule|OA - Inconnu/g,"null")) + +[assign] +path=ApilGraphSourceEditeurIsOaHal +value=get("ApilIsOaHal").replace(/^((?!Oui).)*$/,"null").prepend("OA=").append((";TypeAcces="+self.ApilTypeDaccesHal).replace(/Commun|Editeur seul/g,"Editeur").replace(/OA - Non|Archive seule|OA - Inconnu/g,"null")) + +#On détermine l'ordre de provenance des notices composants la notice Conditor +[assign] +path = ApilProvenance +value = get("sourceUidChain").replace(/\$.*?!/g,"!").split("!").compact() + +#On concatène 'volume', 'issue' et 'pages.range' dans un seul champ +[assign] +path = ApilCollation +value = get("host.volume").concat(_.get(self,"host.issue")).concat(_.get(self,"host.pages.range")).join(" / ") + +#Récupère les fulltext d'unpaywall si le champs 'fulltexturl' (qui vient de conditor) est nul dans une colonne nommée ApilFullText (je ne sais pas comment on déclare le nom de la colonne dans ce cas précis) + +[assign] +path = fulltextUrl +value = get("fulltextUrl").castArray().compact() + +[swing] +test = get("fulltextUrl").isEmpty() + +[swing/assign] +path = fulltextUrl +value=get("enrichments.openAccess.unpaywall.oaLocations").filter(item=>item.hostType==="repository").map(item=>item.url) + +; [assign] +; path = value +; value = get('fulltextUrl').castArray().uniq() + +#Homogénéise les types de document +[assign] +path = ApilWsTypeDoc +value = get("originalGenre").trim() + +[expand] +path = ApilWsTypeDoc +size = 100 +cacheName = 04-homogenize-document-type-json + +[expand/URLConnect] +url = https://mapping-tools.services.istex.fr/v1/homogenize/documentType/json +timeout = 90003 +noerror = true + +# Si le WS renvoie un "n/a" +[swing] +test = get("ApilWsTypeDoc").isEqual("n/a") + +# On l'écrase avec la valeur de "originalGenre" +[swing/assign] +path = ApilWsTypeDoc +value = get("originalGenre").trim() + +# Homogénéise les sources +[assign] +path = ApilWsSource +value = get("host.title",_.get(self,"host.conference.name")).trim() + +# si les champs 'host.title' et 'host.conference.name' ne sont pas vides +[expand] +path = ApilWsSource +size = 100 +cacheName = 04-homogenize-source-json + +[expand/URLConnect] +url = https://mapping-tools.services.istex.fr/v1/homogenize/source/json +timeout = 90004 +noerror = true + +# si le champ "ApilWsSource" issu du WS est "n/a" +[swing] +test = get("ApilWsSource").isEqual("n/a") + +[swing/assign] +path = ApilWsSource +value = get("host.title",_.get(self,"host.conference.name")).trim() + +# Traitement des éditeurs +[assign] +path = ws.ApilRacineDoiPublisher +value = get("doi").split('/').filter(i => i.match(/^10./)).pop() + +[expand] +path = ws.ApilRacineDoiPublisher +size = 1 +cacheName = 04-api-crossref-prefixes-expand + +[expand/URLFetch] +target = value +url = fix('https://api.crossref.org/prefixes/').append(self.value) +json = true +timeout = 60000 +noerror = true +retries = 2 + +[expand/assign] +path = value +value = get('value.message.name', 'n/a') + +# Dans un champ temporaire, récupérer la valeur host.publisher si elle est présente, sinon récupérer celle du WS DOI. +[assign] +path = tmp.ApilWsPublisher +value = get("host.publisher",_.get(self,"ws.ApilRacineDoiPublisher")) + +# Homogénéise l'éditeur +[assign] +path = ApilWsPublisher +value = get("tmp.ApilWsPublisher").trim() + +[expand] +path = ApilWsPublisher +size = 100 +cacheName = 04-homogenize-publisher-json + +[expand/URLConnect] +url = https://mapping-tools.services.istex.fr/v1/homogenize/publisher/json +timeout = 90006 +noerror = true + +# Si host.publisher existe et que le ApilWsPublisher vaut n/a, +[swing] +test = has("host.publisher") +test = get("ApilWsPublisher").isEqual("n/a") + +# On l'écrase avec la valeur de host.publisher +[swing/assign] +path = ApilWsPublisher +value = get("host.publisher") + +# Enrichissements pays +[assign] +path = ws.libpostal +value = get("authors") \ + .flatMap("affiliations") \ + .map("address").uniq() \ + .map((address, id) => ({ \ + id, \ + value: address \ + })) + +[map] +path = ws.libpostal + +[map/expand] +path = value +size = 100 +cacheName = 04-address-expand + +[map/expand/URLConnect] +url = https://affiliations-tools.services.istex.fr/v1/addresses/parse +timeout = 90007 +noerror = true + +[map/expand/assign] +path = value.value.address +value = get('value.id') + +path = value.value.country +value = get('value.value.country').replace(/\W/g, ' ').trim() + +[map/expand/assign] +path = value +value = get('value.value') + +[map/expand/expand] +path = value.country +size = 10 +cacheName = 04-country-expand + +[map/expand/expand/URLConnect] +url = https://loterre-resolvers.services.istex.fr/v1/9SD/identify +timeout = 90008 + +[map/exchange] +value = get('value') + +# TODO: si champ state, on est aux États-Unis (United States of America) + +[assign] +path = ApilWsCodeISO +value = get("ws.libpostal").castArray().filter(Boolean) \ + .map(n => n.country?.cartographyCode) \ + .uniq().filter(Boolean) + +path = ApilWsCountry +value = get("ws.libpostal").castArray().filter(Boolean) \ + .map(n => n.country?.["prefLabel@en"]) \ + .uniq().filter(Boolean) + +# Suppression des champs non voulus +[exchange] +value = omit(['tmp']) diff --git a/data-workflow/v1/conditormetrie.ini b/data-workflow/v1/conditormetrie.ini new file mode 100644 index 0000000..73b42cb --- /dev/null +++ b/data-workflow/v1/conditormetrie.ini @@ -0,0 +1,65 @@ +# Entrypoint output format +mimeType = application/json + +# OpenAPI Documentation - JSON format (dot notation) +post.operationId = post-v1-conditormetrie +post.description = Enrichissements biliométriques sur un corpus Conditor +post.summary = Le résulat produit une liste de notices enrichies +post.tags.0 = data-workflow +post.requestBody.content.application/x-tar.schema.type = string +post.requestBody.content.application/x-tar.schema.format = binary +post.requestBody.required = true +post.responses.default.description = Informations permettant de récupérer les données le moment venu +post.parameters.0.description = Indenter le JSON résultant +post.parameters.0.in = query +post.parameters.0.name = indent +post.parameters.0.schema.type = boolean +post.parameters.1.description = URL pour signaler que le traitement est terminé +post.parameters.1.in = header +post.parameters.1.name = X-Webhook-Success +post.parameters.1.schema.type = string +post.parameters.1.schema.format = uri +post.parameters.1.required = false +post.parameters.2.description = URL pour signaler que le traitement a échoué +post.parameters.2.in = header +post.parameters.2.name = X-Webhook-Failure +post.parameters.2.schema.type = string +post.parameters.2.schema.format = uri +post.parameters.2.required = false + +[env] +path = language +value = en + +[use] +plugin = basics +plugin = analytics + +# Step 1 (générique): Charger le fichier corpus +[delegate] +file = charger.cfg + +# Step 2 (générique): Traiter de manière asynchnore les items reçus +[tracer] +[fork] +standalone = true +logger = logger.cfg + +# Step 2.1 (spécifique): Lancer un calcul sur tous les items reçus +[fork/delegate] +file = conditormetrie.cfg + +# Step 2.2 (générique): Enregister le résulat et signaler que le traitment est fini +[fork/delegate] +file = recorder.cfg + +# Step 3 : Renvoyer immédiatement un seul élément indiquant comment récupérer le résulat quand il sera prêt +[shift] +[replace] +path = id +value = conditormetrie +path = value +value = env('identifier') + +[JSONString] +indent = env('indent')