flm-citations 0.2.4__tar.gz
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.
- flm_citations-0.2.4/LICENSE.txt +21 -0
- flm_citations-0.2.4/PKG-INFO +19 -0
- flm_citations-0.2.4/flm_citations/__init__.py +25 -0
- flm_citations-0.2.4/flm_citations/_cslformatter.py +72 -0
- flm_citations-0.2.4/flm_citations/american-physical-society-et-al--patched.csl +169 -0
- flm_citations-0.2.4/flm_citations/citesources/__init__.py +1 -0
- flm_citations-0.2.4/flm_citations/citesources/arxiv.py +195 -0
- flm_citations-0.2.4/flm_citations/citesources/base.py +145 -0
- flm_citations-0.2.4/flm_citations/citesources/bibliographyfile.py +122 -0
- flm_citations-0.2.4/flm_citations/citesources/doi.py +92 -0
- flm_citations-0.2.4/flm_citations/citesources/manual.py +43 -0
- flm_citations-0.2.4/flm_citations/feature.py +436 -0
- flm_citations-0.2.4/flm_citations/flmcitationsscanner.py +32 -0
- flm_citations-0.2.4/pyproject.toml +21 -0
- flm_citations-0.2.4/setup.py +35 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Philippe Faist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flm-citations
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Support for citations in FLM (see flm package)
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Philippe Faist
|
|
7
|
+
Requires-Python: >=3.8,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
15
|
+
Requires-Dist: arxiv (>=1.4.2,<2.0.0)
|
|
16
|
+
Requires-Dist: backoff (>=2.1.2,<3.0.0)
|
|
17
|
+
Requires-Dist: citeproc-py (>=0.6.0,<0.7.0)
|
|
18
|
+
Requires-Dist: flm-core (>=0.3.0a10)
|
|
19
|
+
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
### FIXME: Merge feature config of imported configs don't work yet ... :/
|
|
4
|
+
### Need to fix this in flm.main.run(). Probably all $import's should be
|
|
5
|
+
### processed first, before merging anything.
|
|
6
|
+
|
|
7
|
+
flm_default_import_config = {
|
|
8
|
+
'flm': {
|
|
9
|
+
'features': {
|
|
10
|
+
'flm_citations': {},
|
|
11
|
+
# {
|
|
12
|
+
# 'sources': [
|
|
13
|
+
# {
|
|
14
|
+
# '$defaults': True,
|
|
15
|
+
# },
|
|
16
|
+
# ],
|
|
17
|
+
# },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def FeatureClass(*args, **kwargs):
|
|
24
|
+
from .feature import FeatureClass as _FeatureClass
|
|
25
|
+
return _FeatureClass(*args, **kwargs)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
_escape_chars = {
|
|
4
|
+
'\\': r'\textbackslash',
|
|
5
|
+
'%': r'\%',
|
|
6
|
+
'#': r'\#',
|
|
7
|
+
'&': r'\&',
|
|
8
|
+
'$': r'\$',
|
|
9
|
+
'{': r'\{',
|
|
10
|
+
'}': r'\}',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def _multiple_replace(s, rep_dict):
|
|
14
|
+
# https://stackoverflow.com/a/15448887/1694896
|
|
15
|
+
pattern = re.compile("|".join([
|
|
16
|
+
re.escape(k)
|
|
17
|
+
for k in sorted(rep_dict.keys(),key=len,reverse=True)
|
|
18
|
+
]), flags=re.DOTALL)
|
|
19
|
+
return pattern.sub(lambda x: rep_dict[x.group(0)], s)
|
|
20
|
+
|
|
21
|
+
def preformat(text):
|
|
22
|
+
# if isinstance(text, list): # ???
|
|
23
|
+
# return [preformat(z) for z in text]
|
|
24
|
+
text = str(text)
|
|
25
|
+
|
|
26
|
+
s = _multiple_replace(text, _escape_chars)
|
|
27
|
+
|
|
28
|
+
return s
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FLMCommandWrapper(str):
|
|
32
|
+
cmd = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _wrap(cls, text):
|
|
36
|
+
text = preformat(text)
|
|
37
|
+
return f'\\{cls.cmd}{{{text}}}'
|
|
38
|
+
|
|
39
|
+
def __new__(cls, text):
|
|
40
|
+
return super(FLMCommandWrapper, cls).__new__(cls, cls._wrap(text))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Italic(FLMCommandWrapper):
|
|
44
|
+
cmd = 'textit'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Oblique(Italic):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Bold(FLMCommandWrapper):
|
|
52
|
+
cmd = 'textbf'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Light(FLMCommandWrapper):
|
|
56
|
+
cmd = 'textit' # italic instead
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Underline(FLMCommandWrapper):
|
|
60
|
+
cmd = 'textit' # italic instead
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Superscript(FLMCommandWrapper):
|
|
64
|
+
cmd = 'textit' # italic instead
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Subscript(FLMCommandWrapper):
|
|
68
|
+
cmd = 'textit' # italic instead
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SmallCaps(FLMCommandWrapper):
|
|
72
|
+
cmd = 'textit' # italic instead
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only" default-locale="en-US">
|
|
3
|
+
<info>
|
|
4
|
+
<title>American Physical Society - et al. (if more than 3 authors)</title>
|
|
5
|
+
<title-short>APS-et-al</title-short>
|
|
6
|
+
<id>http://www.zotero.org/styles/american-physical-society-et-al</id>
|
|
7
|
+
<link href="http://www.zotero.org/styles/american-physical-society-et-al" rel="self"/>
|
|
8
|
+
<link href="http://www.zotero.org/styles/american-physics-society" rel="template"/>
|
|
9
|
+
<link href="https://cdn.journals.aps.org/files/styleguide-pr.pdf" rel="documentation"/>
|
|
10
|
+
<author>
|
|
11
|
+
<name>Jonas Lähnemann</name>
|
|
12
|
+
<email>jonas@pdi-berlin.de</email>
|
|
13
|
+
<name>Philippe Faist</name><!-- Patched (Feb 2023) -->
|
|
14
|
+
</author>
|
|
15
|
+
<category citation-format="numeric"/>
|
|
16
|
+
<category field="physics"/>
|
|
17
|
+
<summary>Short variant (first author et al., if more than 3 authors) of the American Physical Society (APS) citation style. Volume is bold and year comes last in normal brackets. Patched by Philippe Faist in Feb 2023.</summary>
|
|
18
|
+
<updated>2020-10-01T22:47:43+00:00</updated>
|
|
19
|
+
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
|
20
|
+
</info>
|
|
21
|
+
<macro name="author">
|
|
22
|
+
<names variable="author">
|
|
23
|
+
<name delimiter=", " initialize-with=". " and="text"/>
|
|
24
|
+
<label form="long" prefix=", " suffix=" "/>
|
|
25
|
+
<!-- <et-al font-style="italic"/> -->
|
|
26
|
+
<substitute>
|
|
27
|
+
<names variable="editor"/>
|
|
28
|
+
<names variable="translator"/>
|
|
29
|
+
</substitute>
|
|
30
|
+
</names>
|
|
31
|
+
</macro>
|
|
32
|
+
<macro name="editor">
|
|
33
|
+
<names variable="editor">
|
|
34
|
+
<label form="verb" suffix=" "/>
|
|
35
|
+
<name delimiter=", " initialize-with=". " and="text"/>
|
|
36
|
+
<!-- <et-al font-style="italic"/> -->
|
|
37
|
+
</names>
|
|
38
|
+
</macro>
|
|
39
|
+
<macro name="year-date">
|
|
40
|
+
<choose>
|
|
41
|
+
<if variable="issued">
|
|
42
|
+
<date variable="issued">
|
|
43
|
+
<date-part name="year"/>
|
|
44
|
+
</date>
|
|
45
|
+
</if>
|
|
46
|
+
<else>
|
|
47
|
+
<!-- <text term="no date" form="short"/> -->
|
|
48
|
+
</else>
|
|
49
|
+
</choose>
|
|
50
|
+
</macro>
|
|
51
|
+
<macro name="day-date">
|
|
52
|
+
<choose>
|
|
53
|
+
<if variable="issued">
|
|
54
|
+
<date variable="issued">
|
|
55
|
+
<date-part name="day" suffix=" "/>
|
|
56
|
+
<date-part name="month" form="long" suffix=" "/>
|
|
57
|
+
<date-part name="year"/>
|
|
58
|
+
</date>
|
|
59
|
+
</if>
|
|
60
|
+
<else>
|
|
61
|
+
<text term="no date" form="short"/>
|
|
62
|
+
</else>
|
|
63
|
+
</choose>
|
|
64
|
+
</macro>
|
|
65
|
+
<macro name="publisher">
|
|
66
|
+
<group prefix="(" suffix=")" delimiter=", ">
|
|
67
|
+
<text variable="publisher"/>
|
|
68
|
+
<text variable="publisher-place" text-case="title"/>
|
|
69
|
+
<text macro="year-date"/>
|
|
70
|
+
</group>
|
|
71
|
+
</macro>
|
|
72
|
+
<macro name="edition">
|
|
73
|
+
<choose>
|
|
74
|
+
<if is-numeric="edition">
|
|
75
|
+
<group delimiter=" ">
|
|
76
|
+
<number variable="edition" form="ordinal"/>
|
|
77
|
+
<text term="edition" form="short"/>
|
|
78
|
+
</group>
|
|
79
|
+
</if>
|
|
80
|
+
<else>
|
|
81
|
+
<text variable="edition"/>
|
|
82
|
+
</else>
|
|
83
|
+
</choose>
|
|
84
|
+
</macro>
|
|
85
|
+
<citation collapse="citation-number">
|
|
86
|
+
<sort>
|
|
87
|
+
<key variable="citation-number"/>
|
|
88
|
+
</sort>
|
|
89
|
+
<layout delimiter="," prefix=" [" suffix="]">
|
|
90
|
+
<text variable="citation-number"/>
|
|
91
|
+
</layout>
|
|
92
|
+
</citation>
|
|
93
|
+
<bibliography et-al-min="7" et-al-use-first="1" entry-spacing="0" second-field-align="flush">
|
|
94
|
+
<layout suffix=".">
|
|
95
|
+
<!-- PATCH Feb 2023 (Philippe Faist): remove citation number -->
|
|
96
|
+
<!-- <text variable="citation-number" prefix="[" suffix="]"/> -->
|
|
97
|
+
<group delimiter=", "><!-- added -->
|
|
98
|
+
<text macro="author" /> <!-- suffix=", "/> -->
|
|
99
|
+
<choose>
|
|
100
|
+
<if type="bill book graphic legal_case legislation motion_picture report song" match="any">
|
|
101
|
+
<group delimiter=" ">
|
|
102
|
+
<group delimiter=", ">
|
|
103
|
+
<text variable="title" text-case="title" font-style="italic"/>
|
|
104
|
+
<text macro="edition"/>
|
|
105
|
+
</group>
|
|
106
|
+
<group delimiter=", ">
|
|
107
|
+
<text macro="publisher"/>
|
|
108
|
+
<group delimiter=" ">
|
|
109
|
+
<label variable="page" form="short"/>
|
|
110
|
+
<text variable="page"/>
|
|
111
|
+
</group>
|
|
112
|
+
</group>
|
|
113
|
+
</group>
|
|
114
|
+
</if>
|
|
115
|
+
<else-if type="chapter paper-conference" match="any">
|
|
116
|
+
<group delimiter=" ">
|
|
117
|
+
<text term="in"/>
|
|
118
|
+
<group delimiter=", ">
|
|
119
|
+
<text variable="container-title" form="short" text-case="title" font-style="italic"/>
|
|
120
|
+
<text macro="editor"/>
|
|
121
|
+
<text macro="edition"/>
|
|
122
|
+
</group>
|
|
123
|
+
<group delimiter=", ">
|
|
124
|
+
<text macro="publisher"/>
|
|
125
|
+
<group delimiter=" ">
|
|
126
|
+
<label variable="page" form="short"/>
|
|
127
|
+
<text variable="page"/>
|
|
128
|
+
</group>
|
|
129
|
+
</group>
|
|
130
|
+
</group>
|
|
131
|
+
</else-if>
|
|
132
|
+
<else-if type="patent">
|
|
133
|
+
<group delimiter=" ">
|
|
134
|
+
<text variable="number"/>
|
|
135
|
+
<text macro="day-date" prefix="(" suffix=")"/>
|
|
136
|
+
</group>
|
|
137
|
+
</else-if>
|
|
138
|
+
<else-if type="thesis">
|
|
139
|
+
<group delimiter=", ">
|
|
140
|
+
<text variable="title" text-case="title"/>
|
|
141
|
+
<text variable="genre"/>
|
|
142
|
+
<text variable="publisher"/>
|
|
143
|
+
<text macro="year-date"/>
|
|
144
|
+
</group>
|
|
145
|
+
</else-if>
|
|
146
|
+
<else>
|
|
147
|
+
<group delimiter=" ">
|
|
148
|
+
<choose>
|
|
149
|
+
<if type="article-journal">
|
|
150
|
+
<text variable="container-title" form="short"/>
|
|
151
|
+
</if>
|
|
152
|
+
<else>
|
|
153
|
+
<text variable="container-title"/>
|
|
154
|
+
</else>
|
|
155
|
+
</choose>
|
|
156
|
+
<group delimiter=", ">
|
|
157
|
+
<text variable="volume" font-weight="bold"/>
|
|
158
|
+
<group delimiter=" ">
|
|
159
|
+
<text variable="page-first" form="short"/>
|
|
160
|
+
<text macro="year-date" prefix="(" suffix=")"/>
|
|
161
|
+
</group>
|
|
162
|
+
</group>
|
|
163
|
+
</group>
|
|
164
|
+
</else>
|
|
165
|
+
</choose>
|
|
166
|
+
</group><!-- added -->
|
|
167
|
+
</layout>
|
|
168
|
+
</bibliography>
|
|
169
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
import arxiv
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .base import CitationSourceBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_rx_arxiv_id_from_entryid = re.compile(
|
|
13
|
+
# note: <arxivid> does NOT include the version number
|
|
14
|
+
r'https?://arxiv.org/abs/(?P<arxivid>.*?)(?P<versionnumstr>v(?P<versionnum>\d+))?$',
|
|
15
|
+
flags=re.IGNORECASE
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_rx_ver = re.compile(r'v\d+$')
|
|
19
|
+
|
|
20
|
+
class CitationSourceArxiv(CitationSourceBase):
|
|
21
|
+
|
|
22
|
+
def __init__(self, **kwargs):
|
|
23
|
+
|
|
24
|
+
self.chain_to_doi = kwargs.get('chain_to_doi', True)
|
|
25
|
+
|
|
26
|
+
override_options = {
|
|
27
|
+
'chains_to_sources': ['doi'] if self.chain_to_doi else [],
|
|
28
|
+
'source_name': 'ArXiv API citation info source',
|
|
29
|
+
}
|
|
30
|
+
default_options = {
|
|
31
|
+
'chunk_query_delay_ms': 3100,
|
|
32
|
+
'cite_prefix': 'arxiv',
|
|
33
|
+
'use_requests': True,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
super().__init__(
|
|
37
|
+
override_options,
|
|
38
|
+
kwargs,
|
|
39
|
+
default_options,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.override_arxiv_dois_file = self.options.get('override_arxiv_dois_file', None)
|
|
43
|
+
self.override_arxiv_dois = self.options.get('override_arxiv_dois', {})
|
|
44
|
+
|
|
45
|
+
# silence some arxiv.arxiv messages
|
|
46
|
+
if not self.options.get('keep_arxiv_arxiv_logging_info_output', False):
|
|
47
|
+
logging.getLogger('arxiv.arxiv').setLevel(level=logging.WARNING)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def source_initialize_run(self):
|
|
51
|
+
|
|
52
|
+
self._arxiv_to_doi_override = {}
|
|
53
|
+
if self.override_arxiv_dois_file:
|
|
54
|
+
content = self.fetch_url(self.override_arxiv_dois_file)
|
|
55
|
+
data = yaml.safe_load(content)
|
|
56
|
+
self._arxiv_to_doi_override.update(data)
|
|
57
|
+
|
|
58
|
+
self._arxiv_to_doi_override.update(self.override_arxiv_dois)
|
|
59
|
+
|
|
60
|
+
self.data_for_versionless_arxivid = {
|
|
61
|
+
arxivid: []
|
|
62
|
+
for arxivid in self.cite_key_list
|
|
63
|
+
if not _rx_ver.search(arxivid)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def retrieve_chunk(self, arxivid_list):
|
|
67
|
+
|
|
68
|
+
if not arxivid_list:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# fetch meta-info from the arxiv for all encountered arXiv IDs, and
|
|
73
|
+
# build the associated citation text endnotes.
|
|
74
|
+
#
|
|
75
|
+
big_slow_client = arxiv.Client(
|
|
76
|
+
page_size=len(arxivid_list) + 100,
|
|
77
|
+
delay_seconds=4,
|
|
78
|
+
num_retries=5
|
|
79
|
+
)
|
|
80
|
+
searchobj = arxiv.Search(
|
|
81
|
+
id_list=arxivid_list,
|
|
82
|
+
)
|
|
83
|
+
for result in big_slow_client.results(searchobj):
|
|
84
|
+
#
|
|
85
|
+
# build citation from the arxiv meta-information
|
|
86
|
+
#
|
|
87
|
+
|
|
88
|
+
m = _rx_arxiv_id_from_entryid.match(result.entry_id)
|
|
89
|
+
if m is None:
|
|
90
|
+
logger.warning(f"Unable to parse arXiv ID from {result.entry_id!r}")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
arxivid = m.group('arxivid').lower()
|
|
94
|
+
versionnum = m.group('versionnum')
|
|
95
|
+
arxivver = int(versionnum) if versionnum else None
|
|
96
|
+
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"Processing received information for ‘{arxivid}’ (got v{arxivver})"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
doi = None
|
|
102
|
+
if result.doi is not None and result.doi:
|
|
103
|
+
doi = str(result.doi)
|
|
104
|
+
|
|
105
|
+
# override the given DOI in exceptional cases where it might be
|
|
106
|
+
# missing, incomplete, or incorrect:
|
|
107
|
+
if arxivid in self._arxiv_to_doi_override:
|
|
108
|
+
override_doi = self._arxiv_to_doi_override[arxivid]
|
|
109
|
+
logger.debug(f"Overriding reported DOI (‘{doi}’) for arXiv "
|
|
110
|
+
f"item ‘{arxivid}’ with manually specified DOI "
|
|
111
|
+
f"‘{override_doi}’ given in citation hints")
|
|
112
|
+
doi = override_doi
|
|
113
|
+
|
|
114
|
+
# construct CSL-JSON entry from the retrieved metadata
|
|
115
|
+
citeprocjsond = {
|
|
116
|
+
'type': 'article-journal',
|
|
117
|
+
'title': result.title,
|
|
118
|
+
'author': [
|
|
119
|
+
{'name': a.name}
|
|
120
|
+
for a in result.authors
|
|
121
|
+
],
|
|
122
|
+
'published': {
|
|
123
|
+
'date-parts': [[
|
|
124
|
+
result.published.year,
|
|
125
|
+
result.published.month,
|
|
126
|
+
result.published.day,
|
|
127
|
+
]],
|
|
128
|
+
},
|
|
129
|
+
'doi': doi,
|
|
130
|
+
'arxivid': arxivid,
|
|
131
|
+
'arxiv_version_number': arxivver,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if arxivid in self.cite_key_list:
|
|
135
|
+
self.citation_manager.store_citation(self.cite_prefix, arxivid, citeprocjsond)
|
|
136
|
+
|
|
137
|
+
if arxivid in self.data_for_versionless_arxivid:
|
|
138
|
+
self.data_for_versionless_arxivid[arxivid].append( citeprocjsond )
|
|
139
|
+
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def source_finalize_run(self):
|
|
143
|
+
|
|
144
|
+
# all the queries for ids ran, now figure out which versions to link to
|
|
145
|
+
# when a non-versioned key is provided
|
|
146
|
+
|
|
147
|
+
for arxivid, versionslist in self.data_for_versionless_arxivid.items():
|
|
148
|
+
|
|
149
|
+
if not versionslist:
|
|
150
|
+
logger.error(
|
|
151
|
+
f"No arXiv data received for arXiv id ‘{arxivid}’, what happened?!?"
|
|
152
|
+
)
|
|
153
|
+
raise ValueError(f"No arXiv data found for ‘{arxivid}’")
|
|
154
|
+
|
|
155
|
+
best = None
|
|
156
|
+
for current in versionslist:
|
|
157
|
+
if best is None and current is not None:
|
|
158
|
+
best = current
|
|
159
|
+
continue
|
|
160
|
+
if current is None:
|
|
161
|
+
continue
|
|
162
|
+
# "arxiv_version_number === null" only happens if arXiv API
|
|
163
|
+
# responded with an article whose ID didn't have a version
|
|
164
|
+
# number -> use that one directly as it is the answer to our
|
|
165
|
+
# query!
|
|
166
|
+
if best['arxiv_version_number'] is None:
|
|
167
|
+
continue
|
|
168
|
+
if current['arxiv_version_number'] is None:
|
|
169
|
+
best = current
|
|
170
|
+
continue
|
|
171
|
+
if current['arxiv_version_number'] >= best['arxiv_version_number']:
|
|
172
|
+
best = current
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
self._store_citation(arxivid, best)
|
|
176
|
+
|
|
177
|
+
def _store_citation(self, arxivid, csljsondata):
|
|
178
|
+
doi = csljsondata.get('doi', None)
|
|
179
|
+
if doi and self.chain_to_doi:
|
|
180
|
+
# chain to DOI
|
|
181
|
+
self.citation_manager.store_citation_chained(
|
|
182
|
+
self.cite_prefix, arxivid,
|
|
183
|
+
'doi', doi,
|
|
184
|
+
{
|
|
185
|
+
'arxivid': arxivid,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
self.citation_manager.store_citation(self.cite_prefix, arxivid, csljsondata)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# for when using shorthane naming
|
|
195
|
+
CitationSourceClass = CitationSourceArxiv
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CitationSourceBase:
|
|
11
|
+
|
|
12
|
+
def __init__(self, override_options, kwargs, default_options):
|
|
13
|
+
super().__init__()
|
|
14
|
+
|
|
15
|
+
self.options = dict(default_options)
|
|
16
|
+
self.options.update(kwargs)
|
|
17
|
+
self.options.update(override_options)
|
|
18
|
+
|
|
19
|
+
self.chunk_size = self.options.get('chunk_size', 512)
|
|
20
|
+
self.chunk_query_delay_ms = self.options.get('chunk_query_delay_ms', 1000)
|
|
21
|
+
|
|
22
|
+
self.cite_prefix = self.options['cite_prefix']
|
|
23
|
+
|
|
24
|
+
self.chains_to_sources = self.options.get('chains_to_sources', [])
|
|
25
|
+
self.source_name = self.options.get('source_name', '<unknown source>')
|
|
26
|
+
|
|
27
|
+
self.requests_session = None
|
|
28
|
+
if self.options.get('use_requests', False):
|
|
29
|
+
if 'requests_session' in self.options:
|
|
30
|
+
self.requests_session = self.options['requests_session']
|
|
31
|
+
else:
|
|
32
|
+
self.requests_session = requests.Session()
|
|
33
|
+
|
|
34
|
+
self.cite_key_list = None
|
|
35
|
+
|
|
36
|
+
self.citation_manager = None
|
|
37
|
+
|
|
38
|
+
def set_citation_manager(self, citation_manager):
|
|
39
|
+
self.citation_manager = citation_manager
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def retrieve_citations(self, cite_key_list):
|
|
43
|
+
|
|
44
|
+
self.cite_key_list = cite_key_list
|
|
45
|
+
|
|
46
|
+
self.source_initialize_run()
|
|
47
|
+
|
|
48
|
+
total_retrieved = 0
|
|
49
|
+
remaining_keys = list(cite_key_list)
|
|
50
|
+
|
|
51
|
+
last_chunk_query_monotonic_s = None
|
|
52
|
+
|
|
53
|
+
if len(remaining_keys):
|
|
54
|
+
logger.info(f"{self.source_name}: there are "
|
|
55
|
+
f"{len(remaining_keys)} citation(s) to query")
|
|
56
|
+
else:
|
|
57
|
+
logger.debug(f"{self.source_name}: there are no citations to query")
|
|
58
|
+
|
|
59
|
+
while len(remaining_keys) > 0:
|
|
60
|
+
|
|
61
|
+
# if applicable, wait before another chunk query call
|
|
62
|
+
if (self.chunk_query_delay_ms and last_chunk_query_monotonic_s is not None):
|
|
63
|
+
dt_s = (time.monotonic() - last_chunk_query_monotonic_s)
|
|
64
|
+
dt_s_to_wait = (self.chunk_query_delay_ms/1000) - dt_s
|
|
65
|
+
if dt_s_to_wait > 0:
|
|
66
|
+
time.sleep(dt_s_to_wait)
|
|
67
|
+
|
|
68
|
+
# create a chunk of IDs to query
|
|
69
|
+
chunk_ids, remaining_keys = \
|
|
70
|
+
remaining_keys[:self.chunk_size], remaining_keys[self.chunk_size:]
|
|
71
|
+
|
|
72
|
+
last_chunk_query_monotonic_s = time.monotonic()
|
|
73
|
+
self.retrieve_chunk(chunk_ids)
|
|
74
|
+
|
|
75
|
+
# bookkeeping & logging
|
|
76
|
+
total_retrieved += len(chunk_ids)
|
|
77
|
+
logger.info(f"{self.source_name}: {total_retrieved}/{len(cite_key_list)}")
|
|
78
|
+
|
|
79
|
+
self.source_finalize_run()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# helper for fetching URLs
|
|
83
|
+
def fetch_url(self, url, binary=False, json=False, **kwargs):
|
|
84
|
+
|
|
85
|
+
if url is None:
|
|
86
|
+
raise ValueError(f"fetch_url(): url is None!")
|
|
87
|
+
|
|
88
|
+
logger.debug(f"Fetching URL ‘{url}’ ...")
|
|
89
|
+
|
|
90
|
+
p = urlparse(url)
|
|
91
|
+
|
|
92
|
+
if not p.scheme or p.scheme == 'file':
|
|
93
|
+
# Read a local file.
|
|
94
|
+
if binary:
|
|
95
|
+
open_args, open_kwargs = ('rb',), {}
|
|
96
|
+
else:
|
|
97
|
+
open_args, open_kwargs = ('r',), { 'encoding': 'utf-8' }
|
|
98
|
+
with open(p.path, *open_args, **open_kwargs) as f:
|
|
99
|
+
return f.read()
|
|
100
|
+
|
|
101
|
+
req_kwargs = {}
|
|
102
|
+
|
|
103
|
+
method = kwargs.pop('method', 'get')
|
|
104
|
+
reqfn = getattr(self.requests_session, method)
|
|
105
|
+
|
|
106
|
+
if method == 'post':
|
|
107
|
+
req_kwargs['data'] = kwargs.pop('body', None)
|
|
108
|
+
|
|
109
|
+
# e.g., headers etc.
|
|
110
|
+
req_kwargs.update(kwargs)
|
|
111
|
+
|
|
112
|
+
# fire!
|
|
113
|
+
r = reqfn(url)
|
|
114
|
+
|
|
115
|
+
if not r.ok:
|
|
116
|
+
# raise errors
|
|
117
|
+
r.raise_for_status()
|
|
118
|
+
|
|
119
|
+
if binary:
|
|
120
|
+
return r.content
|
|
121
|
+
|
|
122
|
+
r.encoding = 'utf-8'
|
|
123
|
+
|
|
124
|
+
if json:
|
|
125
|
+
return r.json()
|
|
126
|
+
|
|
127
|
+
return r.text
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# -------------
|
|
132
|
+
|
|
133
|
+
# can be reimplemented
|
|
134
|
+
|
|
135
|
+
def source_initialize_run(self):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def source_finalize_run(self):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# must be reimplemented
|
|
143
|
+
|
|
144
|
+
def retrieve_chunk(self, chunk_keys):
|
|
145
|
+
raise RuntimeError("The method retrieve_chunk() must be reimplemented by subclasses!")
|