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 CHANGED
@@ -55,4 +55,4 @@ def get_right_id(uri):
55
55
  return pi
56
56
 
57
57
 
58
- __version__ = '0.0.1.dev21'
58
+ __version__ = '0.0.1.dev23'
@@ -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/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
- m = re.search(element, chunk)
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
- def resolution_chain_responses(iri, raise_on_final=True):
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 = requests.head(iri)
14
- yield head
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
- yield head
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
- if head.status_code == 404:
28
- head.raise_for_status() # probably a permissions issue
29
- elif head.status_code >= 500:
30
- raise exc.RemoteError()
31
- elif head.status_code >= 400:
32
- msg = f'Nothing found due to {head.status_code} at {head.url}\n'
33
- raise exc.ResolutionError(msg)
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 an referent unknown error. """
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
- raise exc.TransportError() from e1
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(self.identifier_actionable,
612
- raise_on_final=False))
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
- return uri.progenitor().headers
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\.[0-9]{4,9}[-._;()/:a-zA-Z0-9]+$'
30
- canonical_regex = '^https:\/\/doi.org/10\.[0-9]{4,9}[-._;()/:a-zA-Z0-9]+$'
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
- total = 0
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['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']
@@ -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
- yield from [rdflib.Literal(s) for s in d['aliases']]
217
- yield from [rdflib.Literal(s) for s in d['acronyms']]
218
- 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']]
219
240
 
220
241
  @property
221
242
  def synonyms(self):
222
243
  out = []
223
244
  m = self.metadata()
224
- for a in m['aliases'] + m['acronyms']:
225
- 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)
226
252
 
227
- for l in m['labels']:
228
- out.append(l['label'])
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
- 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.dev21
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.6
24
+ Requires-Python: >=3.7
24
25
  Description-Content-Type: text/markdown
25
26
  License-File: LICENSE
26
- Requires-Dist: orthauth[sxpr,yaml] >=0.0.13
27
+ Requires-Dist: orthauth[sxpr,yaml]>=0.0.18
27
28
  Requires-Dist: requests
28
- Provides-Extra: oauth
29
- Requires-Dist: google-auth-oauthlib ; extra == 'oauth'
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] ; extra == 'org'
36
+ Requires-Dist: beautifulsoup4[html5lib]; extra == "org"
32
37
  Provides-Extra: rdf
33
- Requires-Dist: pyontutils >=0.1.28 ; extra == 'rdf'
34
- Provides-Extra: test
35
- Requires-Dist: pytest ; extra == 'test'
36
- Requires-Dist: joblib >=1.1.0 ; extra == 'test'
37
- Requires-Dist: beautifulsoup4[html5lib] ; extra == 'test'
38
- Requires-Dist: pyontutils >=0.1.28 ; extra == 'test'
39
- Requires-Dist: google-auth-oauthlib ; extra == 'test'
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
  [![PyPI version](https://badge.fury.io/py/idlib.svg)](https://pypi.org/project/idlib/)
@@ -1,35 +1,35 @@
1
- idlib/__init__.py,sha256=gU4iaEA5x2uf_1tRUfaQ7d5y4KM-csnRBJyKa7if1UY,1696
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=zLqHrSwtgbQll-04J2B5j8W69AXpWT_Ql5n0eBHp2pk,1055
5
+ idlib/core.py,sha256=KVhWN6yQfIGA5rG_ydNVC25oMa88lYJwQtAjzzhqziQ,4569
6
6
  idlib/edit.py,sha256=s3yRtvNelQUA8fziu_igZeoFsW4xCK_kgfNVv3TS0iE,3635
7
- idlib/exceptions.py,sha256=4qUTUrGkCMu2jE0TO4_hUCP-7XDCrk6FE08Ff-c1N94,4242
8
- idlib/from_oq.py,sha256=HlgepGbgSUbOt5R79ALRhgiLOEvU6TSu3evB8zq7N84,94170
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=3L5JnSDzbodHET66Mua4zlfJEZbsXACIsWtVd-iDUZM,26381
11
- idlib/utils.py,sha256=zItGz-lXovZytSENUZ9q7A7gLr5uK_9VMawokGSskbk,5127
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=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
17
- idlib/conventions/type.py,sha256=2ATKqoWNaQrc4pirJ2-WucBTsJud7RC4g59Z62DSK7o,898
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=GVL_uiLmU-Xa9odi7pGkbIniU7tTDesKO1pNbsYm2Fs,13100
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=hharUZ5iixVDznHNpEh8BJadQV2dEpNH8h9TdKi0qZE,6634
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=q_NyvVP-Xe9DrzQMFZAC2WeQU6QyxSa6RzrqQ0P7gUA,8067
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=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.dev21.dist-info/LICENSE,sha256=nEuJBsoHfpXGgf6ZyFGUhQJqxFWwhSYF-45pRAqTSb4,1080
32
- idlib-0.0.1.dev21.dist-info/METADATA,sha256=qWQNOrn8om22xx66S43Ao8rdjLjm_rUVKun5tG13yEk,2012
33
- idlib-0.0.1.dev21.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
34
- idlib-0.0.1.dev21.dist-info/top_level.txt,sha256=xIk87gJPbeCKih9BVMP8PlEFdpcsGplenz0VQhx5sP8,6
35
- idlib-0.0.1.dev21.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any