From 4792c49e50b600d5645eda99b1492b48c6fb1b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6der?= Date: Tue, 10 Jan 2023 13:45:48 +0100 Subject: [PATCH] v0.1.0 --- build/lib/pybibget/__init__.py | 119 ++++++ build/lib/pybibget/bibentry.py | 546 +++++++++++++++++++++++++++ dist/pybibget-0.0.2-py3-none-any.whl | Bin 9154 -> 0 bytes dist/pybibget-0.0.2.tar.gz | Bin 8780 -> 0 bytes dist/pybibget-0.1.0-py3-none-any.whl | Bin 0 -> 12645 bytes dist/pybibget-0.1.0.tar.gz | Bin 0 -> 14338 bytes pybibget.egg-info/PKG-INFO | 6 +- pybibget.egg-info/requires.txt | 3 + requirements.txt | 5 +- setup.cfg | 2 +- 10 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 build/lib/pybibget/__init__.py create mode 100644 build/lib/pybibget/bibentry.py delete mode 100644 dist/pybibget-0.0.2-py3-none-any.whl delete mode 100644 dist/pybibget-0.0.2.tar.gz create mode 100644 dist/pybibget-0.1.0-py3-none-any.whl create mode 100644 dist/pybibget-0.1.0.tar.gz diff --git a/build/lib/pybibget/__init__.py b/build/lib/pybibget/__init__.py new file mode 100644 index 0000000..b94fd98 --- /dev/null +++ b/build/lib/pybibget/__init__.py @@ -0,0 +1,119 @@ +import argparse +import asyncio +import re +import sys +import logging as log +log.getLogger('asyncio').setLevel(log.WARNING) +from pybibget.bibentry import Bibget +from pybtex.database import parse_string + +def add_optional_args(parser): + parser.add_argument('-v', '--verbose', action='store_true', help='verbose output') + parser.add_argument('-d', '--debug', action='store_true', help='debug output') + parser.add_argument('--skip-doi-msc', action='store_true', help='skip MathSciNet lookup for DOIs') + + +def pybibget(): + """ + Reads citation keys from command line and calls get_citations() + """ + parser = argparse.ArgumentParser(prog='pybibget', description='Command line utility to automatically retrieve BibTeX citations from MathSciNet, arXiv and PubMed') + parser.add_argument('keys', type=str, metavar='citekeys', nargs='*', help='MathSciNet (MRxxxxx), arXiv (xxxx.xxxxx), PubMed (PMID:xxxxxxxx) or DOI (10.xxx/xxxxx) citation keys (separated by spaces)') + parser.add_argument('-w', action='store', dest='file_name', help='Append output to file (default: write output to stdout)') + add_optional_args(parser) + args = parser.parse_args() + kwargs = {'file': args.file_name } + if args.debug: + kwargs['verbose'] = log.DEBUG + elif args.verbose: + kwargs['verbose'] = log.INFO + if not args.keys: + parser.print_help() + exit(1) + + get_citations(args.keys, **kwargs) + + +def pybibparse(): + """ + Reads latex file name from the command line, parses the .blg file and calls get_citations() + """ + parser = argparse.ArgumentParser(prog='pybibget', description='Command line utility to automatically retrieve BibTeX citations from MathSciNet, arXiv and PubMed') + parser.add_argument('file_name', type=str, metavar='tex_file(.tex)', nargs=1, help='LaTeX file to be parsed for missing citations') + parser.add_argument('-w', action='store', dest='write', metavar="output.bib", nargs='?', const=" ", help='Append output to file (default: write output to stdout). A bib file name can be specified via "-w file_name.bib" but usually the .bib file is found automatically.') + add_optional_args(parser) + args = parser.parse_args() + if not args.file_name: + parser.print_help() + sys.exit() + if args.file_name[0].endswith(".tex"): + base_file_name = args.file_name[0][:-4] + else: + base_file_name = args.file_name[0] + + with open(base_file_name+".blg") as file: + blg_file = file.read() + missing_cites = re.findall(r"I didn't find a database entry for '([A-Za-z0-9\.\-_ :\/]*)'", blg_file) \ + + re.findall(r'I didn\'t find a database entry for "([A-Za-z0-9\.\-_ :\/]*)"', blg_file) + bib_file_names = re.findall(r"Found BibTeX data source '([A-Za-z0-9.\-_\/]*)'", blg_file) \ + + re.findall(r"Looking for bibtex file '([A-Za-z0-9.\-_\/]*)'", blg_file) \ + + re.findall(r'Database file #\d: ([A-Za-z0-9.\-_\/]*)\n', blg_file) \ + + re.findall(r'I couldn\'t open database file ([A-Za-z0-9.\-_\/]*)\n', blg_file) + + if missing_cites: + kwargs = {} + if args.debug: + kwargs['verbose'] = log.DEBUG + elif args.verbose: + kwargs['verbose'] = log.INFO + if args.write: + if args.write == " " and not bib_file_names: + print("No .bib file found. Please specify the .bib file via '-w file_name.bib'") + sys.exit() + kwargs['file'] = bib_file_names[0] if args.write == " " else args.write + get_citations(missing_cites, **kwargs) + else: + print("No missing citations found. Make sure that biber/bibtex is run successfully before running pybibget.") + +def pybibupdate(): + parser = argparse.ArgumentParser(prog='pybibget', description='Command line utility to update BibTeX citations from MathSciNet and Scopus') + parser.add_argument('file_name', type=str, metavar='bib_file(.bib)', help='bib file to be parsed for citations') + args = parser.parse_args() + if not args.file_name: + parser.print_help() + sys.exit() + if not args.file_name.endswith(".bib"): + args.file_name += ".bib" + with open(args.file_name) as file: + bib_file = file.read() + bibliography = parse_string(bib_file, 'bibtex').entries + + log.basicConfig(format="%(levelname)s: %(message)s", level=log.WARNING) + + bibget = Bibget(mathscinet=True) + updated_bibliography = asyncio.run(bibget.update_all(bibliography)) + with open(args.file_name, 'w') as file: + file.write(updated_bibliography.to_string('bibtex')) + print(f"Wrote the updated bibliography to {args.file_name}.") + +def get_citations(keys, verbose=log.WARNING, file=None): + """ + Retrieves BibTeX entries for given citation keys and writes them to file or stdout + """ + log.basicConfig(format="%(levelname)s: %(message)s", level=verbose) + + bibget = Bibget(mathscinet=True) + bib_data = asyncio.run(bibget.citations(keys)) + number_of_entries = len(bib_data.entries) + bib_data = bib_data.to_string('bibtex') + if file: + with open(file, 'a') as obj: + obj.write(bib_data) + print(f"Successfully appended {number_of_entries} BibTeX entries to {file}.") + else: + print("\n"+bib_data) + return number_of_entries + + +if __name__ == '__main__': + sys.exit(pybibget()) diff --git a/build/lib/pybibget/bibentry.py b/build/lib/pybibget/bibentry.py new file mode 100644 index 0000000..fa58f4f --- /dev/null +++ b/build/lib/pybibget/bibentry.py @@ -0,0 +1,546 @@ +from urllib import parse +import re +import asyncio +import logging as log +from lxml import html, etree +import os +import textwrap +import json +import os.path +import httpx +from appdirs import AppDirs +from itertools import zip_longest +from aiolimiter import AsyncLimiter +from pybtex.database import Entry, Person, BibliographyData, parse_string +from pylatexenc.latexencode import unicode_to_latex +from pylatexenc.latex2text import LatexNodes2Text +ATOM = 'http://www.w3.org/2005/Atom' +ARXIV = 'http://arxiv.org/schemas/atom' +RE_MSC = r'MR\d{4,10}' +RE_PMID = r'PMID:\d{4,10}' +RE_DOI = r'10\.\d{4,9}\/[-._;()\/:A-Za-z0-9]+' +RE_ARXIV_OLD = r'\b[a-zA-Z\-\.]{2,10}\/\d{7}(?:v\d)?\b' +RE_ARXIV_NEW = r'\b\d{4}\.\d{4,5}(?:v\d)?\b' + +def column_print(str1,str2,maxwidth=80): + width = min(os.get_terminal_size().columns//2 - 3,maxwidth) + lines1, lines2 = str1.splitlines(), str2.splitlines() + print("-"*(width*2+5)) + for line1, line2 in zip_longest(lines1, lines2, fillvalue=''): + line1 = textwrap.shorten(line1, width=width, placeholder="...") + line2 = textwrap.shorten(line2, width=width, placeholder="...") + print(f"{line1:<{width}} -> {line2:<{width}}") + print("-"*(width*2+5)) + +def nested_dict(dic, key): + for ke, it in dic.items(): + if ke == key: + yield it + yield from [] if not isinstance(it, dict) else nested_dict(it, key) + + +def msg_looking(key, service): + return f"Looking for {key} on {service}" + + +def msg_found(key, service, continuation=None): + ret_str = f"{key} found on {service}" + if continuation: + ret_str += f". {continuation}" + return ret_str + + +def msg_not_found(key, service, continuation=None, reason=None): + ret_str = f"{key} not found on {service}" + if reason: + ret_str += f" ({reason})" + if continuation: + ret_str += f". {continuation}" + return ret_str + + +def create_bibentry(entry_type,sanitize=True,author=None,key=None,**kwargs): + """ + Create a bibentry from a dictionary. + + Parameters + ---------- + entry_type : str + The entry type. + **kwargs : dict + The entry fields. + + Returns + --------- + bibentry : pybtex.database.Entry + """ + bibentry = Entry(entry_type) + if author: + bibentry.persons['author'] = [Person(sanitize_string(str(person))) for person in author] + for key, value in kwargs.items(): + bibentry.fields[key] = sanitize_string(value, title = key in ['title','booktitle']) if sanitize and key in ["title","author","journal","booktitle","publisher"] else value + bibentry.key = "" if key is None else key + return bibentry + + +class Bibget(): + def __init__(self, mathscinet=True): + self.mathscinet = mathscinet + self.config_file = os.path.join(AppDirs("pybibget", "pybibget").user_data_dir, "config.json") + if os.path.isfile(self.config_file): + self.rate_limit = AsyncLimiter(int(json.load(open(self.config_file))["scopus_rate_limit"]), 1) + self.api_key = json.load(open(self.config_file))["scopus_api_key"] + self.scopus = len(self.api_key) > 0 + else: + os.makedirs(os.path.dirname(self.config_file), exist_ok=True) + with open(self.config_file, "w+") as file: + file.write(json.dumps({"scopus_api_key": "", "scopus_rate_limit": 6})) + self.scopus = False + + def setup_scopus(self,message): + self.api_key = input(message) + with open(self.config_file, "w+") as file: + file.write(json.dumps({"scopus_api_key": self.api_key, "scopus_rate_limit": 6})) + self.scopus = len(self.api_key) > 0 + + async def citations(self,keys): + if any(map(lambda key: re.match(RE_DOI, key) or re.match(RE_PMID, key), keys)) and not self.scopus: + self.setup_scopus(f"Scopus can result in more reliable results than crossref.org, but requires an API key. If you want to use Scopus, please register at https://dev.elsevier.com/ and enter your API key below. If you don't want to use Scopus, just press [enter]. You can also enter your API key later in {self.config_file}\n") + bibentries = await asyncio.gather(*[self.citation(key) for key in keys],return_exceptions=True) + bib_data = BibliographyData() + for entry_key in bibentries: + if isinstance(entry_key, Exception): + log.error(entry_key) + else: + entry,key = entry_key + bib_data.entries[key] = entry + return bib_data + + async def citation(self,key): + """ + Get a bibentry from a citation key. + + Parameters + ---------- + key : str + The citation key. + + Returns + --------- + bibentry : pybtex.database.Entry + + Raises + ---------- + ValueError + If the citation key is invalid or the entry is not found. + """ + if re.match(RE_MSC, key): + log.info(msg_looking(key, "MathSciNet")) + return (await self.citation_msc(mrkey=key), key) + elif re.match(RE_PMID, key): + if self.scopus: + log.info(msg_looking(key, "Scopus")) + try: + return (await self.citation_scopus(pmid=key), key) + except Exception as exc: + log.warning(exc) + log.info(msg_looking(key, "PubMed")) + return (await self.citation_pubmed(key), key) + elif re.match(RE_ARXIV_OLD, key) or re.match(RE_ARXIV_NEW, key): + log.info(msg_looking(key, "arXiv")) + return (await self.citation_arxiv(key), key) + elif re.match(RE_DOI, key): + if self.mathscinet: + try: + log.info(msg_looking(key, "MathSciNet")) + return (await self.citation_msc(doi=key), key) + except Exception as exc: + log.warning(exc) + if self.scopus: + try: + log.info(msg_looking(key, "Scopus")) + return (await self.citation_scopus(doi=key), key) + except Exception as exc: + log.warning(exc) + log.info(msg_looking(key, "Crossref")) + return (await self.citation_crossref(key), key) + else: + raise ValueError(f"{key} = Invalid citation key") + + async def citation_msc(self,mrkey=None, doi=None): + """ + Get a bibentry from a MathSciNet citation key or DOI. + + Parameters + ---------- + mrkey : str, optional + The MathSciNet citation key, must start with MRxxx. The default is None. + doi : str, optional + The DOI, must start with 10.xxx/xxx. The default is None. + + Returns + --------- + bibentry : pybtex.database.Entry + + Raises + ---------- + ValueError + If the neither mrnumber nor doi are provided, or the entry is not found. + """ + if not mrkey and not doi: + raise ValueError("Either MRnumber or doi must be specified.") + base_url = "https://mathscinet.ams.org/mathscinet/search/publications.html?fmt=bibtex&pg1=" + url = base_url + "MR&s1=" + mrkey[2:] if mrkey else base_url + "DOI&s1=" + doi + async with httpx.AsyncClient() as client: + page = await client.get(url) + try: + tree = html.fromstring(page.text) + bibstrings = tree.xpath('//pre/text()') + bibstr = bibstrings[0] + if len(bibstrings)>1: + log.warn(f"MathSciNet returned more than one entry for {mrkey if mrkey else doi}. Using the first one but this may be wrong.") + entries = parse_string(bibstr, 'bibtex').entries + log.info(msg_found(mrkey if mrkey else doi, "MathSciNet")) + return list(entries.values())[0] + except: + reason = str(page.status_code) + if page.status_code == 200: + reason += "; " + tree.xpath('//head/title/text()')[0].replace("\n", "") + raise ValueError(msg_not_found(mrkey if mrkey else doi, "MathSciNet", reason=reason, continuation="Trying crossref.org" if doi else None)) + + async def citation_crossref(self,doi): + """ + Get a bibentry from a DOI. + + Parameters + ---------- + doi : str + The DOI, must start with 10.xxx/xxx + + Returns + --------- + bibentry : pybtex.database.Entry + + Raises + ---------- + ValueError + If the entry is not found. + """ + url = "https://api.crossref.org/v1/works/" + doi + "/transform" + headers = {'Accept': 'application/x-bibtex; charset=utf-8'} + async with httpx.AsyncClient() as client: + page = await client.get(url, headers=headers, follow_redirects=True) + try: + entries = parse_string(page.text, 'bibtex').entries + entry = sanitize_entry(list(entries.values())[0]) + log.info(msg_found(doi, "crossref.org")) + return entry + except Exception as exc: + log.info(exc) + reason = page.status_code + entries = parse_string(page.text, 'bibtex').entries + entry = list(entries.values())[0] + raise ValueError(msg_not_found(doi, "crossref.org", reason=reason)) + + async def citation_scopus(self,doi=None, pmid=None): + """ + Get a bibentry from Scoups via DOI or PMID. + + Parameters + ---------- + doi : str, optional + The DOI, must start with 10.xxx/xxx. The default is None. + pmid : str, optional + The PMID, must start with PMID:xxxx. The default is None. + + Returns + --------- + bibentry : pybtex.database.Entry + + Raises + ---------- + ValueError + If the entry is not found. + """ + + if doi: + url = "https://api.elsevier.com/content/abstract/doi/" + doi + key = doi + elif pmid: + url = "https://api.elsevier.com/content/abstract/pubmed_id/" + pmid[5:] + key = pmid + else: + raise ValueError("Either doi or PMID must be specified.") + url += "?view=FULL&apiKey=" + self.api_key + headers = {'Accept': 'application/json; charset=utf-8'} + async with httpx.AsyncClient() as client: + async with self.rate_limit: + page = await client.get(url, headers=headers, follow_redirects=True) + if log.root.level == log.DEBUG: + with open("test"+key.replace("/","-")+".json","w+") as f: + f.writelines(page.text) + try: + results = page.json() + results_bib = results['abstracts-retrieval-response']['item']['bibrecord']['head'] + fields = {} + citation_type = results_bib['source']['@type'] + fields['title'] = results_bib['citation-title'] + author_flat = [] + author_groups = results_bib['author-group'] + if type(author_groups) is not list: + author_groups = [author_groups] + for author_group in author_groups: + authors = author_group['author'] + if type(authors) is not list: + authors = [authors] + for author in authors: + author_flat.append(f"{author['preferred-name']['ce:surname']}, {author['preferred-name']['ce:given-name']}") + fields['author'] = [Person(author) for author in author_flat] + try: + fields['year'] = list(results_bib['source']['publicationyear'].values())[0] + except Exception as e: + log.warning(str(e)) + if pmid: + doi = results['abstracts-retrieval-response']['coredata']['prism:doi'] + fields['doi'] = doi + fields['url'] = "https://doi.org/" + doi + if citation_type == 'j': + citation_type = 'article' + fields['journal'] = results_bib['source']['sourcetitle-abbrev'] if 'sourcetitle-abbrev' in results_bib['source'] else results_bib['source']['sourcetitle'] + try: + fields['volume'] = results_bib['source']['volisspag']['voliss']['@volume'] + fields['number'] = results_bib['source']['volisspag']['voliss']['@issue'] + except KeyError as e: + log.info(f"{key}: No volume or issue found on Scopus.") + try: + fields['pages'] = '--'.join(results_bib['source']['volisspag']['pagerange'].values()) + except KeyError as e: + log.info(f"{key}: No page range found on Scopus.") + elif citation_type in ['p','k']: + citation_type = 'inproceedings' if citation_type == 'p' else 'incollection' + fields['publisher'] = results_bib['source']['publisher']['publishername'] + fields['booktitle'] = results_bib['source']['sourcetitle-abbrev'] + elif citation_type == 'b': + citation_type = 'book' + fields['publisher'] = results_bib['source']['publisher']['publishername'] + fields['title'] = results_bib['source']['sourcetitle'] + try: + fields['pages'] = results_bib['source']['volisspag']['pagerange']['@last'] + except: + log.warning(f"{key}: Number of pages not found on Scopus.") + else: + raise ValueError("Unknown citation type: " + citation_type) + if pmid: + fields['pmid'] = pmid[5:] + else: + try: + fields['pmid'] = results['abstracts-retrieval-response']['coredata']['pubmed-id'] + except: + pass + bibentry = create_bibentry(citation_type ,**fields) + log.info(msg_found(key, "Scopus")) + return(bibentry) + except Exception as exc: + reason = str(page.status_code) + if page.status_code == 401: + reason += "; Error 401 suggests that either the supplied API key is wrong, or requires a VPN connection" + raise ValueError(msg_not_found(key, "Scopus", reason=reason)) from exc + + async def citation_arxiv(self,arxiv_key): + """ + Get a bibentry from an arXiv identifier. + + Parameters + ---------- + arxiv_key : str + The arXiv identifier. Can be either the old (hep-th/xxxxx) or the new (2201.xxxx) format. + + Returns + --------- + bibentry : pybtex.database.Entry + + Raises + ---------- + ValueError + If the entry is not found. + """ + url = "http://export.arxiv.org/api/query?id_list=" + arxiv_key + async with httpx.AsyncClient() as client: + page = await client.get(url, follow_redirects=True) + try: + tree = etree.fromstring(page.text.encode()) + if doi := tree.xpath("//a:entry/b:doi", namespaces={'a': ATOM, 'b': ARXIV}): + log.info(msg_found(arxiv_key, "arXiv", continuation=f"Detected {doi[0].text}")) + bibentry, _ = await self.citation(doi[0].text) + elif title := tree.xpath("//a:entry/a:title", namespaces={'a': ATOM}): + fields = [("title", title[0].text)] + if journal := tree.xpath("//a:entry/a:journal", namespaces={'a': ATOM}): + fields += [("note", journal[0].text)] + else: + fields += [("note", "Preprint")] + fields += [("year", tree.xpath("//a:entry/a:published",namespaces={'a': ATOM})[0].text[:4])] + bibentry = Entry("unpublished", fields=fields) + bibentry.persons["author"] = [Person(author.text) for author in tree.xpath("//a:entry/a:author/a:name", namespaces={'a': ATOM})] + bibentry = sanitize_entry(bibentry) + log.info(msg_found(arxiv_key, "arXiv", continuation="No DOI found, using title and authors")) + else: + raise ValueError(f"empty arXiv entry returned") + bibentry.fields["eprint"] = arxiv_key + bibentry.fields["archiveprefix"] = "arXiv" + return bibentry + except Exception as exc: + reason = str(page.status_code) + "; " + exc.args[0] + raise ValueError(msg_not_found(arxiv_key, "arXiv", reason=reason)) + + async def citation_pubmed(self,pmid): + doi = await self.get_doi(pmid=pmid) + log.info(msg_looking(doi, "Crossref (forwarded from PubMed)")) + if self.scopus: + try: + result = await self.citation_scopus(doi=doi) + result.fields['pmid'] = pmid[5:] + return result + except Exception as exc: + log.warning(msg_not_found(doi, "Scopus", reason=str(exc), continuation="Trying crossref.org")) + result = await self.citation_crossref(doi=doi) + result.fields['pmid'] = pmid[5:] + return result + + async def get_doi(self,pmid=None): + """ + Get a DOI from a PubMed ID. + + Parameters + ---------- + pmid : str, optional + The PubMed ID. The default is None. + + Returns + --------- + doi : str + + Raises + ---------- + ValueError + If the PubMed ID is invalid or the DOI is not found. + """ + if not pmid: + raise ValueError("No PubMed ID provided.") + if not re.match(RE_PMID, pmid): + raise ValueError("Invalid PubMed ID.") + url = f"https://pubmed.ncbi.nlm.nih.gov/{pmid[5:]}/?format=pubmed" + async with httpx.AsyncClient() as client: + page = await client.get(url, follow_redirects=True) + try: + doi = re.search("AID - (10\.\d{4,9}\/[-._;()\/:A-Za-z0-9]+) \[doi\]", page.text) + return doi.group(1) + except Exception as exc: + raise ValueError(f"DOI not found for PubMed ID {pmid}!") from exc + + async def prompt(self,entry,candidate=None,prefix=None): + prompt = prefix + "Press" + if candidate: + prompt += " 'y' to replace," + ans = input(prompt + " [enter] to leave the old citation, or enter a DOI for a custom replacement: ") + if ans == "": + return entry + elif ans == "y" and candidate: + return candidate + else: + return await self.update(entry, candidate_doi=ans) + + async def update(self,entry,candidate=None,candidate_doi=None): + title = LatexNodes2Text(math_mode='verbatim').latex_to_text(entry.fields["title"]) + if 'mrnumber' in entry.fields: + log.info(f"MR{entry.fields['mrnumber']} ({title}): Skipping MR entry") + return entry + if candidate: + candidate.key = entry.key + if 'eprint' in entry.fields: + candidate.fields['eprint'] = entry.fields['eprint'] + candidate.fields['archiveprefix'] = entry.fields['archiveprefix'] + if 'pmid' in entry.fields and 'pmid' not in candidate.fields: + candidate.fields['pmid'] = entry.fields['pmid'] + print("Found the following replacement:") + column_print(entry.to_string('bibtex'), candidate.to_string('bibtex')) + return await self.prompt(entry,candidate=candidate,prefix="Replace old citation? ") + if candidate_doi: + if not re.match(RE_DOI, candidate_doi): + return await self.prompt(entry,prefix="Invalid DOI! ") + try: + candidate,_ = await self.citation(candidate_doi) + return await self.update(entry, candidate=candidate) + except Exception as exc: + print(f"{candidate_doi}: No citation found; leaving old citation") + print(exc) + return entry + if 'doi' in entry.fields and 'mrnumber' not in entry.fields: + try: + updated_entry = await self.citation_msc(doi=entry.fields['doi']) + return await self.update(entry,updated_entry) + except: + log.info(f"{entry.fields['doi']} ({title}): Not found on MathSciNet, leaving old citation") + return entry + try: + log.info(f'"{title}": Checking for DOI on Scopus') + updated_entry = await self.lookup_scopus(title) + return await self.update(entry, candidate=updated_entry) + except Exception as exc: + log.debug(f'"{title}": {str(exc)}') + return await self.prompt(entry, prefix=f'"{title}": No entry found on Scopus. ') + + async def update_all(self,bibliography): + while not self.scopus: + self.setup_scopus(f"Scopus is required for 'pybibupdate' and requires an API key. Please register at https://dev.elsevier.com/ and enter your API key below.\n") + + updated_bibliography = BibliographyData() + for key,entry in bibliography.items(): + updated_bibliography.entries[key] = await self.update(entry) + + return updated_bibliography + + async def lookup_scopus(self,title): + url = "https://api.elsevier.com/content/search/scopus?query=TITLE%28%22" + parse.quote(title,safe="") + "%22%29" + url += "&apiKey=" + self.api_key + headers = {'Accept': 'application/json; charset=utf-8'} + async with httpx.AsyncClient() as client: + async with self.rate_limit: + page = await client.get(url, headers=headers, follow_redirects=True) + try: + results = page.json() + doi = results['search-results']['entry'][0]['prism:doi'] + try: + return await self.citation_msc(doi) + except Exception as exc: + return await self.citation_scopus(doi=doi) + except Exception as exc: + raise ValueError(msg_not_found(title, "Scopus", reason=str(exc))) + + +def sanitize_entry(entry): + """ + Sanitize a bibentry. Protects title capitalization, removes newlines and tabs, and converts unicode characters to LaTeX. + """ + if "title" in entry.fields: + entry.fields["title"] = sanitize_string(entry.fields["TITLE"],title=True) + if "AUTHOR" in entry.persons: + for author in entry.persons["AUTHOR"]: + author.first_names = [sanitize_string(name) for name in author.first_names] + author.last_names = [sanitize_string(name) for name in author.last_names] + if "month" in entry.fields: + entry.fields.pop("month") + if "url" in entry.fields: + entry.fields["url"] = parse.unquote(entry.fields["url"]) + return entry + + +def sanitize_string(string, title=False): + """ + Sanitize a string: Removes newlines and tabs, and converts unicode characters to LaTeX. If title is True, also protects title capitalization. + """ + string = string.replace("\n", "").replace("\t", "").replace("\\\\","\\") + string = LatexNodes2Text(math_mode='verbatim').latex_to_text(string) + string = unicode_to_latex(string,non_ascii_only=True) + if title: + string = re.sub(r'\b([A-Z].*?)\b',r'{\1}',string) + return string \ No newline at end of file diff --git a/dist/pybibget-0.0.2-py3-none-any.whl b/dist/pybibget-0.0.2-py3-none-any.whl deleted file mode 100644 index 6b991d79e2d5a47728681bfb1265c87d391f7bef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9154 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 diff --git a/dist/pybibget-0.1.0-py3-none-any.whl b/dist/pybibget-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..dcd5719b6397df41f91e133a8e7d147168c4b789 GIT binary patch literal 12645 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 SH%0evA%2Lkb}$QYu>S*GI9tO2 literal 0 HcmV?d00001 diff --git a/pybibget.egg-info/PKG-INFO b/pybibget.egg-info/PKG-INFO index ccb8d05..0386160 100644 --- a/pybibget.egg-info/PKG-INFO +++ b/pybibget.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pybibget -Version: 0.0.2 +Version: 0.1.0 Summary: Command line utility to automatically retrieve BibTeX citations from MathSciNet, arXiv, PubMed and doi.org Home-page: https://github.com/wirhabenzeit/pybibget Author: Dominik Schröder @@ -172,6 +172,10 @@ With the option `-w [file_name]` the obtained citations are automatically append Succesfully appended 2 BibTeX entries to bibliography.bib ``` +### Updating existing bibliographies + +`pybibupdate [file.bib]` scans an existing `.bib`-file and searches for entries with updated information on [Scopus](https://www.scopus.com/). This functionality requires an API-key which can be obtained from [https://dev.elsevier.com](https://dev.elsevier.com) + ## Data Sources ### MathSciNet diff --git a/pybibget.egg-info/requires.txt b/pybibget.egg-info/requires.txt index 1a18274..7679071 100644 --- a/pybibget.egg-info/requires.txt +++ b/pybibget.egg-info/requires.txt @@ -2,3 +2,6 @@ requests>=2.28.1 pybtex>=0.24.0 lxml>=4.9.2 pylatexenc>=1.3 +aiolimiter>=1.0.0 +appdirs>=1.0.0 +httpx>=0.21.0 diff --git a/requirements.txt b/requirements.txt index 48801d5..c757d5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ requests >= 2.28.1 pybtex >= 0.24.0 lxml >= 4.9.2 -pylatexenc >= 1.3 \ No newline at end of file +pylatexenc >= 1.3 +aiolimiter >= 1.0.0 +appdirs >= 1.0.0 +httpx >= 0.21.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 0fd2f40..6bf36f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pybibget -version = 0.0.2 +version = 0.1.0 author = Dominik Schröder author_email = dschroeder@ethz.ch url = https://github.com/wirhabenzeit/pybibget