idlib 0.0.1.dev22__py2.py3-none-any.whl → 0.0.1.dev24__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 CHANGED
@@ -55,4 +55,4 @@ def get_right_id(uri):
55
55
  return pi
56
56
 
57
57
 
58
- __version__ = '0.0.1.dev22'
58
+ __version__ = '0.0.1.dev24'
@@ -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=auth.get_path('protocols-io-api-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/cache.py CHANGED
@@ -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
- folder.mkdir(parents=True)
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
- with open(filepath, write_mode) as f:
104
- serialize(output, f)
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
idlib/core.py CHANGED
@@ -3,19 +3,45 @@ from . import exceptions as exc
3
3
  from .utils import log
4
4
 
5
5
 
6
- def resolution_chain(iri):
7
- 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):
8
8
  yield head.url
9
9
 
10
10
 
11
- def try_get(s, head):
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):
12
15
  # see whether the server has a bad/broken support for head requests
13
16
  # sometimes they return e.g. 400 instead of 405, and really they should
14
17
  # just work because they work correctly with get request ...
15
18
  head = s.get(head.url, stream=True)
19
+
16
20
  if head.ok:
17
21
  log.info(f'bad HEAD implementation {head.url}')
18
- else:
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:
19
45
  content = head.content
20
46
  if content:
21
47
  if head.headers['Content-Type'] == 'application/json':
@@ -29,14 +55,34 @@ def try_get(s, head):
29
55
  return head
30
56
 
31
57
 
32
- def resolution_chain_responses(iri, raise_on_final=True):
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):
33
79
  #doi = doi # TODO
34
80
  s = requests.Session()
35
81
  head = s.head(iri, allow_redirects=False)
36
82
  if head.status_code < 400:
37
83
  yield head
38
84
  else:
39
- head = try_get(s, head)
85
+ head = try_get(s, head, headers_fun=headers_fun)
40
86
  yield head
41
87
 
42
88
  while head.is_redirect and head.status_code < 400: # FIXME redirect loop issue
@@ -49,21 +95,14 @@ def resolution_chain_responses(iri, raise_on_final=True):
49
95
  if head.status_code < 400:
50
96
  yield head
51
97
  else:
52
- head = try_get(s, head)
98
+ head = try_get(s, head, headers_fun=headers_fun)
53
99
  yield head
54
100
 
55
101
  if not head.is_redirect:
56
102
  break
57
103
 
58
104
  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)
105
+ resolution_raise_logic(head)
67
106
 
68
107
  if head.status_code >= 400:
69
108
  # XXX this seems like the "right" thing to do, but it will
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
 
@@ -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
- raise exc.TransportError() from e1
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)
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):
@@ -437,7 +440,12 @@ class HelpTestStreams:
437
440
  HelpTestStreams._copy = copy
438
441
  import pickle
439
442
  HelpTestStreams._pickle = pickle
440
- from joblib import Parallel, delayed
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
- bads.append(d.identifier.__dict__)
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
- for i in self.ids_bad:
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(self.identifier_actionable,
612
- raise_on_final=False))
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):
idlib/systems/doi.py CHANGED
@@ -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
- 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
+ # 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' in doi or 'doi.org' in doi:
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
- return self.suffix is not None and self.suffix.startswith('10.')
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
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['name']
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
- return self.metadata()['name']
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
- yield from [rdflib.Literal(s) for s in d['aliases']]
218
- yield from [rdflib.Literal(s) for s in d['acronyms']]
219
- yield from [rdflib.Literal(l['label'], lang=l['iso639']) for l in d['labels']]
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
- for a in m['aliases'] + m['acronyms']:
226
- out.append(a)
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
- for l in m['labels']:
229
- out.append(l['label'])
253
+ for l in m['labels']:
254
+ out.append(l['label'])
230
255
 
231
256
  return out
232
257
 
idlib/systems/rrid.py CHANGED
@@ -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 BaseException as e:
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
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
- number = number * _base + _index[c]
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
1
+ Metadata-Version: 2.4
2
2
  Name: idlib
