idlib 0.0.1.dev22__tar.gz → 0.0.1.dev24__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.
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/PKG-INFO +18 -5
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/__init__.py +1 -1
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/apis/protocols_io.py +9 -5
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/cache.py +16 -3
- idlib-0.0.1.dev24/idlib/core.py +116 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/from_oq.py +51 -3
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/streams.py +34 -6
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/doi.py +23 -8
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/ror.py +34 -9
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/rrid.py +69 -3
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/utils.py +6 -1
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib.egg-info/PKG-INFO +18 -5
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib.egg-info/requires.txt +2 -2
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/setup.py +2 -2
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/test/test_identifiers.py +11 -1
- idlib-0.0.1.dev22/idlib/core.py +0 -77
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/LICENSE +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/Pipfile +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/README.md +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/docs/identifiers.org +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/docs/stream-grammer.org +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/apis/__init__.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/auth-config.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/config.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/conventions/__init__.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/conventions/core.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/conventions/local.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/conventions/type.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/edit.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/exceptions.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/formats/__init__.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/formats/rdf.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/identifiers.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/__init__.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/ark.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/handle.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/isni.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/orcid.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/pmid.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/urn.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib/systems/web.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib.egg-info/SOURCES.txt +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib.egg-info/dependency_links.txt +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/idlib.egg-info/top_level.txt +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/setup.cfg +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/test/__init__.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/test/test_cache.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/test/test_simple.py +0 -0
- {idlib-0.0.1.dev22 → idlib-0.0.1.dev24}/test/test_utils.py +0 -0
|
@@ -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.dev24
|
|
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
|
|
@@ -8,7 +8,6 @@ Author-email: tgbugs@gmail.com
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: python persistent identifiers
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.7
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -16,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
21
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -30,14 +30,27 @@ Provides-Extra: test
|
|
|
30
30
|
Requires-Dist: pytest; extra == "test"
|
|
31
31
|
Requires-Dist: joblib>=1.1.0; extra == "test"
|
|
32
32
|
Requires-Dist: beautifulsoup4[html5lib]; extra == "test"
|
|
33
|
-
Requires-Dist: pyontutils>=0.1.
|
|
33
|
+
Requires-Dist: pyontutils>=0.1.40; extra == "test"
|
|
34
34
|
Requires-Dist: google-auth-oauthlib; extra == "test"
|
|
35
35
|
Provides-Extra: org
|
|
36
36
|
Requires-Dist: beautifulsoup4[html5lib]; extra == "org"
|
|
37
37
|
Provides-Extra: rdf
|
|
38
|
-
Requires-Dist: pyontutils>=0.1.
|
|
38
|
+
Requires-Dist: pyontutils>=0.1.40; extra == "rdf"
|
|
39
39
|
Provides-Extra: oauth
|
|
40
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
|
|
41
54
|
|
|
42
55
|
# idlib
|
|
43
56
|
[](https://pypi.org/project/idlib/)
|
|
@@ -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import pickle
|
|
2
3
|
import hashlib
|
|
3
4
|
import inspect
|
|
4
5
|
from pathlib import Path
|
|
@@ -66,6 +67,10 @@ def cache(folder, ser='json', clear_cache=False, create=False, return_path=False
|
|
|
66
67
|
serialize = json.dump
|
|
67
68
|
deserialize = json.load
|
|
68
69
|
mode = 't'
|
|
70
|
+
elif ser == 'pickle':
|
|
71
|
+
serialize = pickle.dump
|
|
72
|
+
deserialize = pickle.load
|
|
73
|
+
mode = 'b'
|
|
69
74
|
else:
|
|
70
75
|
raise TypeError('Bad serialization format.')
|
|
71
76
|
|
|
@@ -77,9 +82,11 @@ def cache(folder, ser='json', clear_cache=False, create=False, return_path=False
|
|
|
77
82
|
if not create:
|
|
78
83
|
raise FileNotFoundError(f'Cache base folder does not exist! {folder}')
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
# TOCTOU race condition is hit idlib is imported by multi-process workers
|
|
86
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
81
87
|
|
|
82
88
|
if clear_cache:
|
|
89
|
+
import shutil
|
|
83
90
|
log.debug(f'clearing cache for {folder}')
|
|
84
91
|
shutil.rmtree(folder)
|
|
85
92
|
folder.mkdir()
|
|
@@ -100,8 +107,14 @@ def cache(folder, ser='json', clear_cache=False, create=False, return_path=False
|
|
|
100
107
|
else:
|
|
101
108
|
output = function(*args, **kwargs)
|
|
102
109
|
if output is not None:
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
try:
|
|
111
|
+
with open(filepath, write_mode) as f:
|
|
112
|
+
serialize(output, f)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
filepath.unlink()
|
|
115
|
+
# TODO remove serialization on failure so we
|
|
116
|
+
# don't think a real cache exists
|
|
117
|
+
raise e
|
|
105
118
|
|
|
106
119
|
if isinstance(output, dict) and COOLDOWN in output:
|
|
107
120
|
# a hack to put a dummy variable in the cache
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from . import exceptions as exc
|
|
3
|
+
from .utils import log
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolution_chain(iri, headers_fun=None):
|
|
7
|
+
for head in resolution_chain_responses(iri, headers_fun=headers_fun):
|
|
8
|
+
yield head.url
|
|
9
|
+
|
|
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):
|
|
79
|
+
#doi = doi # TODO
|
|
80
|
+
s = requests.Session()
|
|
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
|
+
|
|
88
|
+
while head.is_redirect and head.status_code < 400: # FIXME redirect loop issue
|
|
89
|
+
yield head.next
|
|
90
|
+
try:
|
|
91
|
+
head = s.send(head.next)
|
|
92
|
+
except requests.exceptions.SSLError as e:
|
|
93
|
+
raise exc.InbetweenError(msg) from e
|
|
94
|
+
|
|
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
|
+
|
|
101
|
+
if not head.is_redirect:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
if raise_on_final: # we still want the chain ... null pointer error comes later?
|
|
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
|
|
@@ -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
|
|
|
@@ -419,6 +432,10 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
419
432
|
if data:
|
|
420
433
|
doi = data['doi']
|
|
421
434
|
if doi:
|
|
435
|
+
if not doi.startswith('http'):
|
|
436
|
+
# why is there no scheme? who knows!?
|
|
437
|
+
doi = 'https://' + doi
|
|
438
|
+
|
|
422
439
|
return idlib.Doi(doi)
|
|
423
440
|
|
|
424
441
|
@property
|
|
@@ -645,7 +662,7 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
645
662
|
return data
|
|
646
663
|
|
|
647
664
|
_apiuri_private_cache = {} # XXX FIXME hack
|
|
648
|
-
def data4(self, fail_ok=False):
|
|
665
|
+
def data4(self, fail_ok=False, try_refresh_cache=False):
|
|
649
666
|
if not hasattr(self, '_data'):
|
|
650
667
|
self._data_in_error = True
|
|
651
668
|
if not isinstance(self._progenitors, dict):
|
|
@@ -661,7 +678,7 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
661
678
|
# however this is not trivial to retrive
|
|
662
679
|
# the /v3/protocols/{id} endpoint returns bad/mangled data and we need
|
|
663
680
|
# the /v1/ data to be able to actually render to org
|
|
664
|
-
blob, path = self._get_data(apiuri)
|
|
681
|
+
blob, path = self._get_data(apiuri, _refresh_cache=try_refresh_cache)
|
|
665
682
|
if 'stream-http' not in self._progenitors:
|
|
666
683
|
self._progenitors['path'] = path
|
|
667
684
|
|
|
@@ -690,6 +707,16 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
690
707
|
if fail_ok: return
|
|
691
708
|
raise exc.IdDoesNotExistError(message)
|
|
692
709
|
elif sc in (250, 205): # access requested, not authorized
|
|
710
|
+
if path_older_than_day(path):
|
|
711
|
+
# there seems to be a case where a protocol is shared
|
|
712
|
+
# with us and there is an old cache, in that case we
|
|
713
|
+
# hit an infinite loop because getting data for the
|
|
714
|
+
# progenitor says we should be able to resolve the
|
|
715
|
+
# final protocol now but it was cached on cooldown, if
|
|
716
|
+
# we can get to the first progenitor then we likely
|
|
717
|
+
# have access to the final url and get will succeed
|
|
718
|
+
return self.data4(fail_ok=fail_ok, try_refresh_cache=True)
|
|
719
|
+
|
|
693
720
|
try:
|
|
694
721
|
# there might be a private id in the progenitor chain
|
|
695
722
|
nself = self.progenitor(type='id-converted-from')
|
|
@@ -735,6 +762,11 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
735
762
|
# and pio json mean there is an extra layer that needs
|
|
736
763
|
# interpretation ... i.e. we definitely got to the remote
|
|
737
764
|
raise NotImplementedError(msg)
|
|
765
|
+
elif sc == 1266:
|
|
766
|
+
msg = 'rate limit exceeded'
|
|
767
|
+
if fail_ok: return
|
|
768
|
+
path.unlink() # hopefully works next time?
|
|
769
|
+
raise exc.AccessLimitError(msg)
|
|
738
770
|
else:
|
|
739
771
|
msg = f'unhandled pio status code {sc}\n' + message
|
|
740
772
|
raise NotImplementedError(msg)
|
|
@@ -793,8 +825,10 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
793
825
|
# for how we got there, even if we toss the crazy slugs
|
|
794
826
|
self._progenitors = {} # FIXME yes this blasts progens every time
|
|
795
827
|
uh = self.uri_human
|
|
828
|
+
_sigh = uh._progenitors.pop('id-converted-from') # avoid infinite recursion
|
|
796
829
|
uai = uh.uri_api_int
|
|
797
830
|
d4 = uai.data4(fail_ok=fail_ok)
|
|
831
|
+
uh._progenitors['id-converted-from'] = _sigh
|
|
798
832
|
self._progenitors['id-converted-to'] = uai
|
|
799
833
|
if 'path' in uai._progenitors:
|
|
800
834
|
self._progenitors['path'] = uai._progenitors['path']
|
|
@@ -810,12 +844,18 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
810
844
|
except Exception as e0:
|
|
811
845
|
try:
|
|
812
846
|
return self.data1(fail_ok=fail_ok)
|
|
847
|
+
except exc.IdDoesNotExistError as ene1:
|
|
848
|
+
raise ene1
|
|
813
849
|
except Exception as e1:
|
|
814
|
-
|
|
850
|
+
# FIXME error type ...
|
|
851
|
+
raise exc.InbetweenError() from e1
|
|
815
852
|
else:
|
|
816
853
|
return self.data4(fail_ok=fail_ok)
|
|
817
854
|
except (AttributeError, NotImplementedError) as e:
|
|
818
855
|
raise e
|
|
856
|
+
except exc.IdDoesNotExistError as e:
|
|
857
|
+
# don't wait for pioids that don't exist
|
|
858
|
+
raise e
|
|
819
859
|
except Exception as e:
|
|
820
860
|
wait_until = time() + self._wait_time
|
|
821
861
|
self._data_in_error_sigh = wait_until, e
|
|
@@ -2041,6 +2081,14 @@ class Pio(formats.Rdf, idlib.Stream):
|
|
|
2041
2081
|
if include_private and self.identifier.is_private():
|
|
2042
2082
|
out['uri_private'] = self.identifier # FIXME some way to avoid leaking these if needed?
|
|
2043
2083
|
return out
|
|
2084
|
+
except exc.MalformedIdentifierError as e:
|
|
2085
|
+
out = super().asDict(include_description)
|
|
2086
|
+
out['malformed'] = True
|
|
2087
|
+
if 'errors' not in out:
|
|
2088
|
+
out['errors'] = []
|
|
2089
|
+
|
|
2090
|
+
out['errors'].append({'message': 'malformed'})
|
|
2091
|
+
return out
|
|
2044
2092
|
except exc.RemoteError as e:
|
|
2045
2093
|
# we don't have any metadata but we will return what little info we have
|
|
2046
2094
|
return super().asDict(include_description)
|
|
@@ -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):
|
|
@@ -437,7 +440,12 @@ class HelpTestStreams:
|
|
|
437
440
|
HelpTestStreams._copy = copy
|
|
438
441
|
import pickle
|
|
439
442
|
HelpTestStreams._pickle = pickle
|
|
440
|
-
|
|
443
|
+
try:
|
|
444
|
+
from joblib import Parallel, delayed
|
|
445
|
+
except Exception as e:
|
|
446
|
+
log.exception(e)
|
|
447
|
+
from pyontutils.asyncd import Async as Parallel, deferred as delayed
|
|
448
|
+
|
|
441
449
|
HelpTestStreams._Parallel = Parallel
|
|
442
450
|
HelpTestStreams._delayed = delayed
|
|
443
451
|
|
|
@@ -461,7 +469,11 @@ class HelpTestStreams:
|
|
|
461
469
|
print(d.identifier.prefix)
|
|
462
470
|
print(d.identifier.suffix)
|
|
463
471
|
if d.identifier.prefix is None or d.identifier.suffix is None:
|
|
464
|
-
|
|
472
|
+
if hasattr(d.identifier, 'iri') and hasattr(d.identifier, 'valid') and d.identifier.valid:
|
|
473
|
+
# doi case
|
|
474
|
+
pass
|
|
475
|
+
else:
|
|
476
|
+
bads.append(d.identifier.__dict__)
|
|
465
477
|
|
|
466
478
|
try:
|
|
467
479
|
d.identifier_bound_metadata
|
|
@@ -498,9 +510,18 @@ class HelpTestStreams:
|
|
|
498
510
|
|
|
499
511
|
def test_malformed(self):
|
|
500
512
|
bads = []
|
|
501
|
-
|
|
513
|
+
vbads = []
|
|
514
|
+
pre_garbage = 'oops '
|
|
515
|
+
post_garbage = ' oops'
|
|
516
|
+
bad_ids = (
|
|
517
|
+
list(self.ids_bad) +
|
|
518
|
+
[pre_garbage + i for i in self.ids] +
|
|
519
|
+
[i + post_garbage for i in self.ids])
|
|
520
|
+
for i in bad_ids:
|
|
502
521
|
try:
|
|
503
522
|
d = self.stream(i)
|
|
523
|
+
d.identifier.validate()
|
|
524
|
+
vbads.append(d)
|
|
504
525
|
d.data()
|
|
505
526
|
bads.append(d)
|
|
506
527
|
except Exception as e:
|
|
@@ -508,6 +529,7 @@ class HelpTestStreams:
|
|
|
508
529
|
pass
|
|
509
530
|
|
|
510
531
|
assert not bads, bads
|
|
532
|
+
assert not vbads, vbads
|
|
511
533
|
|
|
512
534
|
def test_hash_eq_id(self):
|
|
513
535
|
hrm = self.ids[0]
|
|
@@ -608,8 +630,10 @@ class StreamUri(Stream):
|
|
|
608
630
|
dc[self.identifier_actionable] = tuple(
|
|
609
631
|
resp if resp is None else StringProgenitor(resp.url, progenitor=resp)
|
|
610
632
|
for resp in
|
|
611
|
-
self._resolution_chain_responses(
|
|
612
|
-
|
|
633
|
+
self._resolution_chain_responses(
|
|
634
|
+
self.identifier_actionable,
|
|
635
|
+
raise_on_final=False,
|
|
636
|
+
headers_fun=self._resolution_chain_headers_fun))
|
|
613
637
|
except (exc.ResolutionError, exc.RemoteError) as e:
|
|
614
638
|
# TODO
|
|
615
639
|
# FIXME partial resolution is a complete nightmare
|
|
@@ -626,6 +650,10 @@ class StreamUri(Stream):
|
|
|
626
650
|
def dereference(self, asType=None):
|
|
627
651
|
drc = self.dereference_chain()
|
|
628
652
|
uri = drc[-1]
|
|
653
|
+
if uri is None:
|
|
654
|
+
# null pointer
|
|
655
|
+
self._resolution_raise_logic(drc[-2]._progenitor)
|
|
656
|
+
|
|
629
657
|
if asType:
|
|
630
658
|
nid = asType(uri)
|
|
631
659
|
if isinstance(nid, idlib.Stream):
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import unquote, quote
|
|
1
3
|
import ontquery as oq # temp implementation detail
|
|
2
4
|
import idlib
|
|
3
5
|
from idlib import formats
|
|
@@ -26,8 +28,11 @@ class DoiId(oq.OntId, idlib.Identifier, idlib.Stream): # also _technically_ a s
|
|
|
26
28
|
_namespaces = _DoiPrefixes
|
|
27
29
|
_local_conventions = _namespaces
|
|
28
30
|
_id_class = str # eventually we have to drop down to something
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
# https://www.crossref.org/blog/dois-and-matching-regular-expressions/
|
|
32
|
+
# but we have cases that don't follow in our set ... so we may need a variant regex
|
|
33
|
+
# so we can warn if we see one
|
|
34
|
+
local_regex = '^10\\.[0-9]{4,9}/[-._;()<>/:a-zA-Z0-9]+$'
|
|
35
|
+
canonical_regex = '^https://doi.org/10\\.[0-9]{4,9}/[-._;()<>/:a-zA-Z0-9]+$'
|
|
31
36
|
|
|
32
37
|
def __new__(cls, doi_in_various_states_of_mangling=None, iri=None):
|
|
33
38
|
if doi_in_various_states_of_mangling is None and iri is not None:
|
|
@@ -59,22 +64,29 @@ class DoiId(oq.OntId, idlib.Identifier, idlib.Stream): # also _technically_ a s
|
|
|
59
64
|
|
|
60
65
|
@staticmethod
|
|
61
66
|
def normalize(doi):
|
|
62
|
-
doi = doi.replace(' ', '')
|
|
63
|
-
if 'http'
|
|
67
|
+
doi = doi.strip().replace(' :', ':').replace(': ', ':')
|
|
68
|
+
if doi.startswith('http') or doi.startswith('doi.org'):
|
|
64
69
|
doi = '10.' + doi.split('.org/10.', 1)[-1]
|
|
70
|
+
# space is left in quote to ensure we produce an error because there should be no spaces
|
|
71
|
+
doi = quote(unquote(doi), safe='-._;()/: ')
|
|
65
72
|
elif doi.startswith('doi:'):
|
|
66
|
-
doi = doi.strip('doi:')
|
|
73
|
+
doi = doi.strip('doi:').strip()
|
|
67
74
|
elif doi.startswith('DOI:'):
|
|
68
|
-
doi = doi.strip('DOI:')
|
|
75
|
+
doi = doi.strip('DOI:').strip()
|
|
76
|
+
|
|
69
77
|
return doi
|
|
70
78
|
|
|
71
79
|
@property
|
|
72
80
|
def valid(self):
|
|
73
|
-
|
|
81
|
+
if self.curie is None:
|
|
82
|
+
# we may have one of the pathalogical cases
|
|
83
|
+
return re.match(self.canonical_regex, unquote(self.iri))
|
|
84
|
+
else:
|
|
85
|
+
return self.suffix is not None and self.suffix.startswith('10.')
|
|
74
86
|
|
|
75
87
|
def validate(self):
|
|
76
88
|
if not self.valid:
|
|
77
|
-
raise exc.MalformedIdentifierError(f'{self._unnormalized} does not appear '
|
|
89
|
+
raise exc.MalformedIdentifierError(f'{self._unnormalized!r} does not appear '
|
|
78
90
|
f'to be of type {self.__class__}')
|
|
79
91
|
|
|
80
92
|
|
|
@@ -97,6 +109,9 @@ class Doi(formats.Rdf, idlib.Stream): # FIXME that 'has canonical representaito
|
|
|
97
109
|
def __init__(self, doi_in_various_states_of_mangling=None, iri=None):
|
|
98
110
|
self._identifier = self._id_class(doi_in_various_states_of_mangling, iri)
|
|
99
111
|
|
|
112
|
+
def __repr__(self):
|
|
113
|
+
return f'<idlib.{self.__class__.__name__}({self._identifier.asStr()!r})>'
|
|
114
|
+
|
|
100
115
|
def __gt__(self, other):
|
|
101
116
|
if isinstance(other, idlib.Stream):
|
|
102
117
|
return self.identifier > other.identifier
|
|
@@ -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']
|
|
@@ -170,6 +180,11 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
170
180
|
'Funder': 'Funder', # FIXME funder is a role ...
|
|
171
181
|
'Other': 'Institution',
|
|
172
182
|
}
|
|
183
|
+
for _k, _v in list(_type_map.items()):
|
|
184
|
+
_type_map[_k.lower()] = _v
|
|
185
|
+
del _k
|
|
186
|
+
del _v
|
|
187
|
+
|
|
173
188
|
@property
|
|
174
189
|
def institutionTypes(self):
|
|
175
190
|
metadata = self.metadata()
|
|
@@ -214,19 +229,29 @@ class Ror(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
214
229
|
def synonyms_rdf(self, rdflib): # FIXME annoying
|
|
215
230
|
d = self.metadata()
|
|
216
231
|
# FIXME how to deal with type conversion an a saner way ...
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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']]
|
|
220
240
|
|
|
221
241
|
@property
|
|
222
242
|
def synonyms(self):
|
|
223
243
|
out = []
|
|
224
244
|
m = self.metadata()
|
|
225
|
-
|
|
226
|
-
|
|
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)
|
|
227
252
|
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
for l in m['labels']:
|
|
254
|
+
out.append(l['label'])
|
|
230
255
|
|
|
231
256
|
return out
|
|
232
257
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from time import sleep
|
|
2
3
|
import idlib
|
|
3
4
|
from idlib import formats
|
|
4
5
|
from idlib import streams
|
|
@@ -11,12 +12,12 @@ from idlib.config import auth
|
|
|
11
12
|
|
|
12
13
|
class RridId(idlib.Identifier):
|
|
13
14
|
|
|
14
|
-
canonical_regex = 'RRID:([A-Za-z]+)_([A-Za-z0-9_:-]+)'
|
|
15
|
+
canonical_regex = '^RRID:([A-Za-z]+)_([A-Za-z0-9_:-]+)$'
|
|
15
16
|
# NOTE: the canonical regex fails to capture a couple
|
|
16
17
|
# of legacy cases e.g. RRID:WB-STRAIN_ and RRID:MGI:
|
|
17
18
|
# yes I understand why MGI doesn't want RRID:MGI_MGI:
|
|
18
19
|
# but what other identifier namespaces do they use?
|
|
19
|
-
_variant_regex = 'RRID:([A-Z]+):([A-Za-z0-9_:-]+)' # legacy
|
|
20
|
+
_variant_regex = '^RRID:([A-Z]+):([A-Za-z0-9_:-]+)$' # legacy
|
|
20
21
|
_local_conventions = conv.Identity
|
|
21
22
|
|
|
22
23
|
def __init__(self, rrid):
|
|
@@ -101,10 +102,31 @@ class Rrid(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
101
102
|
elif self._COOLDOWN and self._resp_metadata.status_code == 404:
|
|
102
103
|
msg = f'RRID failure: {self._resp_metadata.status_code} {self.asUri()}'
|
|
103
104
|
return {COOLDOWN: msg,}
|
|
105
|
+
elif self._resp_metadata.status_code == 429:
|
|
106
|
+
headers = self._resp_metadata.headers
|
|
107
|
+
if 'Retry-After' in headers:
|
|
108
|
+
try:
|
|
109
|
+
wait = int(headers['Retry-After'])
|
|
110
|
+
except ValueError as ve:
|
|
111
|
+
try:
|
|
112
|
+
self._resp_metadata.raise_for_status()
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise exc.ResolutionError(identifier) from e
|
|
115
|
+
|
|
116
|
+
if wait > 20:
|
|
117
|
+
try:
|
|
118
|
+
self._resp_metadata.raise_for_status()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise exc.ResolutionError(identifier) from e
|
|
121
|
+
|
|
122
|
+
log.debug(f'waiting {wait}s for {identifier}')
|
|
123
|
+
sleep(wait)
|
|
124
|
+
return self._metadata(identifier)
|
|
125
|
+
|
|
104
126
|
else:
|
|
105
127
|
try:
|
|
106
128
|
self._resp_metadata.raise_for_status()
|
|
107
|
-
except
|
|
129
|
+
except Exception as e:
|
|
108
130
|
raise exc.ResolutionError(identifier) from e
|
|
109
131
|
|
|
110
132
|
@cache_result
|
|
@@ -175,8 +197,52 @@ class Rrid(formats.Rdf, idlib.HelperNoData, idlib.Stream):
|
|
|
175
197
|
if descs:
|
|
176
198
|
return sorted(descs, key=len, reverse=True)[0]
|
|
177
199
|
|
|
200
|
+
def proper_citation(self):
|
|
201
|
+
ms = self.metadata()
|
|
202
|
+
if ms:
|
|
203
|
+
cites = set(m['rrid']['properCitation'] for m in ms if 'rrid' in m and 'properCitation' in m['rrid'])
|
|
204
|
+
if cites:
|
|
205
|
+
return sorted(cites, key=len, reverse=True)[0]
|
|
206
|
+
|
|
207
|
+
# output streams
|
|
208
|
+
|
|
209
|
+
def _triples_gen(self,
|
|
210
|
+
rdflib=None,
|
|
211
|
+
rdf=None,
|
|
212
|
+
rdfs=None,
|
|
213
|
+
owl=None,
|
|
214
|
+
NIFRID=None,
|
|
215
|
+
TEMP=None,
|
|
216
|
+
**kwargs):
|
|
217
|
+
""" implementation of method to produce a
|
|
218
|
+
triplified version of the record """
|
|
219
|
+
s = self.asUri(rdflib.URIRef)
|
|
220
|
+
yield s, rdf.type, owl.NamedIndividual
|
|
221
|
+
try:
|
|
222
|
+
pc = self.proper_citation()
|
|
223
|
+
if pc:
|
|
224
|
+
yield s, TEMP.properCitation, rdflib.Literal(pc)
|
|
225
|
+
except exc.ResolutionError as e:
|
|
226
|
+
log.exception(e)
|
|
227
|
+
yield s, TEMP.resolutionError, rdflib.Literal(True)
|
|
228
|
+
return
|
|
229
|
+
except exc.RemoteError as e:
|
|
230
|
+
log.exception(e)
|
|
231
|
+
yield s, TEMP.remoteError, rdflib.Literal(True)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
yield s, rdfs.label, rdflib.Literal(self.label)
|
|
235
|
+
|
|
178
236
|
# alternate representations
|
|
179
237
|
|
|
238
|
+
def asDict(self):
|
|
239
|
+
out = super().asDict()
|
|
240
|
+
pc = self.proper_citation()
|
|
241
|
+
if pc:
|
|
242
|
+
out['proper_citation'] = pc
|
|
243
|
+
|
|
244
|
+
return out
|
|
245
|
+
|
|
180
246
|
def asUri(self, asType=None):
|
|
181
247
|
# TODO n2t, identifiers.org
|
|
182
248
|
# TODO TODO having an explicit model for resolver/metadata services
|
|
@@ -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.dev24
|
|
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
|
|
@@ -8,7 +8,6 @@ Author-email: tgbugs@gmail.com
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: python persistent identifiers
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.7
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -16,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
21
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -30,14 +30,27 @@ Provides-Extra: test
|
|
|
30
30
|
Requires-Dist: pytest; extra == "test"
|
|
31
31
|
Requires-Dist: joblib>=1.1.0; extra == "test"
|
|
32
32
|
Requires-Dist: beautifulsoup4[html5lib]; extra == "test"
|
|
33
|
-
Requires-Dist: pyontutils>=0.1.
|
|
33
|
+
Requires-Dist: pyontutils>=0.1.40; extra == "test"
|
|
34
34
|
Requires-Dist: google-auth-oauthlib; extra == "test"
|
|
35
35
|
Provides-Extra: org
|
|
36
36
|
Requires-Dist: beautifulsoup4[html5lib]; extra == "org"
|
|
37
37
|
Provides-Extra: rdf
|
|
38
|
-
Requires-Dist: pyontutils>=0.1.
|
|
38
|
+
Requires-Dist: pyontutils>=0.1.40; extra == "rdf"
|
|
39
39
|
Provides-Extra: oauth
|
|
40
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
|
|
41
54
|
|
|
42
55
|
# idlib
|
|
43
56
|
[](https://pypi.org/project/idlib/)
|
|
@@ -19,7 +19,7 @@ with open('README.md', 'rt') as f:
|
|
|
19
19
|
long_description = f.read()
|
|
20
20
|
|
|
21
21
|
org_require = ['beautifulsoup4[html5lib]']
|
|
22
|
-
rdf_require = ['pyontutils>=0.1.
|
|
22
|
+
rdf_require = ['pyontutils>=0.1.40']
|
|
23
23
|
oauth_require = ['google-auth-oauthlib']
|
|
24
24
|
tests_require = (['pytest', 'joblib>=1.1.0'] +
|
|
25
25
|
org_require +
|
|
@@ -36,7 +36,6 @@ setup(name='idlib',
|
|
|
36
36
|
license='MIT',
|
|
37
37
|
classifiers=[
|
|
38
38
|
'Development Status :: 4 - Beta',
|
|
39
|
-
'License :: OSI Approved :: MIT License',
|
|
40
39
|
'Programming Language :: Python :: 3.7',
|
|
41
40
|
'Programming Language :: Python :: 3.8',
|
|
42
41
|
'Programming Language :: Python :: 3.9',
|
|
@@ -44,6 +43,7 @@ setup(name='idlib',
|
|
|
44
43
|
'Programming Language :: Python :: 3.11',
|
|
45
44
|
'Programming Language :: Python :: 3.12',
|
|
46
45
|
'Programming Language :: Python :: 3.13',
|
|
46
|
+
'Programming Language :: Python :: 3.14',
|
|
47
47
|
'Programming Language :: Python :: Implementation :: CPython',
|
|
48
48
|
'Programming Language :: Python :: Implementation :: PyPy',
|
|
49
49
|
'Operating System :: POSIX :: Linux',
|
|
@@ -36,6 +36,7 @@ class TestDoi(HelpTestStreams, unittest.TestCase):
|
|
|
36
36
|
ids = [
|
|
37
37
|
'https://doi.org/10.13003/5jchdy',
|
|
38
38
|
'https://doi.org/10.1101/2020.10.19.343129', # crossref 500 error
|
|
39
|
+
'https://doi.org/10.1002/(SICI)1096-9136(199608)13:8%3C700::AID-DIA174%3E3.0.CO;2-8', # oof
|
|
39
40
|
]
|
|
40
41
|
ids_bad = ['lol not an identifier']
|
|
41
42
|
def test_version_f1000(self):
|
|
@@ -142,7 +143,10 @@ class TestRrid(HelpTestStreams, unittest.TestCase):
|
|
|
142
143
|
'RRID:Addgene_19640',
|
|
143
144
|
#'RRID:NCBITaxon_9606', # TODO not in resolver yet
|
|
144
145
|
]
|
|
145
|
-
ids_bad = [
|
|
146
|
+
ids_bad = [
|
|
147
|
+
'lol not an identifier',
|
|
148
|
+
'RRID:RGD_737929 Charles River RS:0001011',
|
|
149
|
+
]
|
|
146
150
|
|
|
147
151
|
|
|
148
152
|
@skipif_ci
|
|
@@ -183,6 +187,7 @@ class TestPio(HelpTestStreams, unittest.TestCase):
|
|
|
183
187
|
'https://www.protocols.io/view/21417',
|
|
184
188
|
'https://www.protocols.io/api/v3/protocols/21417',
|
|
185
189
|
'https://www.protocols.io/api/v4/protocols/21417',
|
|
190
|
+
'https://www.protocols.io/api/v4/protocols/126454',
|
|
186
191
|
|
|
187
192
|
'pio.view:human-islet-microvasculature-immunofluorescence-in-y3tfynn/materials',
|
|
188
193
|
|
|
@@ -192,6 +197,11 @@ class TestPio(HelpTestStreams, unittest.TestCase):
|
|
|
192
197
|
'https://www.protocols.io/view/18980', # deleted
|
|
193
198
|
'https://www.protocols.io/api/v3/protocols/18980', # deleted
|
|
194
199
|
'https://www.protocols.io/private/D327A68038D5B4ECB32964C22D904270', # deleted
|
|
200
|
+
|
|
201
|
+
# malformed slug causing issues with asDict ... FIXME also possibly changes in Pio api
|
|
202
|
+
# because now the error says NotAuthorized ... which seems suspicious
|
|
203
|
+
'https://www.protocols.io/view/transfection-protocol-for-n2a-and-hek293-using-pro-75mhq50',
|
|
204
|
+
'https://www.protocols.io/api/v4/protocols/transfection-protocol-for-n2a-and-hek293-using-pro-75mhq50',
|
|
195
205
|
]
|
|
196
206
|
|
|
197
207
|
@skipif_no_net
|
idlib-0.0.1.dev22/idlib/core.py
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
from . import exceptions as exc
|
|
3
|
-
from .utils import log
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def resolution_chain(iri):
|
|
7
|
-
for head in resolution_chain_responses(iri):
|
|
8
|
-
yield head.url
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def try_get(s, head):
|
|
12
|
-
# see whether the server has a bad/broken support for head requests
|
|
13
|
-
# sometimes they return e.g. 400 instead of 405, and really they should
|
|
14
|
-
# just work because they work correctly with get request ...
|
|
15
|
-
head = s.get(head.url, stream=True)
|
|
16
|
-
if head.ok:
|
|
17
|
-
log.info(f'bad HEAD implementation {head.url}')
|
|
18
|
-
else:
|
|
19
|
-
content = head.content
|
|
20
|
-
if content:
|
|
21
|
-
if head.headers['Content-Type'] == 'application/json':
|
|
22
|
-
try:
|
|
23
|
-
j = head.json()
|
|
24
|
-
log.error(f'{head.url} error json {j}')
|
|
25
|
-
except Exception as e:
|
|
26
|
-
log.error(f'{head.url} error text {head.text}')
|
|
27
|
-
|
|
28
|
-
head.close()
|
|
29
|
-
return head
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def resolution_chain_responses(iri, raise_on_final=True):
|
|
33
|
-
#doi = doi # TODO
|
|
34
|
-
s = requests.Session()
|
|
35
|
-
head = s.head(iri, allow_redirects=False)
|
|
36
|
-
if head.status_code < 400:
|
|
37
|
-
yield head
|
|
38
|
-
else:
|
|
39
|
-
head = try_get(s, head)
|
|
40
|
-
yield head
|
|
41
|
-
|
|
42
|
-
while head.is_redirect and head.status_code < 400: # FIXME redirect loop issue
|
|
43
|
-
yield head.next
|
|
44
|
-
try:
|
|
45
|
-
head = s.send(head.next)
|
|
46
|
-
except requests.exceptions.SSLError as e:
|
|
47
|
-
raise exc.InbetweenError(msg) from e
|
|
48
|
-
|
|
49
|
-
if head.status_code < 400:
|
|
50
|
-
yield head
|
|
51
|
-
else:
|
|
52
|
-
head = try_get(s, head)
|
|
53
|
-
yield head
|
|
54
|
-
|
|
55
|
-
if not head.is_redirect:
|
|
56
|
-
break
|
|
57
|
-
|
|
58
|
-
if raise_on_final: # we still want the chain ... null pointer error comes later?
|
|
59
|
-
if head.status_code == 404:
|
|
60
|
-
head.raise_for_status() # probably a permissions issue
|
|
61
|
-
elif head.status_code >= 500:
|
|
62
|
-
msg = f'Remote in error due to {head.status_code} at {head.url}\n'
|
|
63
|
-
raise exc.RemoteError(msg)
|
|
64
|
-
elif head.status_code >= 400:
|
|
65
|
-
msg = f'Nothing found due to {head.status_code} at {head.url}\n'
|
|
66
|
-
raise exc.ResolutionError(msg)
|
|
67
|
-
|
|
68
|
-
if head.status_code >= 400:
|
|
69
|
-
# XXX this seems like the "right" thing to do, but it will
|
|
70
|
-
# probably break a bunch of stuff, but at least this way it
|
|
71
|
-
# will be visible now, obviously yeilding None as the final
|
|
72
|
-
# element in the reference chain conflates all the possible
|
|
73
|
-
# reasons so raising is a better solution and is the default
|
|
74
|
-
# for a reason but at least this way the user knows that the
|
|
75
|
-
# final element of the chain dereference to nothing instead of
|
|
76
|
-
# assuming that the last element succeeded
|
|
77
|
-
yield None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|