idlib 0.0.1.dev21__py2.py3-none-any.whl → 0.0.1.dev23__py2.py3-none-any.whl
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.
- idlib/__init__.py +1 -1
- idlib/apis/protocols_io.py +9 -5
- idlib/conventions/type.py +11 -9
- idlib/core.py +96 -13
- idlib/exceptions.py +9 -1
- idlib/from_oq.py +35 -3
- idlib/streams.py +15 -5
- idlib/systems/doi.py +2 -2
- idlib/systems/orcid.py +50 -6
- idlib/systems/ror.py +35 -9
- idlib/systems/rrid.py +44 -0
- idlib/utils.py +6 -1
- {idlib-0.0.1.dev21.dist-info → idlib-0.0.1.dev23.dist-info}/METADATA +29 -15
- {idlib-0.0.1.dev21.dist-info → idlib-0.0.1.dev23.dist-info}/RECORD +17 -17
- {idlib-0.0.1.dev21.dist-info → idlib-0.0.1.dev23.dist-info}/WHEEL +1 -1
- {idlib-0.0.1.dev21.dist-info → idlib-0.0.1.dev23.dist-info/licenses}/LICENSE +0 -0
- {idlib-0.0.1.dev21.dist-info → idlib-0.0.1.dev23.dist-info}/top_level.txt +0 -0
idlib/__init__.py
CHANGED
idlib/apis/protocols_io.py
CHANGED
|
@@ -3,7 +3,6 @@ import sys
|
|
|
3
3
|
import pickle
|
|
4
4
|
from urllib import parse
|
|
5
5
|
from getpass import getpass
|
|
6
|
-
from ..config import auth
|
|
7
6
|
from ..utils import log as _log
|
|
8
7
|
from . import log as _log
|
|
9
8
|
|
|
@@ -68,11 +67,16 @@ def get_auth_code(url):
|
|
|
68
67
|
|
|
69
68
|
|
|
70
69
|
def get_protocols_io_auth(creds_file,
|
|
71
|
-
store_file=
|
|
72
|
-
# yes reading from the store file here at the top level
|
|
73
|
-
# increases startup time, however if it is burried then
|
|
74
|
-
# any config error is deferred until later in runtime
|
|
70
|
+
store_file=None,
|
|
75
71
|
SCOPES='readwrite'):
|
|
72
|
+
if store_file is None:
|
|
73
|
+
# since this file is imported whenever idlib is imported
|
|
74
|
+
# the time cost is prohibitive and we will just have to
|
|
75
|
+
# deal with the configuration value failing later in time
|
|
76
|
+
# by e.g. having a separate check config process
|
|
77
|
+
from ..config import auth
|
|
78
|
+
store_file = auth.get_path('protocols-io-api-store-file')
|
|
79
|
+
|
|
76
80
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
77
81
|
from google.auth.transport.requests import Request
|
|
78
82
|
from idlib import exceptions as exc
|
idlib/conventions/type.py
CHANGED
|
@@ -13,19 +13,21 @@ class ConvTypeBytesHeader(ConventionsType):
|
|
|
13
13
|
self.stop = stop
|
|
14
14
|
self.sentinel = sentinel
|
|
15
15
|
|
|
16
|
-
def _findElement(self, element, chunk):
|
|
17
|
-
|
|
16
|
+
def _findElement(self, element, chunk, prev):
|
|
17
|
+
to_search = prev + chunk
|
|
18
|
+
m = re.search(element, to_search)
|
|
18
19
|
if m is not None:
|
|
20
|
+
lp = len(prev)
|
|
19
21
|
# FIXME icky api
|
|
20
|
-
return m.start(), m.end()
|
|
22
|
+
return m.start() - lp, m.end() -lp
|
|
21
23
|
else:
|
|
22
24
|
return None, None
|
|
23
25
|
|
|
24
|
-
def findStart(self, chunk):
|
|
25
|
-
return self._findElement(self.start, chunk)
|
|
26
|
+
def findStart(self, chunk, prev):
|
|
27
|
+
return self._findElement(self.start, chunk, prev)
|
|
26
28
|
|
|
27
|
-
def findStop(self, chunk):
|
|
28
|
-
return self._findElement(self.stop, chunk)
|
|
29
|
+
def findStop(self, chunk, prev):
|
|
30
|
+
return self._findElement(self.stop, chunk, prev)
|
|
29
31
|
|
|
30
|
-
def findSentinel(self, chunk):
|
|
31
|
-
return self._findElement(self.sentinel, chunk)
|
|
32
|
+
def findSentinel(self, chunk, prev):
|
|
33
|
+
return self._findElement(self.sentinel, chunk, prev)
|
idlib/core.py
CHANGED
|
@@ -1,17 +1,90 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
from . import exceptions as exc
|
|
3
|
+
from .utils import log
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def resolution_chain(iri):
|
|
6
|
-
for head in resolution_chain_responses(iri):
|
|
6
|
+
def resolution_chain(iri, headers_fun=None):
|
|
7
|
+
for head in resolution_chain_responses(iri, headers_fun=headers_fun):
|
|
7
8
|
yield head.url
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
_user_agent_idiocy = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def try_get(s, head, headers_fun=None):
|
|
15
|
+
# see whether the server has a bad/broken support for head requests
|
|
16
|
+
# sometimes they return e.g. 400 instead of 405, and really they should
|
|
17
|
+
# just work because they work correctly with get request ...
|
|
18
|
+
head = s.get(head.url, stream=True)
|
|
19
|
+
|
|
20
|
+
if head.ok:
|
|
21
|
+
log.info(f'bad HEAD implementation {head.url}')
|
|
22
|
+
elif head.status_code < 500:
|
|
23
|
+
head.close()
|
|
24
|
+
headers = {'User-Agent': _user_agent_idiocy}
|
|
25
|
+
head = s.get(head.url, headers=headers, stream=True)
|
|
26
|
+
if head.ok:
|
|
27
|
+
log.info(f'bad HEAD implementation AND bad User-Agent behavior {head.url}')
|
|
28
|
+
else:
|
|
29
|
+
headers = None
|
|
30
|
+
if headers_fun is not None:
|
|
31
|
+
# headers_fun is used to ensure that e.g. auth headers
|
|
32
|
+
# are only sent to the netloc's they are for
|
|
33
|
+
headers = headers_fun(head.url)
|
|
34
|
+
if headers is not None:
|
|
35
|
+
head = s.get(head.url, headers=headers, stream=True)
|
|
36
|
+
if head.ok:
|
|
37
|
+
# we log a warning because dereferencing a public id
|
|
38
|
+
# and encountering a permission boundary is bad
|
|
39
|
+
# TODO likely need a way to fail on this condition
|
|
40
|
+
# or at least indicate that it happend in band
|
|
41
|
+
msg = f'additional headers were added for {head.url}'
|
|
42
|
+
log.warning(msg)
|
|
43
|
+
|
|
44
|
+
if not head.ok:
|
|
45
|
+
content = head.content
|
|
46
|
+
if content:
|
|
47
|
+
if head.headers['Content-Type'] == 'application/json':
|
|
48
|
+
try:
|
|
49
|
+
j = head.json()
|
|
50
|
+
log.error(f'{head.url} error json {j}')
|
|
51
|
+
except Exception as e:
|
|
52
|
+
log.error(f'{head.url} error text {head.text}')
|
|
53
|
+
|
|
54
|
+
head.close()
|
|
55
|
+
return head
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolution_raise_logic(head):
|
|
59
|
+
""" broken out into its own function for reuse to avoid re-resolving
|
|
60
|
+
in cases where we need to interpose additional operations between
|
|
61
|
+
a dereference failure and raising the error
|
|
62
|
+
"""
|
|
63
|
+
if not head.ok:
|
|
64
|
+
try:
|
|
65
|
+
head.raise_for_status()
|
|
66
|
+
except Exception as e:
|
|
67
|
+
if head.status_code == 404:
|
|
68
|
+
msg = f'Nothing found due to {head.status_code} at {head.url}\n'
|
|
69
|
+
raise exc.IdDoesNotExistError(msg) from e # often a permissions issue
|
|
70
|
+
elif head.status_code >= 500:
|
|
71
|
+
msg = f'Remote in error due to {head.status_code} at {head.url}\n'
|
|
72
|
+
raise exc.RemoteError(msg) from e
|
|
73
|
+
elif head.status_code >= 400:
|
|
74
|
+
msg = f'Nothing found due to {head.status_code} at {head.url}\n'
|
|
75
|
+
raise exc.ResolutionError(msg) from e
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolution_chain_responses(iri, raise_on_final=True, headers_fun=None):
|
|
11
79
|
#doi = doi # TODO
|
|
12
80
|
s = requests.Session()
|
|
13
|
-
head =
|
|
14
|
-
|
|
81
|
+
head = s.head(iri, allow_redirects=False)
|
|
82
|
+
if head.status_code < 400:
|
|
83
|
+
yield head
|
|
84
|
+
else:
|
|
85
|
+
head = try_get(s, head, headers_fun=headers_fun)
|
|
86
|
+
yield head
|
|
87
|
+
|
|
15
88
|
while head.is_redirect and head.status_code < 400: # FIXME redirect loop issue
|
|
16
89
|
yield head.next
|
|
17
90
|
try:
|
|
@@ -19,15 +92,25 @@ def resolution_chain_responses(iri, raise_on_final=True):
|
|
|
19
92
|
except requests.exceptions.SSLError as e:
|
|
20
93
|
raise exc.InbetweenError(msg) from e
|
|
21
94
|
|
|
22
|
-
|
|
95
|
+
if head.status_code < 400:
|
|
96
|
+
yield head
|
|
97
|
+
else:
|
|
98
|
+
head = try_get(s, head, headers_fun=headers_fun)
|
|
99
|
+
yield head
|
|
100
|
+
|
|
23
101
|
if not head.is_redirect:
|
|
24
102
|
break
|
|
25
103
|
|
|
26
104
|
if raise_on_final: # we still want the chain ... null pointer error comes later?
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
105
|
+
resolution_raise_logic(head)
|
|
106
|
+
|
|
107
|
+
if head.status_code >= 400:
|
|
108
|
+
# XXX this seems like the "right" thing to do, but it will
|
|
109
|
+
# probably break a bunch of stuff, but at least this way it
|
|
110
|
+
# will be visible now, obviously yeilding None as the final
|
|
111
|
+
# element in the reference chain conflates all the possible
|
|
112
|
+
# reasons so raising is a better solution and is the default
|
|
113
|
+
# for a reason but at least this way the user knows that the
|
|
114
|
+
# final element of the chain dereference to nothing instead of
|
|
115
|
+
# assuming that the last element succeeded
|
|
116
|
+
yield None
|
idlib/exceptions.py
CHANGED
|
@@ -36,7 +36,7 @@ class CouldNotReachReferentError(CouldNotReachError):
|
|
|
36
36
|
|
|
37
37
|
Note that in a system that is not based on redirects
|
|
38
38
|
this is an error that could be returned by the index
|
|
39
|
-
but it would appear as
|
|
39
|
+
but it would appear as a referent unknown error. """
|
|
40
40
|
|
|
41
41
|
# but its supertype is ReferentUnknown or ReferentUnreachable
|
|
42
42
|
|
|
@@ -59,6 +59,14 @@ class ResolutionError(IdlibError):
|
|
|
59
59
|
# we should split into NetworkError/TimeoutError
|
|
60
60
|
# and then have RemoteError, because Resolution error is
|
|
61
61
|
# ambiguous, it is the superclass for all of them I think
|
|
62
|
+
# XXX as used, this is ClientError, and for all but 404
|
|
63
|
+
# and 410 the response means we can't actually say whether
|
|
64
|
+
# it is a true null pointer, the naming is thus very bad
|
|
65
|
+
# because as used this is not a network error, not a timeout
|
|
66
|
+
# error, it is the server saying i do not/cannot understand
|
|
67
|
+
# that is MessageReceivedButError which happens before we
|
|
68
|
+
# know if the id exists and probably actually covers most
|
|
69
|
+
# of the IdExistsButError cases
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
class MalformedIdentifierError(IdlibError):
|
idlib/from_oq.py
CHANGED
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
import json
|
|
3
3
|
from time import time, sleep
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from urllib.parse import urlparse
|
|
5
6
|
import orthauth as oa
|
|
6
7
|
import ontquery as oq # temporary implementation detail
|
|
7
8
|
import idlib
|
|
@@ -20,6 +21,11 @@ from idlib.utils import (log,
|
|
|
20
21
|
from idlib.config import auth
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def path_older_than_day(path):
|
|
25
|
+
ps = path.stat()
|
|
26
|
+
return time() - ps.st_ctime > (60 * 60 * 24)
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
# from neurondm.simple
|
|
24
30
|
class OntTerm(oq.OntTerm):
|
|
25
31
|
""" ask things about a term! """
|
|
@@ -345,6 +351,13 @@ def setup(cls, creds_file=None):
|
|
|
345
351
|
log.warning(e)
|
|
346
352
|
cls._pio_header = None
|
|
347
353
|
|
|
354
|
+
def rchf(url):
|
|
355
|
+
purl = urlparse(url)
|
|
356
|
+
if purl.netloc.endswith('protocols.io'):
|
|
357
|
+
return cls._pio_header
|
|
358
|
+
|
|
359
|
+
cls._resolution_chain_headers_fun = staticmethod(rchf)
|
|
360
|
+
|
|
348
361
|
if not hasattr(idlib.Stream, '_requests'):
|
|
349
362
|
idlib.Stream.__new__(cls)
|
|
350
363
|
|
|
@@ -645,7 +658,7 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
645
658
|
return data
|
|
646
659
|
|
|
647
660
|
_apiuri_private_cache = {} # XXX FIXME hack
|
|
648
|
-
def data4(self, fail_ok=False):
|
|
661
|
+
def data4(self, fail_ok=False, try_refresh_cache=False):
|
|
649
662
|
if not hasattr(self, '_data'):
|
|
650
663
|
self._data_in_error = True
|
|
651
664
|
if not isinstance(self._progenitors, dict):
|
|
@@ -661,7 +674,7 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
661
674
|
# however this is not trivial to retrive
|
|
662
675
|
# the /v3/protocols/{id} endpoint returns bad/mangled data and we need
|
|
663
676
|
# the /v1/ data to be able to actually render to org
|
|
664
|
-
blob, path = self._get_data(apiuri)
|
|
677
|
+
blob, path = self._get_data(apiuri, _refresh_cache=try_refresh_cache)
|
|
665
678
|
if 'stream-http' not in self._progenitors:
|
|
666
679
|
self._progenitors['path'] = path
|
|
667
680
|
|
|
@@ -690,6 +703,16 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
690
703
|
if fail_ok: return
|
|
691
704
|
raise exc.IdDoesNotExistError(message)
|
|
692
705
|
elif sc in (250, 205): # access requested, not authorized
|
|
706
|
+
if path_older_than_day(path):
|
|
707
|
+
# there seems to be a case where a protocol is shared
|
|
708
|
+
# with us and there is an old cache, in that case we
|
|
709
|
+
# hit an infinite loop because getting data for the
|
|
710
|
+
# progenitor says we should be able to resolve the
|
|
711
|
+
# final protocol now but it was cached on cooldown, if
|
|
712
|
+
# we can get to the first progenitor then we likely
|
|
713
|
+
# have access to the final url and get will succeed
|
|
714
|
+
return self.data4(fail_ok=fail_ok, try_refresh_cache=True)
|
|
715
|
+
|
|
693
716
|
try:
|
|
694
717
|
# there might be a private id in the progenitor chain
|
|
695
718
|
nself = self.progenitor(type='id-converted-from')
|
|
@@ -811,7 +834,8 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
811
834
|
try:
|
|
812
835
|
return self.data1(fail_ok=fail_ok)
|
|
813
836
|
except Exception as e1:
|
|
814
|
-
|
|
837
|
+
# FIXME error type ...
|
|
838
|
+
raise exc.InbetweenError() from e1
|
|
815
839
|
else:
|
|
816
840
|
return self.data4(fail_ok=fail_ok)
|
|
817
841
|
except (AttributeError, NotImplementedError) as e:
|
|
@@ -2041,6 +2065,14 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
2041
2065
|
if include_private and self.identifier.is_private():
|
|
2042
2066
|
out['uri_private'] = self.identifier # FIXME some way to avoid leaking these if needed?
|
|
2043
2067
|
return out
|
|
2068
|
+
except exc.MalformedIdentifierError as e:
|
|
2069
|
+
out = super().asDict(include_description)
|
|
2070
|
+
out['malformed'] = True
|
|
2071
|
+
if 'errors' not in out:
|
|
2072
|
+
out['errors'] = []
|
|
2073
|
+
|
|
2074
|
+
out['errors'].append({'message': 'malformed'})
|
|
2075
|
+
return out
|
|
2044
2076
|
except exc.RemoteError as e:
|
|
2045
2077
|
# we don't have any metadata but we will return what little info we have
|
|
2046
2078
|
return super().asDict(include_description)
|
idlib/streams.py
CHANGED
|
@@ -41,6 +41,8 @@ class Stream:
|
|
|
41
41
|
|
|
42
42
|
_progenitors = MappingProxyType({}) # this is a dict, but immutable to prevent accidents
|
|
43
43
|
|
|
44
|
+
_resolution_chain_headers_fun = None
|
|
45
|
+
|
|
44
46
|
def __new__(cls, *args, **kwargs):
|
|
45
47
|
return super().__new__(cls)
|
|
46
48
|
|
|
@@ -56,8 +58,9 @@ class Stream:
|
|
|
56
58
|
import requests # resolver dejoure
|
|
57
59
|
Stream._requests = requests
|
|
58
60
|
|
|
59
|
-
from idlib.core import resolution_chain_responses
|
|
61
|
+
from idlib.core import resolution_chain_responses, resolution_raise_logic
|
|
60
62
|
Stream._resolution_chain_responses = staticmethod(resolution_chain_responses)
|
|
63
|
+
Stream._resolution_raise_logic = staticmethod(resolution_raise_logic)
|
|
61
64
|
|
|
62
65
|
@classmethod
|
|
63
66
|
def fromJson(cls, blob):
|
|
@@ -606,10 +609,12 @@ class StreamUri(Stream):
|
|
|
606
609
|
if not self.identifier_actionable in dc:
|
|
607
610
|
try:
|
|
608
611
|
dc[self.identifier_actionable] = tuple(
|
|
609
|
-
StringProgenitor(resp.url, progenitor=resp)
|
|
612
|
+
resp if resp is None else StringProgenitor(resp.url, progenitor=resp)
|
|
610
613
|
for resp in
|
|
611
|
-
self._resolution_chain_responses(
|
|
612
|
-
|
|
614
|
+
self._resolution_chain_responses(
|
|
615
|
+
self.identifier_actionable,
|
|
616
|
+
raise_on_final=False,
|
|
617
|
+
headers_fun=self._resolution_chain_headers_fun))
|
|
613
618
|
except (exc.ResolutionError, exc.RemoteError) as e:
|
|
614
619
|
# TODO
|
|
615
620
|
# FIXME partial resolution is a complete nightmare
|
|
@@ -626,6 +631,10 @@ class StreamUri(Stream):
|
|
|
626
631
|
def dereference(self, asType=None):
|
|
627
632
|
drc = self.dereference_chain()
|
|
628
633
|
uri = drc[-1]
|
|
634
|
+
if uri is None:
|
|
635
|
+
# null pointer
|
|
636
|
+
self._resolution_raise_logic(drc[-2]._progenitor)
|
|
637
|
+
|
|
629
638
|
if asType:
|
|
630
639
|
nid = asType(uri)
|
|
631
640
|
if isinstance(nid, idlib.Stream):
|
|
@@ -668,7 +677,8 @@ class StreamUri(Stream):
|
|
|
668
677
|
@cache_result
|
|
669
678
|
def headers(self):
|
|
670
679
|
uri = self.dereference()
|
|
671
|
-
|
|
680
|
+
if uri is not None:
|
|
681
|
+
return uri.progenitor().headers
|
|
672
682
|
|
|
673
683
|
def data(self, mimetype_accept=None):
|
|
674
684
|
# FIXME TODO should the data associated with the doi
|
idlib/systems/doi.py
CHANGED
|
@@ -26,8 +26,8 @@ class DoiId(oq.OntId, idlib.Identifier, idlib.Stream): # also _technically_ a s
|
|
|
26
26
|
_namespaces = _DoiPrefixes
|
|
27
27
|
_local_conventions = _namespaces
|
|
28
28
|
_id_class = str # eventually we have to drop down to something
|
|
29
|
-
local_regex = '^10
|
|
30
|
-
canonical_regex = '^https
|
|
29
|
+
local_regex = '^10\\.[0-9]{4,9}[-._;()/:a-zA-Z0-9]+$'
|
|
30
|
+
canonical_regex = '^https://doi.org/10\\.[0-9]{4,9}[-._;()/:a-zA-Z0-9]+$'
|
|
31
31
|
|
|
32
32
|
def __new__(cls, doi_in_various_states_of_mangling=None, iri=None):
|
|
33
33
|
if doi_in_various_states_of_mangling is None and iri is not None:
|
idlib/systems/orcid.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import random
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
import ontquery as oq # temporary implementation detail
|
|
3
4
|
import idlib
|
|
@@ -13,6 +14,22 @@ from idlib.config import auth
|
|
|
13
14
|
# but I need more information ...
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def genorcid():
|
|
18
|
+
""" generate a random orcid-like id suffix """
|
|
19
|
+
_09 = '09'
|
|
20
|
+
_01 = ''.join([str(_) for _ in range(2)])
|
|
21
|
+
_0_9 = ''.join([str(_) for _ in range(10)])
|
|
22
|
+
base = (
|
|
23
|
+
'000' + random.choice(_09) + '-' +
|
|
24
|
+
'00' + random.choice(_01) + random.choice(_0_9) + '-' +
|
|
25
|
+
''.join(random.choice(_0_9) for _ in range(4))
|
|
26
|
+
+ '-' +
|
|
27
|
+
''.join(random.choice(_0_9) for _ in range(3)))
|
|
28
|
+
cs = OrcidId._digit_checksum(base.replace('-', ''))
|
|
29
|
+
fd = 'X' if cs == 10 else str(cs)
|
|
30
|
+
return base + fd
|
|
31
|
+
|
|
32
|
+
|
|
16
33
|
class OrcidPrefixes(conv.QnameAsLocalHelper, oq.OntCuries):
|
|
17
34
|
# set these manually since, sigh, factory patterns
|
|
18
35
|
_dict = {}
|
|
@@ -68,6 +85,16 @@ class OrcidId(oq.OntId, idlib.Identifier):
|
|
|
68
85
|
|
|
69
86
|
return self
|
|
70
87
|
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _digit_checksum(digits):
|
|
90
|
+
total = 0
|
|
91
|
+
for digit_string in digits:
|
|
92
|
+
total = (total + int(digit_string)) * 2
|
|
93
|
+
|
|
94
|
+
remainder = total % 11
|
|
95
|
+
result = (12 - remainder) % 11
|
|
96
|
+
return result
|
|
97
|
+
|
|
71
98
|
@property
|
|
72
99
|
def checksumValid(self):
|
|
73
100
|
""" see
|
|
@@ -77,12 +104,7 @@ class OrcidId(oq.OntId, idlib.Identifier):
|
|
|
77
104
|
try:
|
|
78
105
|
*digits, check_string = self.suffix.replace('-', '')
|
|
79
106
|
check = 10 if check_string == 'X' else int(check_string)
|
|
80
|
-
|
|
81
|
-
for digit_string in digits:
|
|
82
|
-
total = (total + int(digit_string)) * 2
|
|
83
|
-
|
|
84
|
-
remainder = total % 11
|
|
85
|
-
result = (12 - remainder) % 11
|
|
107
|
+
result = self._digit_checksum(digits)
|
|
86
108
|
return result == check
|
|
87
109
|
except ValueError as e:
|
|
88
110
|
raise self.OrcidChecksumError(self) from e
|
|
@@ -186,3 +208,25 @@ class Orcid(idlib.HelperNoData, idlib.Stream):
|
|
|
186
208
|
return (self.identifier.iri
|
|
187
209
|
if asType is None else
|
|
188
210
|
asType(self.identifier.iri))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class OrcidSandboxPrefixes(conv.QnameAsLocalHelper, oq.OntCuries):
|
|
214
|
+
# set these manually since, sigh, factory patterns
|
|
215
|
+
_dict = {}
|
|
216
|
+
_n_to_p = {}
|
|
217
|
+
_strie = {}
|
|
218
|
+
_trie = {}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
OrcidSandboxPrefixes({'orcid': 'https://sandbox.orcid.org/',
|
|
222
|
+
'ORCID': 'https://sandbox.orcid.org/',
|
|
223
|
+
'orcid.pub.3': 'https://pub.sandbox.orcid.org/v3.0/',})
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class OrcidIdSandbox(OrcidId):
|
|
227
|
+
_namespaces = OrcidSandboxPrefixes
|
|
228
|
+
canonical_regex = (f'^https://sandbox.orcid.org/{OrcidId._common}$')
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class OrcidSandbox(Orcid):
|
|
232
|
+
_id_class = OrcidIdSandbox
|
idlib/systems/ror.py
CHANGED
|
@@ -97,7 +97,7 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
97
97
|
def _checksum(self, cypher): # FIXME unqualified checksum goes to ... metadata ???
|
|
98
98
|
m = cypher()
|
|
99
99
|
metadata = self.metadata()
|
|
100
|
-
name = metadata
|
|
100
|
+
name = self._name(metadata)
|
|
101
101
|
m.update(self.identifier.checksum(cypher))
|
|
102
102
|
m.update(self.id_bound_metadata.checksum(cypher))
|
|
103
103
|
m.update(name.encode()) # unix epoch -> ??
|
|
@@ -143,9 +143,19 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
143
143
|
# or the remote telling us that we are in error
|
|
144
144
|
raise exc.RemoteError(self.identifier) from e
|
|
145
145
|
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _name(m):
|
|
148
|
+
if 'names' in m:
|
|
149
|
+
for name in m['names']:
|
|
150
|
+
if 'ror_display' in name['types']:
|
|
151
|
+
return name['value']
|
|
152
|
+
else:
|
|
153
|
+
return m['name']
|
|
154
|
+
|
|
146
155
|
@property
|
|
147
156
|
def name(self):
|
|
148
|
-
|
|
157
|
+
m = self.metadata()
|
|
158
|
+
return self._name(m)
|
|
149
159
|
|
|
150
160
|
def asExternalId(self, id_class):
|
|
151
161
|
eids = self.data['external_ids']
|
|
@@ -167,8 +177,14 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
167
177
|
'Healthcare': 'Institution',
|
|
168
178
|
'Facility': 'CoreFacility',
|
|
169
179
|
'Nonprofit': 'Nonprofit',
|
|
180
|
+
'Funder': 'Funder', # FIXME funder is a role ...
|
|
170
181
|
'Other': 'Institution',
|
|
171
182
|
}
|
|
183
|
+
for _k, _v in list(_type_map.items()):
|
|
184
|
+
_type_map[_k.lower()] = _v
|
|
185
|
+
del _k
|
|
186
|
+
del _v
|
|
187
|
+
|
|
172
188
|
@property
|
|
173
189
|
def institutionTypes(self):
|
|
174
190
|
metadata = self.metadata()
|
|
@@ -213,19 +229,29 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
213
229
|
def synonyms_rdf(self, rdflib): # FIXME annoying
|
|
214
230
|
d = self.metadata()
|
|
215
231
|
# FIXME how to deal with type conversion an a saner way ...
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
if 'names' in d:
|
|
233
|
+
for name in d['names']:
|
|
234
|
+
if 'ror_display' not in name['types']:
|
|
235
|
+
yield rdflib.Literal(name['value'], lang=name['lang'])
|
|
236
|
+
else:
|
|
237
|
+
yield from [rdflib.Literal(s) for s in d['aliases']]
|
|
238
|
+
yield from [rdflib.Literal(s) for s in d['acronyms']]
|
|
239
|
+
yield from [rdflib.Literal(l['label'], lang=l['iso639']) for l in d['labels']]
|
|
219
240
|
|
|
220
241
|
@property
|
|
221
242
|
def synonyms(self):
|
|
222
243
|
out = []
|
|
223
244
|
m = self.metadata()
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
if 'names' in m:
|
|
246
|
+
for name in m['names']:
|
|
247
|
+
if 'ror_display' not in name['types']:
|
|
248
|
+
out.append(name['value'])
|
|
249
|
+
else:
|
|
250
|
+
for a in m['aliases'] + m['acronyms']:
|
|
251
|
+
out.append(a)
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
253
|
+
for l in m['labels']:
|
|
254
|
+
out.append(l['label'])
|
|
229
255
|
|
|
230
256
|
return out
|
|
231
257
|
|
idlib/systems/rrid.py
CHANGED
|
@@ -175,8 +175,52 @@ class Rrid(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
175
175
|
if descs:
|
|
176
176
|
return sorted(descs, key=len, reverse=True)[0]
|
|
177
177
|
|
|
178
|
+
def proper_citation(self):
|
|
179
|
+
ms = self.metadata()
|
|
180
|
+
if ms:
|
|
181
|
+
cites = set(m['rrid']['properCitation'] for m in ms if 'rrid' in m and 'properCitation' in m['rrid'])
|
|
182
|
+
if cites:
|
|
183
|
+
return sorted(cites, key=len, reverse=True)[0]
|
|
184
|
+
|
|
185
|
+
# output streams
|
|
186
|
+
|
|
187
|
+
def _triples_gen(self,
|
|
188
|
+
rdflib=None,
|
|
189
|
+
rdf=None,
|
|
190
|
+
rdfs=None,
|
|
191
|
+
owl=None,
|
|
192
|
+
NIFRID=None,
|
|
193
|
+
TEMP=None,
|
|
194
|
+
**kwargs):
|
|
195
|
+
""" implementation of method to produce a
|
|
196
|
+
triplified version of the record """
|
|
197
|
+
s = self.asUri(rdflib.URIRef)
|
|
198
|
+
yield s, rdf.type, owl.NamedIndividual
|
|
199
|
+
try:
|
|
200
|
+
pc = self.proper_citation()
|
|
201
|
+
if pc:
|
|
202
|
+
yield s, TEMP.properCitation, rdflib.Literal(pc)
|
|
203
|
+
except exc.ResolutionError as e:
|
|
204
|
+
log.exception(e)
|
|
205
|
+
yield s, TEMP.resolutionError, rdflib.Literal(True)
|
|
206
|
+
return
|
|
207
|
+
except exc.RemoteError as e:
|
|
208
|
+
log.exception(e)
|
|
209
|
+
yield s, TEMP.remoteError, rdflib.Literal(True)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
yield s, rdfs.label, rdflib.Literal(self.label)
|
|
213
|
+
|
|
178
214
|
# alternate representations
|
|
179
215
|
|
|
216
|
+
def asDict(self):
|
|
217
|
+
out = super().asDict()
|
|
218
|
+
pc = self.proper_citation()
|
|
219
|
+
if pc:
|
|
220
|
+
out['proper_citation'] = pc
|
|
221
|
+
|
|
222
|
+
return out
|
|
223
|
+
|
|
180
224
|
def asUri(self, asType=None):
|
|
181
225
|
# TODO n2t, identifiers.org
|
|
182
226
|
# TODO TODO having an explicit model for resolver/metadata services
|
idlib/utils.py
CHANGED
|
@@ -124,7 +124,12 @@ def makeEnc(alphabet):
|
|
|
124
124
|
def decode(chars, *, _index=index, _base=base):
|
|
125
125
|
number = 0
|
|
126
126
|
for c in chars:
|
|
127
|
-
|
|
127
|
+
try:
|
|
128
|
+
cind = _index[c]
|
|
129
|
+
except KeyError as e:
|
|
130
|
+
msg = ''.join(chars)
|
|
131
|
+
raise exc.MalformedIdentifierError(msg) from e
|
|
132
|
+
number = number * _base + cind
|
|
128
133
|
|
|
129
134
|
return number
|
|
130
135
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: idlib
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev23
|
|
4
4
|
Summary: A library for working with identifiers of all kinds.
|
|
5
5
|
Home-page: https://github.com/tgbugs/idlib
|
|
6
6
|
Author: Tom Gillespie
|
|
@@ -9,34 +9,48 @@ License: MIT
|
|
|
9
9
|
Keywords: python persistent identifiers
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
21
|
Classifier: Operating System :: POSIX :: Linux
|
|
21
22
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
22
23
|
Classifier: Operating System :: Microsoft :: Windows
|
|
23
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.7
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: orthauth[sxpr,yaml]
|
|
27
|
+
Requires-Dist: orthauth[sxpr,yaml]>=0.0.18
|
|
27
28
|
Requires-Dist: requests
|
|
28
|
-
Provides-Extra:
|
|
29
|
-
Requires-Dist:
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Requires-Dist: joblib>=1.1.0; extra == "test"
|
|
32
|
+
Requires-Dist: beautifulsoup4[html5lib]; extra == "test"
|
|
33
|
+
Requires-Dist: pyontutils>=0.1.28; extra == "test"
|
|
34
|
+
Requires-Dist: google-auth-oauthlib; extra == "test"
|
|
30
35
|
Provides-Extra: org
|
|
31
|
-
Requires-Dist: beautifulsoup4[html5lib]
|
|
36
|
+
Requires-Dist: beautifulsoup4[html5lib]; extra == "org"
|
|
32
37
|
Provides-Extra: rdf
|
|
33
|
-
Requires-Dist: pyontutils
|
|
34
|
-
Provides-Extra:
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
Requires-Dist: pyontutils>=0.1.28; extra == "rdf"
|
|
39
|
+
Provides-Extra: oauth
|
|
40
|
+
Requires-Dist: google-auth-oauthlib; extra == "oauth"
|
|
41
|
+
Dynamic: author
|
|
42
|
+
Dynamic: author-email
|
|
43
|
+
Dynamic: classifier
|
|
44
|
+
Dynamic: description
|
|
45
|
+
Dynamic: description-content-type
|
|
46
|
+
Dynamic: home-page
|
|
47
|
+
Dynamic: keywords
|
|
48
|
+
Dynamic: license
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
Dynamic: provides-extra
|
|
51
|
+
Dynamic: requires-dist
|
|
52
|
+
Dynamic: requires-python
|
|
53
|
+
Dynamic: summary
|
|
40
54
|
|
|
41
55
|
# idlib
|
|
42
56
|
[](https://pypi.org/project/idlib/)
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
idlib/__init__.py,sha256=
|
|
1
|
+
idlib/__init__.py,sha256=PMFL7ZemlNHqabBZ_MyLbYcaxaGBU1ET-nN9BlflWHE,1696
|
|
2
2
|
idlib/auth-config.py,sha256=tYM2aYCYJFG5NB4Af1dSn6YxhafttOp7W7aSc-QbVV8,460
|
|
3
3
|
idlib/cache.py,sha256=lR1I3iEYvBhNeqoptS-Sj7SpIHt1K65nYlBrTK-ROVQ,3875
|
|
4
4
|
idlib/config.py,sha256=Gli2tZlbKxBos8sXDoDK1g9mngfDw1ZWpBt_7MDZH-g,76
|
|
5
|
-
idlib/core.py,sha256=
|
|
5
|
+
idlib/core.py,sha256=KVhWN6yQfIGA5rG_ydNVC25oMa88lYJwQtAjzzhqziQ,4569
|
|
6
6
|
idlib/edit.py,sha256=s3yRtvNelQUA8fziu_igZeoFsW4xCK_kgfNVv3TS0iE,3635
|
|
7
|
-
idlib/exceptions.py,sha256=
|
|
8
|
-
idlib/from_oq.py,sha256=
|
|
7
|
+
idlib/exceptions.py,sha256=QC3Jq98iwezcqB7SmDr-UEOqdNCnvEXVkKB0eJLIwZs,4714
|
|
8
|
+
idlib/from_oq.py,sha256=kt4BpbAZgLo1CwxwyR_swnlVMsekV7ePfZivq9V8yns,95599
|
|
9
9
|
idlib/identifiers.py,sha256=s39bAS-fCQrlwAbMtaa7tjSnqKq4YPlfSbfJ6ju2o7I,3861
|
|
10
|
-
idlib/streams.py,sha256=
|
|
11
|
-
idlib/utils.py,sha256=
|
|
10
|
+
idlib/streams.py,sha256=QhOtT3xnp8FGVt7GEYAFAnd7PKYRKBPbOF6XGit63lQ,26765
|
|
11
|
+
idlib/utils.py,sha256=q2hcz0Qtbya1UpPnlGisougg9EuDZkcAKXJQIN4OFQs,5306
|
|
12
12
|
idlib/apis/__init__.py,sha256=q2-iSMUDPlYbjqGWa1mtbe2G95N7wbjWHGj30BUdVDM,88
|
|
13
|
-
idlib/apis/protocols_io.py,sha256=
|
|
13
|
+
idlib/apis/protocols_io.py,sha256=PKcoSbO9AqTfBYKLWJQyO3vHjug9qEL826s7jY4LWWg,4026
|
|
14
14
|
idlib/conventions/__init__.py,sha256=ug6W8eNHuq9JZy5bwrZ6Rjfu8r81Rrnm0nzApmvvMzw,204
|
|
15
15
|
idlib/conventions/core.py,sha256=g0T1VoNpoC9afvUd0wp6Mu7tRxbPDFK59uwVXV-zTtA,940
|
|
16
16
|
idlib/conventions/local.py,sha256=H1WnXTY06aLq0RB_SsTLVhMZiSP9GGvWuWm_zrFUNmQ,129
|
|
17
|
-
idlib/conventions/type.py,sha256=
|
|
17
|
+
idlib/conventions/type.py,sha256=PM_BkxP5a7BTOSsb77kMmMRF2u5xMdlptIa4QYIaE6Y,1013
|
|
18
18
|
idlib/formats/__init__.py,sha256=yS6l0mbDwa2xjyxhgVzlqIf0LPqJmuhNVg8rfKSsh7M,528
|
|
19
19
|
idlib/formats/rdf.py,sha256=yIKHPaPn-SUuUMkR4VD7KffVgQD8oNaZNxcqDBilxtg,466
|
|
20
20
|
idlib/systems/__init__.py,sha256=3lWSK2DyzYX0d61DJo-gGytc-HoiOlNJl5so4s7Grrg,203
|
|
21
21
|
idlib/systems/ark.py,sha256=i9JqmuTWsJABrLHR0-_FX_PBbbpeFa7X4CO65QhyOD8,111
|
|
22
|
-
idlib/systems/doi.py,sha256=
|
|
22
|
+
idlib/systems/doi.py,sha256=PQAstSUocPpK9Ug7L-eKvJrQhn3jRU9WuKITGyZoL9g,13100
|
|
23
23
|
idlib/systems/handle.py,sha256=Gg97IuysdAdCX9j6EhuNfIddb1GDq0CmwW1SmuLkpgM,95
|
|
24
24
|
idlib/systems/isni.py,sha256=TgiSTlDcSBLRro3gg_YfQJQkfaJ3saw0Au1OMhE23wU,89
|
|
25
|
-
idlib/systems/orcid.py,sha256=
|
|
25
|
+
idlib/systems/orcid.py,sha256=aTVu_Q6fnaQvqmd6jeXdst6yKRTO-BEUMs7_4fdQK5E,7886
|
|
26
26
|
idlib/systems/pmid.py,sha256=Na-4_n7qmJ3VAbG0RUwnNTg7o4Z8OkuL_Hluq_A3IO0,181
|
|
27
|
-
idlib/systems/ror.py,sha256=
|
|
28
|
-
idlib/systems/rrid.py,sha256=
|
|
27
|
+
idlib/systems/ror.py,sha256=3pZ1aLg7NUh-mHQm-qu-hqy5WrJZprtj5lWIWRpP2-M,8894
|
|
28
|
+
idlib/systems/rrid.py,sha256=RcDn3-BRmrKzzxDCypSWRQuivKvWqUqqohY8BeSqRF4,8278
|
|
29
29
|
idlib/systems/urn.py,sha256=p222KjHgfF6y9srCm7kFWuWA9NK4lI-wEQUtSeuBqYk,113
|
|
30
30
|
idlib/systems/web.py,sha256=vrSbQYWnxhe9pSgZcOqwLM3V6XQDLwv1QqWlhWWxbL0,1291
|
|
31
|
-
idlib-0.0.1.
|
|
32
|
-
idlib-0.0.1.
|
|
33
|
-
idlib-0.0.1.
|
|
34
|
-
idlib-0.0.1.
|
|
35
|
-
idlib-0.0.1.
|
|
31
|
+
idlib-0.0.1.dev23.dist-info/licenses/LICENSE,sha256=nEuJBsoHfpXGgf6ZyFGUhQJqxFWwhSYF-45pRAqTSb4,1080
|
|
32
|
+
idlib-0.0.1.dev23.dist-info/METADATA,sha256=leTa1rrRwvU4n5ucULjBXh9zzBHvsjXNE8nj3-qWPog,2330
|
|
33
|
+
idlib-0.0.1.dev23.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
|
|
34
|
+
idlib-0.0.1.dev23.dist-info/top_level.txt,sha256=xIk87gJPbeCKih9BVMP8PlEFdpcsGplenz0VQhx5sP8,6
|
|
35
|
+
idlib-0.0.1.dev23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|