3
- Version: 0.0.1.dev22
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.28; extra == "test"
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.28; extra == "rdf"
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
  [![PyPI version](https://badge.fury.io/py/idlib.svg)](https://pypi.org/project/idlib/)
@@ -1,16 +1,16 @@
1
- idlib/__init__.py,sha256=FdowWsUq9bwbPqZPU2sTU1iWMxm1KetrH4kknNBlMmQ,1696
1
+ idlib/__init__.py,sha256=nM5sqe3xTkzp2ETq97yYQUeeoJ3sHfNb_CtVrzJ7moA,1696
2
2
  idlib/auth-config.py,sha256=tYM2aYCYJFG5NB4Af1dSn6YxhafttOp7W7aSc-QbVV8,460
3
- idlib/cache.py,sha256=lR1I3iEYvBhNeqoptS-Sj7SpIHt1K65nYlBrTK-ROVQ,3875
3
+ idlib/cache.py,sha256=UN3ufibDtPHgdMqKEXqVvVhfUTod08Vs_xsG12D5kns,4396
4
4
  idlib/config.py,sha256=Gli2tZlbKxBos8sXDoDK1g9mngfDw1ZWpBt_7MDZH-g,76
5
- idlib/core.py,sha256=DfJoPe_xqJJducri1ub7e_anJYuhJ37-ZxvZCuuOpaY,2704
5
+ idlib/core.py,sha256=KVhWN6yQfIGA5rG_ydNVC25oMa88lYJwQtAjzzhqziQ,4569
6
6
  idlib/edit.py,sha256=s3yRtvNelQUA8fziu_igZeoFsW4xCK_kgfNVv3TS0iE,3635
7
7
  idlib/exceptions.py,sha256=QC3Jq98iwezcqB7SmDr-UEOqdNCnvEXVkKB0eJLIwZs,4714
8
- idlib/from_oq.py,sha256=HlgepGbgSUbOt5R79ALRhgiLOEvU6TSu3evB8zq7N84,94170
8
+ idlib/from_oq.py,sha256=AVxs7zq8GlAmHOJAu8M3Wb3R5byTc5D4X7U47PzRldM,96351
9
9
  idlib/identifiers.py,sha256=s39bAS-fCQrlwAbMtaa7tjSnqKq4YPlfSbfJ6ju2o7I,3861
10
- idlib/streams.py,sha256=HGcs_S7bOiUYfiSjZNGLXo5bJI7yd0bYjc8BKkdqECI,26439
11
- idlib/utils.py,sha256=zItGz-lXovZytSENUZ9q7A7gLr5uK_9VMawokGSskbk,5127
10
+ idlib/streams.py,sha256=spUVSCyR0gugdZxi8orhTAkUKCAT-kq7J5dyBxl5_uo,27477
11
+ idlib/utils.py,sha256=q2hcz0Qtbya1UpPnlGisougg9EuDZkcAKXJQIN4OFQs,5306
12
12
  idlib/apis/__init__.py,sha256=q2-iSMUDPlYbjqGWa1mtbe2G95N7wbjWHGj30BUdVDM,88
13
- idlib/apis/protocols_io.py,sha256=3PGKBwe9P_DxhScIG-L3ZFtnEHxgHaccLhRGVPZuYHg,3956
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
@@ -19,17 +19,17 @@ 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=PQAstSUocPpK9Ug7L-eKvJrQhn3jRU9WuKITGyZoL9g,13100
22
+ idlib/systems/doi.py,sha256=5Ut8u6m4X_RWJv6yGOTM1MktrJlWnORPfYdmeMT9m_Y,13858
23
23
  idlib/systems/handle.py,sha256=Gg97IuysdAdCX9j6EhuNfIddb1GDq0CmwW1SmuLkpgM,95
24
24
  idlib/systems/isni.py,sha256=TgiSTlDcSBLRro3gg_YfQJQkfaJ3saw0Au1OMhE23wU,89
25
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=xIS30mxKd0IcjwalBjnyUU7GKeeU5pxM3i9hSofG6u8,8129
28
- idlib/systems/rrid.py,sha256=XVHyHyiGC2OvbOSXmfU3_wswS6yUw65qZ3Pct0sjbfU,6886
27
+ idlib/systems/ror.py,sha256=3pZ1aLg7NUh-mHQm-qu-hqy5WrJZprtj5lWIWRpP2-M,8894
28
+ idlib/systems/rrid.py,sha256=BtQzrS90sAGKvnoH5CQK0URxTEw1A0hwNaEngxuGLFs,9136
29
29
  idlib/systems/urn.py,sha256=p222KjHgfF6y9srCm7kFWuWA9NK4lI-wEQUtSeuBqYk,113
30
30
  idlib/systems/web.py,sha256=vrSbQYWnxhe9pSgZcOqwLM3V6XQDLwv1QqWlhWWxbL0,1291
31
- idlib-0.0.1.dev22.dist-info/LICENSE,sha256=nEuJBsoHfpXGgf6ZyFGUhQJqxFWwhSYF-45pRAqTSb4,1080
32
- idlib-0.0.1.dev22.dist-info/METADATA,sha256=kjQ8xltmifA_qoV2GmEQAFeO5P3HzWVMc0MjiINC38o,2052
33
- idlib-0.0.1.dev22.dist-info/WHEEL,sha256=pxeNX5JdtCe58PUSYP9upmc7jdRPgvT0Gm9kb1SHlVw,109
34
- idlib-0.0.1.dev22.dist-info/top_level.txt,sha256=xIk87gJPbeCKih9BVMP8PlEFdpcsGplenz0VQhx5sP8,6
35
- idlib-0.0.1.dev22.dist-info/RECORD,,
31
+ idlib-0.0.1.dev24.dist-info/licenses/LICENSE,sha256=nEuJBsoHfpXGgf6ZyFGUhQJqxFWwhSYF-45pRAqTSb4,1080
32
+ idlib-0.0.1.dev24.dist-info/METADATA,sha256=CW7UYjKLSBcisjzs4NZ-SkT-yIXKIxryIhuOO3D-mn8,2330
33
+ idlib-0.0.1.dev24.dist-info/WHEEL,sha256=4YBfCYNH4wlLpv3pzq1hbEuIlXA4WJabKLFurZ7eTL0,109
34
+ idlib-0.0.1.dev24.dist-info/top_level.txt,sha256=xIk87gJPbeCKih9BVMP8PlEFdpcsGplenz0VQhx5sP8,6
35
+ idlib-0.0.1.dev24.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any