idlib 0.0.1.dev22__tar.gz → 0.0.1.dev23__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.
Files changed (49) hide show
  1. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/PKG-INFO +15 -2
  2. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/__init__.py +1 -1
  3. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/apis/protocols_io.py +9 -5
  4. idlib-0.0.1.dev23/idlib/core.py +116 -0
  5. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/from_oq.py +35 -3
  6. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/streams.py +12 -3
  7. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/ror.py +34 -9
  8. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/rrid.py +44 -0
  9. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/utils.py +6 -1
  10. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib.egg-info/PKG-INFO +15 -2
  11. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/test/test_identifiers.py +6 -0
  12. idlib-0.0.1.dev22/idlib/core.py +0 -77
  13. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/LICENSE +0 -0
  14. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/Pipfile +0 -0
  15. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/README.md +0 -0
  16. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/docs/identifiers.org +0 -0
  17. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/docs/stream-grammer.org +0 -0
  18. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/apis/__init__.py +0 -0
  19. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/auth-config.py +0 -0
  20. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/cache.py +0 -0
  21. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/config.py +0 -0
  22. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/conventions/__init__.py +0 -0
  23. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/conventions/core.py +0 -0
  24. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/conventions/local.py +0 -0
  25. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/conventions/type.py +0 -0
  26. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/edit.py +0 -0
  27. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/exceptions.py +0 -0
  28. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/formats/__init__.py +0 -0
  29. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/formats/rdf.py +0 -0
  30. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/identifiers.py +0 -0
  31. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/__init__.py +0 -0
  32. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/ark.py +0 -0
  33. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/doi.py +0 -0
  34. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/handle.py +0 -0
  35. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/isni.py +0 -0
  36. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/orcid.py +0 -0
  37. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/pmid.py +0 -0
  38. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/urn.py +0 -0
  39. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib/systems/web.py +0 -0
  40. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib.egg-info/SOURCES.txt +0 -0
  41. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib.egg-info/dependency_links.txt +0 -0
  42. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib.egg-info/requires.txt +0 -0
  43. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/idlib.egg-info/top_level.txt +0 -0
  44. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/setup.cfg +0 -0
  45. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/setup.py +0 -0
  46. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/test/__init__.py +0 -0
  47. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/test/test_cache.py +0 -0
  48. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/test/test_simple.py +0 -0
  49. {idlib-0.0.1.dev22 → idlib-0.0.1.dev23}/test/test_utils.py +0 -0
@@ -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.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
@@ -38,6 +38,19 @@ Provides-Extra: rdf
38
38
  Requires-Dist: pyontutils>=0.1.28; 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/)
@@ -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.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
@@ -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
 
@@ -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)
@@ -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):
@@ -608,8 +611,10 @@ class StreamUri(Stream):
608
611
  dc[self.identifier_actionable] = tuple(
609
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):
@@ -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
 
@@ -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
@@ -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.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
@@ -38,6 +38,19 @@ Provides-Extra: rdf
38
38
  Requires-Dist: pyontutils>=0.1.28; 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/)
@@ -183,6 +183,7 @@ class TestPio(HelpTestStreams, unittest.TestCase):
183
183
  'https://www.protocols.io/view/21417',
184
184
  'https://www.protocols.io/api/v3/protocols/21417',
185
185
  'https://www.protocols.io/api/v4/protocols/21417',
186
+ 'https://www.protocols.io/api/v4/protocols/126454',
186
187
 
187
188
  'pio.view:human-islet-microvasculature-immunofluorescence-in-y3tfynn/materials',
188
189
 
@@ -192,6 +193,11 @@ class TestPio(HelpTestStreams, unittest.TestCase):
192
193
  'https://www.protocols.io/view/18980', # deleted
193
194
  'https://www.protocols.io/api/v3/protocols/18980', # deleted
194
195
  'https://www.protocols.io/private/D327A68038D5B4ECB32964C22D904270', # deleted
196
+
197
+ # malformed slug causing issues with asDict ... FIXME also possibly changes in Pio api
198
+ # because now the error says NotAuthorized ... which seems suspicious
199
+ 'https://www.protocols.io/view/transfection-protocol-for-n2a-and-hek293-using-pro-75mhq50',
200
+ 'https://www.protocols.io/api/v4/protocols/transfection-protocol-for-n2a-and-hek293-using-pro-75mhq50',
195
201
  ]
196
202
 
197
203
  @skipif_no_net
@@ -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