idlib 0.0.1.dev23__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.dev23'
58
+ __version__ = '0.0.1.dev24'
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/from_oq.py CHANGED
@@ -432,6 +432,10 @@ class Pio(formats.Rdf, idlib.Stream):
432
432
  if data:
433
433
  doi = data['doi']
434
434
  if doi:
435
+ if not doi.startswith('http'):
436
+ # why is there no scheme? who knows!?
437
+ doi = 'https://' + doi
438
+
435
439
  return idlib.Doi(doi)
436
440
 
437
441
  @property
@@ -758,6 +762,11 @@ class Pio(formats.Rdf, idlib.Stream):
758
762
  # and pio json mean there is an extra layer that needs
759
763
  # interpretation ... i.e. we definitely got to the remote
760
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)
761
770
  else:
762
771
  msg = f'unhandled pio status code {sc}\n' + message
763
772
  raise NotImplementedError(msg)
@@ -816,8 +825,10 @@ class Pio(formats.Rdf, idlib.Stream):
816
825
  # for how we got there, even if we toss the crazy slugs
817
826
  self._progenitors = {} # FIXME yes this blasts progens every time
818
827
  uh = self.uri_human
828
+ _sigh = uh._progenitors.pop('id-converted-from') # avoid infinite recursion
819
829
  uai = uh.uri_api_int
820
830
  d4 = uai.data4(fail_ok=fail_ok)
831
+ uh._progenitors['id-converted-from'] = _sigh
821
832
  self._progenitors['id-converted-to'] = uai
822
833
  if 'path' in uai._progenitors:
823
834
  self._progenitors['path'] = uai._progenitors['path']
@@ -833,6 +844,8 @@ class Pio(formats.Rdf, idlib.Stream):
833
844
  except Exception as e0:
834
845
  try:
835
846
  return self.data1(fail_ok=fail_ok)
847
+ except exc.IdDoesNotExistError as ene1:
848
+ raise ene1
836
849
  except Exception as e1:
837
850
  # FIXME error type ...
838
851
  raise exc.InbetweenError() from e1
@@ -840,6 +853,9 @@ class Pio(formats.Rdf, idlib.Stream):
840
853
  return self.data4(fail_ok=fail_ok)
841
854
  except (AttributeError, NotImplementedError) as e:
842
855
  raise e
856
+ except exc.IdDoesNotExistError as e:
857
+ # don't wait for pioids that don't exist
858
+ raise e
843
859
  except Exception as e:
844
860
  wait_until = time() + self._wait_time
845
861
  self._data_in_error_sigh = wait_until, e
idlib/streams.py CHANGED
@@ -440,7 +440,12 @@ class HelpTestStreams:
440
440
  HelpTestStreams._copy = copy
441
441
  import pickle
442
442
  HelpTestStreams._pickle = pickle
443
- 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
+
444
449
  HelpTestStreams._Parallel = Parallel
445
450
  HelpTestStreams._delayed = delayed
446
451
 
@@ -464,7 +469,11 @@ class HelpTestStreams:
464
469
  print(d.identifier.prefix)
465
470
  print(d.identifier.suffix)
466
471
  if d.identifier.prefix is None or d.identifier.suffix is None:
467
- 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__)
468
477
 
469
478
  try:
470
479
  d.identifier_bound_metadata
@@ -501,9 +510,18 @@ class HelpTestStreams:
501
510
 
502
511
  def test_malformed(self):
503
512
  bads = []
504
- 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:
505
521
  try:
506
522
  d = self.stream(i)
523
+ d.identifier.validate()
524
+ vbads.append(d)
507
525
  d.data()
508
526
  bads.append(d)
509
527
  except Exception as e:
@@ -511,6 +529,7 @@ class HelpTestStreams:
511
529
  pass
512
530
 
513
531
  assert not bads, bads
532
+ assert not vbads, vbads
514
533
 
515
534
  def test_hash_eq_id(self):
516
535
  hrm = self.ids[0]
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/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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idlib
3
- Version: 0.0.1.dev23
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,12 +30,12 @@ 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
41
  Dynamic: author
@@ -1,13 +1,13 @@
1
- idlib/__init__.py,sha256=PMFL7ZemlNHqabBZ_MyLbYcaxaGBU1ET-nN9BlflWHE,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
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=kt4BpbAZgLo1CwxwyR_swnlVMsekV7ePfZivq9V8yns,95599
8
+ idlib/from_oq.py,sha256=AVxs7zq8GlAmHOJAu8M3Wb3R5byTc5D4X7U47PzRldM,96351
9
9
  idlib/identifiers.py,sha256=s39bAS-fCQrlwAbMtaa7tjSnqKq4YPlfSbfJ6ju2o7I,3861
10
- idlib/streams.py,sha256=QhOtT3xnp8FGVt7GEYAFAnd7PKYRKBPbOF6XGit63lQ,26765
10
+ idlib/streams.py,sha256=spUVSCyR0gugdZxi8orhTAkUKCAT-kq7J5dyBxl5_uo,27477
11
11
  idlib/utils.py,sha256=q2hcz0Qtbya1UpPnlGisougg9EuDZkcAKXJQIN4OFQs,5306
12
12
  idlib/apis/__init__.py,sha256=q2-iSMUDPlYbjqGWa1mtbe2G95N7wbjWHGj30BUdVDM,88
13
13
  idlib/apis/protocols_io.py,sha256=PKcoSbO9AqTfBYKLWJQyO3vHjug9qEL826s7jY4LWWg,4026
@@ -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
27
  idlib/systems/ror.py,sha256=3pZ1aLg7NUh-mHQm-qu-hqy5WrJZprtj5lWIWRpP2-M,8894
28
- idlib/systems/rrid.py,sha256=RcDn3-BRmrKzzxDCypSWRQuivKvWqUqqohY8BeSqRF4,8278
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.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,,
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 (80.9.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