deriva 1.7.1__py3-none-any.whl → 1.7.3__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.
@@ -33,7 +33,7 @@ class AttrSpecList(BaseSpecList):
33
33
  return None
34
34
  new = []
35
35
  for item in orig_list:
36
- new.append(unicode(item))
36
+ new.append(item)
37
37
  return new
38
38
 
39
39
  def add_list(self, dictlist):
@@ -85,7 +85,7 @@ class AttrConfig:
85
85
  self.toplevel_config = ConfigUtil.find_toplevel_node(self.catalog.getCatalogModel(), schema_name, table_name)
86
86
 
87
87
  def make_speclist(self, name):
88
- d = self.config.get(unicode(name))
88
+ d = self.config.get(name)
89
89
  if d is None:
90
90
  d = [dict()]
91
91
  return AttrSpecList(self.known_attrs, d)
deriva/core/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.7.1"
1
+ __version__ = "1.7.3"
2
2
 
3
3
  from deriva.core.utils.core_utils import *
4
4
  from deriva.core.base_cli import BaseCLI, KeyValuePairArgs
deriva/core/datapath.py CHANGED
@@ -5,6 +5,7 @@ import copy
5
5
  from datetime import date
6
6
  import itertools
7
7
  import logging
8
+ import time
8
9
  import re
9
10
  from requests import HTTPError
10
11
  import warnings
@@ -695,6 +696,102 @@ class _ResultSet (object):
695
696
  logger.debug("Fetched %d entities" % len(self._results_doc))
696
697
  return self
697
698
 
699
+ def _json_size_approx(data):
700
+ """Return approximate byte count for minimal JSON encoding of data
701
+
702
+ Minimal encoding has no optional whitespace/indentation.
703
+ """
704
+ nbytes = 0
705
+
706
+ if isinstance(data, (list, tuple)):
707
+ nbytes += 2
708
+ for elem in data:
709
+ nbytes += _json_size_approx(elem) + 1
710
+ elif isinstance(data, dict):
711
+ nbytes += 2
712
+ for k, v in data.items():
713
+ nbytes += _json_size_approx(k) + _json_size_approx(v) + 2
714
+ elif isinstance(data, str):
715
+ nbytes += len(data.encode("utf-8")) + 2
716
+ else:
717
+ nbytes += len(str(data))
718
+
719
+ return nbytes
720
+
721
+ def _generate_batches(entities, max_batch_rows=1000, max_batch_bytes=250*1024):
722
+ """Generate a series of entity batches as slices of the input entities
723
+
724
+ """
725
+ if not isinstance(entities, (list, tuple)):
726
+ raise TypeError('invalid type %s for entities, list or tuple expected' % (type(entities),))
727
+
728
+ if not max_batch_rows:
729
+ logger.debug("disabling batching due to max_batch_rows=%r" % (max_batch_rows,))
730
+ return entities
731
+
732
+ top = len(entities)
733
+ lower = 0
734
+
735
+ while lower < top:
736
+ # to ensure progress, always use at least one row per batch regardless of nbytes
737
+ upper = lower + 1
738
+ batch_nbytes = _json_size_approx(entities[lower])
739
+
740
+ # advance upper position until a batch size limit is reached
741
+ while (upper - lower) < max_batch_rows:
742
+ if upper >= top:
743
+ break
744
+ batch_nbytes += _json_size_approx(entities[upper])
745
+ if batch_nbytes > max_batch_bytes:
746
+ break
747
+ upper += 1
748
+
749
+ # generate one batch and advance for next batch
750
+ logger.debug("yielding batch of %d/%d entities (%d:%d)" % (upper-lower, top, lower, upper))
751
+ yield entities[lower:upper]
752
+ lower = upper
753
+
754
+ def _request_with_retry(request_func, retry_codes={408, 429, 500, 502, 503, 504}, backoff_factor=4, max_attempts=5):
755
+ """Perform request func with exponential backoff and retry.
756
+
757
+ :param request_func: A function returning a requests.Response object or raising HTTPError
758
+ :param retry_codes: HTTPError status codes on which to attempt retry
759
+ :param backoff_factor: Base number of seconds for factor**attempt exponential backoff
760
+ :param max_attempts: Max number of request attempts.
761
+
762
+ Retry will be attempted on HTTPError exceptions which match retry_codes and
763
+ also on other unknown exceptions, presumed to be transport errors.
764
+
765
+ The request_func should do the equivalent of resp.raise_on_status() so that
766
+ it only returns a response object for successful requests.
767
+ """
768
+ attempt = 0
769
+ last_ex = None
770
+
771
+ while attempt < max_attempts:
772
+ try:
773
+ if attempt > 0:
774
+ delay = backoff_factor**(attempt-1)
775
+ logger.debug("sleeping %d seconds before retry %d..." % (delay, attempt))
776
+ time.sleep(delay)
777
+ attempt += 1
778
+ return request_func()
779
+ except HTTPError as e:
780
+ logger.debug(e.response.text)
781
+ last_ex = e
782
+ if 400 <= e.response.status_code < 500:
783
+ last_ex = DataPathException(_http_error_message(e), e)
784
+ if int(e.response.status_code) not in retry_codes:
785
+ raise last_ex
786
+ except Exception as e:
787
+ logger.debug(e.response.text)
788
+ last_ex = e
789
+
790
+ # early return means we don't get here on successful requests
791
+ logger.warning("maximum request retry limit %d exceeded" % (max_attempts,))
792
+ if last_ex is None:
793
+ raise ValueError('exceeded max_attempts without catching a request exception')
794
+ raise last_ex
698
795
 
699
796
  class _TableWrapper (object):
700
797
  """Wraps a Table for datapath expressions.
@@ -836,7 +933,7 @@ class _TableWrapper (object):
836
933
  """
837
934
  return self.path.denormalize(context_name=context_name, heuristic=heuristic, groupkey_name=groupkey_name)
838
935
 
839
- def insert(self, entities, defaults=set(), nondefaults=set(), add_system_defaults=True, on_conflict_skip=False):
936
+ def insert(self, entities, defaults=set(), nondefaults=set(), add_system_defaults=True, on_conflict_skip=False, retry_codes={408, 429, 500, 502, 503, 504}, backoff_factor=4, max_attempts=5, max_batch_rows=1000, max_batch_bytes=250*1024):
840
937
  """Inserts entities into the table.
841
938
 
842
939
  :param entities: an iterable collection of entities (i.e., rows) to be inserted into the table.
@@ -844,7 +941,23 @@ class _TableWrapper (object):
844
941
  :param nondefaults: optional, set of columns names to override implicit system defaults
845
942
  :param add_system_defaults: flag to add system columns to the set of default columns.
846
943
  :param on_conflict_skip: flag to skip entities that violate uniqueness constraints.
944
+ :param retry_codes: set of HTTP status codes for which retry should be considered.
945
+ :param backoff_factor: number of seconds for base of exponential retry backoff.
946
+ :param max_attempts: maximum number of requests attempts with retry.
947
+ :param max_batch_rows: maximum number of rows for one request, or False to disable batching.
948
+ :param max_batch_bytes: approximate maximum number of bytes for one request.
847
949
  :return a collection of newly created entities.
950
+
951
+ Retry will only be attempted for idempotent insertion
952
+ requests, which are when a user-controlled, non-nullable key
953
+ is present in the table and the key's constituent column(s)
954
+ are not listed as defaults, and on_conflict_skip=True.
955
+
956
+ When performing retries, an exponential backoff delay is
957
+ introduced after each failed attempt. The delay is
958
+ backoff_factor**attempt_number seconds for attempts 0 through
959
+ max_attempts-1.
960
+
848
961
  """
849
962
  # empty entities will be accepted but results are therefore an empty entity set
850
963
  if not entities:
@@ -879,17 +992,55 @@ class _TableWrapper (object):
879
992
  if not hasattr(entities[0], 'keys'):
880
993
  raise TypeError('entities[0] does not look like a dictionary -- does not have a "keys()" method')
881
994
 
882
- try:
883
- resp = self._schema._catalog._wrapped_catalog.post(path, json=entities, headers={'Content-Type': 'application/json'})
884
- return _ResultSet(self.path.uri, lambda ignore1, ignore2, ignore3: resp.json())
885
- except HTTPError as e:
886
- logger.debug(e.response.text)
887
- if 400 <= e.response.status_code < 500:
888
- raise DataPathException(_http_error_message(e), e)
889
- else:
890
- raise e
891
-
892
- def update(self, entities, correlation={'RID'}, targets=None):
995
+ # perform one batch request in a helper we can hand to retry helper
996
+ def request_func(batch):
997
+ return self._schema._catalog._wrapped_catalog.post(path, json=batch, headers={'Content-Type': 'application/json'})
998
+
999
+ def _has_user_pkey(table):
1000
+ """Return True if table has at least one primary key other than the system RID key"""
1001
+ for key in table.keys:
1002
+ if { c.name for c in key.unique_columns } != {'RID'}:
1003
+ if all([ not c.nullok for c in key.unique_columns ]) \
1004
+ and all([ c.name not in defaults for c in key.unique_columns ]):
1005
+ return True
1006
+ return False
1007
+
1008
+ # determine whether insert is idempotent and therefore retry safe
1009
+ retry_safe = on_conflict_skip and _has_user_pkey(self._wrapped_table)
1010
+
1011
+ # perform all requests in a helper we can hand to _ResultSet
1012
+ def results_func(ignore1, ignore2, ignore3):
1013
+ results = []
1014
+ for batch in _generate_batches(
1015
+ entities,
1016
+ max_batch_rows=max_batch_rows,
1017
+ max_batch_bytes=max_batch_bytes
1018
+ ):
1019
+ try:
1020
+ if retry_safe:
1021
+ resp = _request_with_retry(
1022
+ lambda: request_func(batch),
1023
+ retry_codes=retry_codes,
1024
+ backoff_factor=backoff_factor,
1025
+ max_attempts=max_attempts
1026
+ )
1027
+ else:
1028
+ resp = request_func(batch)
1029
+ results.extend(resp.json())
1030
+ except HTTPError as e:
1031
+ logger.debug(e.response.text)
1032
+ if 400 <= e.response.status_code < 500:
1033
+ raise DataPathException(_http_error_message(e), e)
1034
+ else:
1035
+ raise e
1036
+ return results
1037
+
1038
+ result = _ResultSet(self.path.uri, results_func)
1039
+ result.fetch()
1040
+ return result
1041
+
1042
+
1043
+ def update(self, entities, correlation={'RID'}, targets=None, retry_codes={408, 429, 500, 502, 503, 504}, backoff_factor=4, max_attempts=5, max_batch_rows=1000, max_batch_bytes=250*1024):
893
1044
  """Update entities of a table.
894
1045
 
895
1046
  For more information see the ERMrest protocol for the `attributegroup` interface. By default, this method will
@@ -901,7 +1052,17 @@ class _TableWrapper (object):
901
1052
  :param correlation: an iterable collection of column names used to correlate input set to the set of rows to be
902
1053
  updated in the catalog. E.g., `{'col name'}` or `{mytable.mycolumn}` will work if you pass a _ColumnWrapper object.
903
1054
  :param targets: an iterable collection of column names used as the targets of the update operation.
904
- :return: a collection of updated entities as returned by the corresponding ERMrest interface.
1055
+ :param retry_codes: set of HTTP status codes for which retry should be considered.
1056
+ :param backoff_factor: number of seconds for base of exponential retry backoff.
1057
+ :param max_attempts: maximum number of requests attempts with retry.
1058
+ :param max_batch_rows: maximum number of rows for one request, or False to disable batching.
1059
+ :param max_batch_bytes: approximate maximum number of bytes for one request.
1060
+ :return a collection of newly created entities.
1061
+
1062
+ When performing retries, an exponential backoff delay is
1063
+ introduced after each failed attempt. The delay is
1064
+ backoff_factor**attempt_number seconds for attempts 0 through
1065
+ max_attempts-1.
905
1066
  """
906
1067
  # empty entities will be accepted but results are therefore an empty entity set
907
1068
  if not entities:
@@ -936,16 +1097,37 @@ class _TableWrapper (object):
936
1097
  targets=','.join(target_cnames)
937
1098
  )
938
1099
 
939
- try:
940
- resp = self._schema._catalog._wrapped_catalog.put(path, json=entities, headers={'Content-Type': 'application/json'})
941
- return _ResultSet(self.path.uri, lambda ignore1, ignore2, ignore3: resp.json())
942
- except HTTPError as e:
943
- logger.debug(e.response.text)
944
- if 400 <= e.response.status_code < 500:
945
- raise DataPathException(_http_error_message(e), e)
946
- else:
947
- raise e
948
-
1100
+ # perform one batch request in a helper we can hand to retry helper
1101
+ def request_func(batch):
1102
+ return self._schema._catalog._wrapped_catalog.put(path, json=batch, headers={'Content-Type': 'application/json'})
1103
+
1104
+ # perform all requests in a helper we can hand to _ResultSet
1105
+ def results_func(ignore1, ignore2, ignore3):
1106
+ results = []
1107
+ for batch in _generate_batches(
1108
+ entities,
1109
+ max_batch_rows=max_batch_rows,
1110
+ max_batch_bytes=max_batch_bytes
1111
+ ):
1112
+ try:
1113
+ resp = _request_with_retry(
1114
+ lambda: request_func(batch),
1115
+ retry_codes=retry_codes,
1116
+ backoff_factor=backoff_factor,
1117
+ max_attempts=max_attempts
1118
+ )
1119
+ results.extend(resp.json())
1120
+ except HTTPError as e:
1121
+ logger.debug(e.response.text)
1122
+ if 400 <= e.response.status_code < 500:
1123
+ raise DataPathException(_http_error_message(e), e)
1124
+ else:
1125
+ raise e
1126
+ return results
1127
+
1128
+ result = _ResultSet(self.path.uri, results_func)
1129
+ result.fetch()
1130
+ return result
949
1131
 
950
1132
  class _TableAlias (_TableWrapper):
951
1133
  """Represents a table alias in datapath expressions.
@@ -53,11 +53,15 @@ class DerivaServer (DerivaBinding):
53
53
  """
54
54
  return ErmrestCatalog.connect(self, catalog_id, snaptime)
55
55
 
56
- def create_ermrest_catalog(self, id=None, owner=None):
56
+ def create_ermrest_catalog(self, id=None, owner=None, name=None, description=None, is_persistent=None, clone_source=None):
57
57
  """Create an ERMrest catalog.
58
58
 
59
59
  :param id: The (str) id desired by the client (default None)
60
60
  :param owner: The initial (list of str) ACL desired by the client (default None)
61
+ :param name: Initial (str) catalog name if not None
62
+ :param description: Initial (str) catalog description if not None
63
+ :param is_persistent: Initial (bool) catalog persistence flag if not None
64
+ :param clone_source: Initial catalog clone_source if not None
61
65
 
62
66
  The new catalog id will be returned in the response, and used
63
67
  in future catalog access. The use of the id parameter
@@ -77,8 +81,17 @@ class DerivaServer (DerivaBinding):
77
81
  owner ACL influences which client(s) are allowed to retry
78
82
  creation with the same id.
79
83
 
84
+ The name, description, is_persistent, and clone_source
85
+ parameters are passed through to the catalog creation service
86
+ to initialize those respective metadata fields of the new
87
+ catalog's registry entry. See ERMrest documentation for more
88
+ detail. Authorization failures may occur when attempting to
89
+ set the is_persistent flag. By default, these fields are not
90
+ initialized in the catalog creation request, and they instead
91
+ receive server-assigned defaults.
92
+
80
93
  """
81
- return ErmrestCatalog.create(self, id, owner)
94
+ return ErmrestCatalog.create(self, id, owner, name, description, is_persistent, clone_source)
82
95
 
83
96
  def connect_ermrest_alias(self, id):
84
97
  """Connect to an ERMrest alias and return the alias binding.
@@ -88,12 +101,14 @@ class DerivaServer (DerivaBinding):
88
101
  """
89
102
  return ErmrestAlias.connect(self, id)
90
103
 
91
- def create_ermrest_alias(self, id=None, owner=None, alias_target=None):
104
+ def create_ermrest_alias(self, id=None, owner=None, alias_target=None, name=None, description=None):
92
105
  """Create an ERMrest catalog alias.
93
106
 
94
107
  :param id: The (str) id desired by the client (default None)
95
108
  :param owner: The initial (list of str) ACL desired by the client (default None)
96
109
  :param alias_target: The initial target catalog id binding desired by the client (default None)
110
+ :param name: Initial (str) catalog name if not None
111
+ :param description: Initial (str) catalog description if not None
97
112
 
98
113
  The new alias id will be returned in the response, and used
99
114
  in future alias access. The use of the id parameter
@@ -118,8 +133,13 @@ class DerivaServer (DerivaBinding):
118
133
  influences which client(s) are allowed to retry creation with
119
134
  the same id.
120
135
 
136
+ The name and description parameters are passed through to the
137
+ alias creation service to initialize those respective metadata
138
+ fields of the new aliase's registry entry. See ERMrest
139
+ documentation for more detail.
140
+
121
141
  """
122
- return ErmrestAlias.create(self, id, owner, alias_target)
142
+ return ErmrestAlias.create(self, id, owner, alias_target, name, description)
123
143
 
124
144
  class ErmrestCatalogMutationError(Exception):
125
145
  pass
@@ -204,15 +224,22 @@ class ErmrestCatalog(DerivaBinding):
204
224
  )
205
225
 
206
226
  @classmethod
207
- def _digest_catalog_args(cls, id, owner):
227
+ def _digest_catalog_args(cls, id, owner, name=None, description=None, is_persistent=None, clone_source=None):
208
228
  rep = dict()
209
229
 
210
- if isinstance(id, str):
211
- rep['id'] = id
212
- elif isinstance(id, (type(nochange), type(None))):
213
- pass
214
- else:
215
- raise TypeError('id must be of type str or None or nochange, not %s' % type(id))
230
+ for v, k, typ in [
231
+ (id, 'id', str),
232
+ (name, 'name', str),
233
+ (description, 'description', str),
234
+ (is_persistent, 'is_persistent', bool),
235
+ (clone_source, 'clone_source', str),
236
+ ]:
237
+ if isinstance(v, typ):
238
+ rep[k] = v
239
+ elif isinstance(v, (type(nochange), type(None))):
240
+ pass
241
+ else:
242
+ raise TypeError('%s must be of type %s or None or nochange, not %s' % (k, typ.__name__, type(v)))
216
243
 
217
244
  if isinstance(owner, list):
218
245
  for e in owner:
@@ -227,12 +254,16 @@ class ErmrestCatalog(DerivaBinding):
227
254
  return rep
228
255
 
229
256
  @classmethod
230
- def create(cls, deriva_server, id=None, owner=None):
257
+ def create(cls, deriva_server, id=None, owner=None, name=None, description=None, is_persistent=None, clone_source=None):
231
258
  """Create an ERMrest catalog and return the ERMrest catalog binding.
232
259
 
233
260
  :param deriva_server: The DerivaServer binding which hosts ermrest.
234
261
  :param id: The (str) id desired by the client (default None)
235
262
  :param owner: The initial (list of str) ACL desired by the client (default None)
263
+ :param name: Initial (str) catalog name if not None
264
+ :param description: Initial (str) catalog description if not None
265
+ :param is_persistent: Initial (bool) catalog persistence flag if not None
266
+ :param clone_source: Initial catalog clone_source if not None
236
267
 
237
268
  The new catalog id will be returned in the response, and used
238
269
  in future catalog access. The use of the id parameter
@@ -252,9 +283,18 @@ class ErmrestCatalog(DerivaBinding):
252
283
  influences which client(s) are allowed to retry creation with
253
284
  the same id.
254
285
 
286
+ The name, description, is_persistent, and clone_source
287
+ parameters are passed through to the catalog creation service
288
+ to initialize those respective metadata fields of the new
289
+ catalog's registry entry. See ERMrest documentation for more
290
+ detail. Authorization failures may occur when attempting to
291
+ set the is_persistent flag. By default, these fields are not
292
+ initialized in the catalog creation request, and they instead
293
+ receive server-assigned defaults.
294
+
255
295
  """
256
296
  path = '/ermrest/catalog'
257
- r = deriva_server.post(path, json=cls._digest_catalog_args(id, owner))
297
+ r = deriva_server.post(path, json=cls._digest_catalog_args(id, owner, name, description, is_persistent, clone_source))
258
298
  r.raise_for_status()
259
299
  return cls.connect(deriva_server, r.json()['id'])
260
300
 
@@ -655,7 +695,8 @@ class ErmrestCatalog(DerivaBinding):
655
695
  copy_annotations=True,
656
696
  copy_policy=True,
657
697
  truncate_after=True,
658
- exclude_schemas=None):
698
+ exclude_schemas=None,
699
+ dst_properties=None):
659
700
  """Clone this catalog's content into dest_catalog, creating a new catalog if needed.
660
701
 
661
702
  :param dst_catalog: Destination catalog or None to request creation of new destination (default).
@@ -664,13 +705,22 @@ class ErmrestCatalog(DerivaBinding):
664
705
  :param copy_policy: Copy access-control policies when True (default).
665
706
  :param truncate_after: Truncate destination history after cloning when True (default).
666
707
  :param exclude_schemas: A list of schema names to exclude from the cloning process.
708
+ :param dst_properties: A dictionary of custom catalog-creation properties.
667
709
 
668
- When dest_catalog is provided, attempt an idempotent clone,
710
+ When dst_catalog is provided, attempt an idempotent clone,
669
711
  assuming content MAY be partially cloned already using the
670
712
  same parameters. This routine uses a table-level annotation
671
713
  "tag:isrd.isi.edu,2018:clone-state" to save progress markers
672
714
  which help it restart efficiently if interrupted.
673
715
 
716
+ When dst_catalog is not provided, a new catalog is
717
+ provisioned. The optional dst_properties can customize
718
+ metadata properties during this step:
719
+
720
+ - name: str
721
+ - description: str (markdown-formatted)
722
+ - is_persistent: boolean
723
+
674
724
  Cloning preserves source row RID values for application tables
675
725
  so that any RID-based foreign keys are still valid. It is not
676
726
  generally advisable to try to merge more than one source into
@@ -692,10 +742,33 @@ class ErmrestCatalog(DerivaBinding):
692
742
  session_config["allow_retry_on_all_methods"] = True
693
743
 
694
744
  if dst_catalog is None:
695
- # TODO: refactor with DerivaServer someday
696
- server = DerivaBinding(self._scheme, self._server, self._credentials, self._caching, session_config)
697
- dst_id = server.post("/ermrest/catalog").json()["id"]
698
- dst_catalog = ErmrestCatalog(self._scheme, self._server, dst_id, self._credentials, self._caching, session_config)
745
+ if dst_properties is not None:
746
+ if not isinstance(dst_properties, dict):
747
+ raise TypeError('dst_properties must be of type dict or None, not %s' % (type(dst_properties),))
748
+ else:
749
+ dst_properties = {}
750
+ kwargs = {
751
+ "name": dst_properties.get('name', 'Clone of %r' % (self._catalog_id,)),
752
+ "description": dst_properties.get(
753
+ 'description',
754
+ '''A cloned copy of catalog %r made with ErmrestCatalog.clone_catalog() using the following parameters:
755
+ - `copy_data`: %r
756
+ - `copy_annotations`: %r
757
+ - `copy_policy`: %r
758
+ - `truncate_after`: %r
759
+ - `exclude_schemas`: %r
760
+ ''' % (
761
+ self._catalog_id,
762
+ copy_data,
763
+ copy_annotations,
764
+ copy_policy,
765
+ truncate_after,
766
+ exclude_schemas,
767
+ )),
768
+ "clone_source": dst_properties.get('clone_source', self._catalog_id),
769
+ }
770
+ server = self.deriva_server
771
+ dst_catalog = server.create_ermrest_catalog(**kwargs)
699
772
 
700
773
  # set top-level config right away and find fatal usage errors...
701
774
  if copy_policy:
@@ -1051,8 +1124,8 @@ class ErmrestAlias(DerivaBinding):
1051
1124
  )
1052
1125
 
1053
1126
  @classmethod
1054
- def _digest_alias_args(cls, id, owner, alias_target):
1055
- rep = ErmrestCatalog._digest_catalog_args(id, owner)
1127
+ def _digest_alias_args(cls, id, owner, alias_target, name, description):
1128
+ rep = ErmrestCatalog._digest_catalog_args(id, owner, name, description)
1056
1129
 
1057
1130
  if isinstance(alias_target, (str, type(None))):
1058
1131
  rep['alias_target'] = alias_target
@@ -1064,13 +1137,15 @@ class ErmrestAlias(DerivaBinding):
1064
1137
  return rep
1065
1138
 
1066
1139
  @classmethod
1067
- def create(cls, deriva_server, id=None, owner=None, alias_target=None):
1140
+ def create(cls, deriva_server, id=None, owner=None, alias_target=None, name=None, description=None):
1068
1141
  """Create an ERMrest catalog alias.
1069
1142
 
1070
1143
  :param deriva_server: The DerivaServer binding which hosts ermrest
1071
1144
  :param id: The (str) id desired by the client (default None)
1072
1145
  :param owner: The initial (list of str) ACL desired by the client (default None)
1073
1146
  :param alias_target: The initial target catalog id desired by the client (default None)
1147
+ :param name: Initial (str) catalog name if not None
1148
+ :param description: Initial (str) catalog description if not None
1074
1149
 
1075
1150
  The new alias id will be returned in the response, and used
1076
1151
  in future alias access. The use of the id parameter
@@ -1095,9 +1170,14 @@ class ErmrestAlias(DerivaBinding):
1095
1170
  influences which client(s) are allowed to retry creation with
1096
1171
  the same id.
1097
1172
 
1173
+ The name and description parameters are passed through to the
1174
+ alias creation service to initialize those respective metadata
1175
+ fields of the new aliase's registry entry. See ERMrest
1176
+ documentation for more detail.
1177
+
1098
1178
  """
1099
1179
  path = '/ermrest/alias'
1100
- r = deriva_server.post(path, json=cls._digest_alias_args(id, owner, alias_target))
1180
+ r = deriva_server.post(path, json=cls._digest_alias_args(id, owner, alias_target, name, description))
1101
1181
  r.raise_for_status()
1102
1182
  return cls.connect(deriva_server, r.json()['id'])
1103
1183
 
@@ -1,7 +1,13 @@
1
1
 
2
+ from __future__ import annotations
3
+
2
4
  from collections import OrderedDict
5
+ from collections.abc import Iterable
6
+ from enum import Enum
3
7
  import json
4
8
  import re
9
+ import base64
10
+ import hashlib
5
11
 
6
12
  from . import AttrDict, tag, urlquote, stob
7
13
 
@@ -17,6 +23,74 @@ class NoChange (object):
17
23
  # singletone to use in APIs below
18
24
  nochange = NoChange()
19
25
 
26
+ def make_id(*components):
27
+ """Build an identifier that will be OK for ERMrest and Postgres.
28
+
29
+ Naively, append as '_'.join(components).
30
+
31
+ Fallback to heuristics mixing truncation with short hashes.
32
+ """
33
+ # accept lists at top-level for convenience (compound keys, etc.)
34
+ expanded = []
35
+ for e in components:
36
+ if isinstance(e, list):
37
+ expanded.extend(e)
38
+ else:
39
+ expanded.append(e)
40
+
41
+ # prefer to use naive name as requested
42
+ naive_result = '_'.join(expanded)
43
+ naive_len = len(naive_result.encode('utf8'))
44
+ if naive_len <= 63:
45
+ return naive_result
46
+
47
+ # we'll need to truncate and hash in some way...
48
+ def hash(s, nbytes):
49
+ return base64.urlsafe_b64encode(hashlib.md5(s.encode('utf8')).digest()).decode()[0:nbytes]
50
+
51
+ def truncate(s, maxlen):
52
+ encoded_len = len(s.encode('utf8'))
53
+ # we need to chop whole (unicode) chars but test encoded byte lengths!
54
+ for i in range(max(1, len(s) - maxlen), len(s) - 1):
55
+ result = s[0:-1 * i].rstrip()
56
+ if len(result.encode('utf8')) <= (maxlen - 2):
57
+ return result + '..'
58
+ return s
59
+
60
+ naive_hash = hash(naive_result, 5)
61
+ parts = [
62
+ (i, expanded[i])
63
+ for i in range(len(expanded))
64
+ ]
65
+
66
+ # try to find a solution truncating individual fields
67
+ for maxlen in [15, 12, 9]:
68
+ parts.sort(key=lambda p: (len(p[1].encode('utf8')), p[0]), reverse=True)
69
+ for i in range(len(parts)):
70
+ idx, part = parts[i]
71
+ if len(part.encode('utf8')) > maxlen:
72
+ parts[i] = (idx, truncate(part, maxlen))
73
+ candidate_result = '_'.join([
74
+ p[1]
75
+ for p in sorted(parts, key=lambda p: p[0])
76
+ ] + [naive_hash])
77
+ if len(candidate_result.encode('utf8')) < 63:
78
+ return candidate_result
79
+
80
+ # fallback to truncating original naive name
81
+ # try to preserve suffix and trim in middle
82
+ result = ''.join([
83
+ truncate(naive_result, len(naive_result)//3),
84
+ naive_result[-len(naive_result)//3:],
85
+ '_',
86
+ naive_hash
87
+ ])
88
+ if len(result.encode('utf8')) <= 63:
89
+ return result
90
+
91
+ # last-ditch (e.g. multibyte unicode suffix worst case)
92
+ return truncate(naive_result, 55) + naive_hash
93
+
20
94
  def presence_annotation(tag_uri):
21
95
  """Decorator to establish property getter/setter/deleter for presence annotations.
22
96
 
@@ -684,6 +758,15 @@ class KeyedList (list):
684
758
  list.append(self, e)
685
759
  self.elements[e.name] = e
686
760
 
761
+ class FindAssociationResult (object):
762
+ """Wrapper for results of Table.find_associations()"""
763
+ def __init__(self, table, self_fkey, other_fkeys):
764
+ self.table = table
765
+ self.name = table.name
766
+ self.schema = table.schema
767
+ self.self_fkey = self_fkey
768
+ self.other_fkeys = other_fkeys
769
+
687
770
  class Table (object):
688
771
  """Named table.
689
772
  """
@@ -1075,6 +1158,184 @@ class Table (object):
1075
1158
  provide_system
1076
1159
  )
1077
1160
 
1161
+ @classmethod
1162
+ def define_association(
1163
+ cls,
1164
+ associates: Iterable[Key | Table | tuple[str, Key | Table]],
1165
+ metadata: Iterable[Key | Table | dict | tuple[str, bool, Key | Table]] = [],
1166
+ table_name: str | None = None,
1167
+ comment: str | None = None,
1168
+ provide_system: bool = True) -> dict:
1169
+ """Build an association table definition.
1170
+
1171
+ :param associates: the existing Key instances being associated
1172
+ :param metadata: additional metadata fields for impure associations
1173
+ :param table_name: name for the association table or None for default naming
1174
+ :param comment: comment for the association table or None for default comment
1175
+ :param provide_system: add ERMrest system columns when True
1176
+
1177
+ This is a utility function to help build an association table
1178
+ definition. It simplifies the task, but removes some
1179
+ control. For full customization, consider using Table.define()
1180
+ directly instead.
1181
+
1182
+ A normal ("pure") N-ary association is a table with N foreign
1183
+ keys referencing N primary keys in referenced tables, with a
1184
+ composite primary key covering the N foreign keys. These pure
1185
+ association tables manage a set of distinct combinations of
1186
+ the associated foreign key values.
1187
+
1188
+ An "impure" association table adds additional metadata
1189
+ alongside the N foreign keys.
1190
+
1191
+ The "associates" parameter takes an iterable of Key instances
1192
+ from other tables. The association will be comprised of
1193
+ foreign keys referencing these associates. Optionally, a tuple
1194
+ of (str, Key) can supply a string _base name_ to influence how
1195
+ the foreign key columns and constraint will be named in the
1196
+ new association table. A bare Key instance will get a base
1197
+ name derived from the referenced table name.
1198
+
1199
+ The "metadata" parameter takes an iterable of plain dict
1200
+ column definitions or Key instances. Each dict must be a
1201
+ scalar column definition, such as produced by the
1202
+ Column.define() class method. Key instance will cause
1203
+ corresponding columns and foreign keys to be added to the
1204
+ association table to act as metadata. Optionally, a tuple of
1205
+ (str, bool, Key) can supply a string _base name_ and a boolean
1206
+ _nullok_ property to influence how the foreign key columns and
1207
+ constraint will be constructed and named. A bare Key instance
1208
+ will get a base name derived from the referened table name,
1209
+ and presumed as nullok=False.
1210
+
1211
+ If a Table instance is supplied instead of a Key instance for
1212
+ associates or metadata inputs, an attempt will be made to
1213
+ locate a key based on the RID system column. If this key
1214
+ cannot be found, a KeyError will be raised.
1215
+
1216
+ """
1217
+ associates = list(associates)
1218
+ metadata = list(metadata)
1219
+
1220
+ if len(associates) < 2:
1221
+ raise ValueError('An association table requires at least 2 associates')
1222
+
1223
+ cdefs = []
1224
+ kdefs = []
1225
+ fkdefs = []
1226
+
1227
+ used_names = set()
1228
+
1229
+ def check_basename(basename):
1230
+ if not isinstance(base_name, str):
1231
+ raise TypeError('Base name %r is not of required type str' % (base_name,))
1232
+ if base_name in used_names:
1233
+ raise ValueError('Base name %r is not unique among associates and metadata' % (base_name,))
1234
+ used_names.add(base_name)
1235
+
1236
+ def choose_basename(key):
1237
+ base_name = key.table.name
1238
+ n = 2
1239
+ while base_name in used_names:
1240
+ base_name = '%s%d' % (key.table.name, n)
1241
+ n += 1
1242
+ used_names.add(base_name)
1243
+ return base_name
1244
+
1245
+ def check_key(key):
1246
+ if isinstance(key, Table):
1247
+ return key.key_by_columns(["RID"])
1248
+ return key
1249
+
1250
+ # check and normalize associates into list[(str, Key)] with distinct base names
1251
+ for i in range(len(associates)):
1252
+ if isinstance(associates[i], tuple):
1253
+ base_name, key = associates[i]
1254
+ check_basename(base_name)
1255
+ key = check_key(key)
1256
+ associates[i] = (base_name, key)
1257
+ else:
1258
+ key = check_key(associates[i])
1259
+ base_name = choose_basename(key)
1260
+ associates[i] = (base_name, key)
1261
+
1262
+ # build assoc table name if not provided
1263
+ if table_name is None:
1264
+ table_name = make_id(*[ assoc[1].table.name for assoc in associates ])
1265
+
1266
+ def simplify_type(ctype):
1267
+ if ctype.is_domain and ctype.typename.startswith('ermrest_'):
1268
+ return ctype.base_type
1269
+
1270
+ return ctype
1271
+
1272
+ def cdefs_for_key(base_name, key, nullok=False):
1273
+ return [
1274
+ Column.define(
1275
+ '%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name,
1276
+ simplify_type(col.type),
1277
+ nullok=nullok,
1278
+ )
1279
+ for col in key.unique_columns
1280
+ ]
1281
+
1282
+ def fkdef_for_key(base_name, key):
1283
+ return ForeignKey.define(
1284
+ [
1285
+ '%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name
1286
+ for col in key.unique_columns
1287
+ ],
1288
+ key.table.schema.name,
1289
+ key.table.name,
1290
+ [ col.name for col in key.unique_columns ],
1291
+ on_update='CASCADE',
1292
+ on_delete='CASCADE',
1293
+ constraint_name=make_id(table_name, base_name, 'fkey'),
1294
+ )
1295
+
1296
+ # build core association definition (i.e. the "pure" parts)
1297
+ k_cnames = []
1298
+ for base_name, key in associates:
1299
+ cdefs.extend(cdefs_for_key(base_name, key))
1300
+ fkdefs.append(fkdef_for_key(base_name, key))
1301
+
1302
+ k_cnames.extend([
1303
+ '%s_%s' % (base_name, col.name) if len(key.unique_columns) > 1 else base_name
1304
+ for col in key.unique_columns
1305
+ ])
1306
+
1307
+ kdefs.append(
1308
+ Key.define(
1309
+ k_cnames,
1310
+ constraint_name=make_id(table_name, 'assoc', 'key'),
1311
+ )
1312
+ )
1313
+
1314
+ # check and normalize metadata into list[dict | (str, bool, Key)]
1315
+ for i in range(len(metadata)):
1316
+ if isinstance(metadata[i], tuple):
1317
+ base_name, nullok, key = metadata[i]
1318
+ check_basename(base_name)
1319
+ key = check_key(key)
1320
+ metadata[i] = (base_name, nullok, key)
1321
+ elif isinstance(metadata[i], dict):
1322
+ pass
1323
+ else:
1324
+ key = check_key(metadata[i])
1325
+ base_name = choose_basename(key)
1326
+ metadata[i] = (base_name, False, key)
1327
+
1328
+ # add metadata to definition
1329
+ for md in metadata:
1330
+ if isinstance(md, dict):
1331
+ cdefs.append(md)
1332
+ else:
1333
+ base_name, nullok, key = md
1334
+ cdefs.extend(cdefs_for_key(base_name, key, nullok))
1335
+ fkdefs.append(fkdef_for_key(base_name, key))
1336
+
1337
+ return Table.define(table_name, cdefs, kdefs, fkdefs, comment=comment, provide_system=provide_system)
1338
+
1078
1339
  def prejson(self, prune=True):
1079
1340
  return {
1080
1341
  "schema_name": self.schema.name,
@@ -1348,7 +1609,7 @@ class Table (object):
1348
1609
  if raise_nomatch:
1349
1610
  raise KeyError(from_to_map)
1350
1611
 
1351
- def is_association(self, min_arity=2, max_arity=2, unqualified=True, pure=True, no_overlap=True):
1612
+ def is_association(self, min_arity=2, max_arity=2, unqualified=True, pure=True, no_overlap=True, return_fkeys=False):
1352
1613
  """Return (truthy) integer arity if self is a matching association, else False.
1353
1614
 
1354
1615
  min_arity: minimum number of associated fkeys (default 2)
@@ -1356,6 +1617,7 @@ class Table (object):
1356
1617
  unqualified: reject qualified associations when True (default True)
1357
1618
  pure: reject impure assocations when True (default True)
1358
1619
  no_overlap: reject overlapping associations when True (default True)
1620
+ return_fkeys: return the set of N associated ForeignKeys if True
1359
1621
 
1360
1622
  The default behavior with no arguments is to test for pure,
1361
1623
  unqualified, non-overlapping, binary assocations.
@@ -1444,9 +1706,43 @@ class Table (object):
1444
1706
  # reject: impure association
1445
1707
  return False
1446
1708
 
1447
- # return (truthy) arity
1448
- return len(covered_fkeys)
1709
+ # return (truthy) arity or fkeys
1710
+ if return_fkeys:
1711
+ return covered_fkeys
1712
+ else:
1713
+ return len(covered_fkeys)
1714
+
1715
+ def find_associations(self, min_arity=2, max_arity=2, unqualified=True, pure=True, no_overlap=True) -> Iterable[FindAssociationResult]:
1716
+ """Yield (iterable) Association objects linking to this table and meeting all criteria.
1449
1717
 
1718
+ min_arity: minimum number of associated fkeys (default 2)
1719
+ max_arity: maximum number of associated fkeys (default 2) or None
1720
+ unqualified: reject qualified associations when True (default True)
1721
+ pure: reject impure assocations when True (default True)
1722
+ no_overlap: reject overlapping associations when True (default True)
1723
+
1724
+ See documentation for sibling method Table.is_association(...)
1725
+ for more explanation of these association detection criteria.
1726
+
1727
+ """
1728
+ peer_tables = set()
1729
+ for fkey in self.referenced_by:
1730
+ peer = fkey.table
1731
+ if peer in peer_tables:
1732
+ # check each peer only once
1733
+ continue
1734
+ peer_tables.add(peer)
1735
+ answer = peer.is_association(min_arity=min_arity, max_arity=max_arity, unqualified=unqualified, pure=pure, no_overlap=no_overlap, return_fkeys=True)
1736
+ if answer:
1737
+ answer = set(answer)
1738
+ for fkey in answer:
1739
+ if fkey.pk_table == self:
1740
+ answer.remove(fkey)
1741
+ yield FindAssociationResult(peer, fkey, answer)
1742
+ # arbitrarily choose first fkey to self
1743
+ # in case association is back to same table
1744
+ break
1745
+
1450
1746
  @presence_annotation(tag.immutable)
1451
1747
  def immutable(self): pass
1452
1748
 
@@ -1495,6 +1791,40 @@ class Table (object):
1495
1791
  @object_annotation(tag.viz_3d_display)
1496
1792
  def viz_3d_display(self): pass
1497
1793
 
1794
+ class Quantifier (str, Enum):
1795
+ """Logic quantifiers"""
1796
+ any = 'any'
1797
+ all = 'all'
1798
+
1799
+ def find_tables_with_foreign_keys(target_tables: Iterable[Table], quantifier: Quantifier=Quantifier.all) -> set[Table]:
1800
+ """Return set of tables with foreign key references to target tables.
1801
+
1802
+ :param target_tables: an iterable of ermrest_model.Table instances
1803
+ :param quantifier: one of the Quantifiers 'any' or 'all' (default 'all')
1804
+
1805
+ Each returned Table instance will be a table that references the
1806
+ targets according to the selected quantifier. A reference is a
1807
+ direct foreign key in the returned table that refers to a primary
1808
+ key of the target table.
1809
+
1810
+ - quantifier==all: a returned table references ALL targets
1811
+ - quantifier==any: a returned table references AT LEAST ONE target
1812
+
1813
+ For proper function, all target_tables instances MUST come from
1814
+ the same root Model instance hierarchy.
1815
+
1816
+ """
1817
+ candidates = None
1818
+ for table in target_tables:
1819
+ referring = { fkey.table for fkey in table.referenced_by }
1820
+ if candidates is None:
1821
+ candidates = referring
1822
+ elif quantifier == Quantifier.all:
1823
+ candidates.intersection_update(referring)
1824
+ else:
1825
+ candidates.update(referring)
1826
+ return candidates
1827
+
1498
1828
  class Column (object):
1499
1829
  """Named column.
1500
1830
  """
@@ -1696,7 +2026,6 @@ class Column (object):
1696
2026
 
1697
2027
  @object_annotation(tag.column_display)
1698
2028
  def column_display(self): pass
1699
-
1700
2029
 
1701
2030
  def _constraint_name_parts(constraint, doc):
1702
2031
  # modern systems should have 0 or 1 names here
@@ -1781,10 +2110,29 @@ class Key (object):
1781
2110
  }
1782
2111
 
1783
2112
  @classmethod
1784
- def define(cls, colnames, constraint_names=[], comment=None, annotations={}):
1785
- """Build a key definition."""
2113
+ def define(cls, colnames, constraint_names=[], comment=None, annotations={}, constraint_name=None):
2114
+ """Build a key definition.
2115
+
2116
+ :param colnames: List of names of columns participating in the key
2117
+ :param constraint_names: Legacy input [ [ schema_name, constraint_name ] ] (for API backwards-compatibility)
2118
+ :param comment: Comment string
2119
+ :param annotations: Dictionary of { annotation_uri: annotation_value, ... }
2120
+ :param constraint_name: Constraint name string
2121
+
2122
+ The constraint_name kwarg takes a bare constraint name string
2123
+ and acts the same as setting the legacy constraint_names kwarg
2124
+ to: [ [ "placeholder", constraint_name ] ]. This odd syntax
2125
+ is for backwards-compatibility with earlier API versions, and
2126
+ mirrors the structure of constraint names in ERMrest model
2127
+ description outputs. In those outputs, the "placeholder" field
2128
+ contains the schema name of the table containing the
2129
+ constraint.
2130
+
2131
+ """
1786
2132
  if not isinstance(colnames, list):
1787
2133
  raise TypeError('Colnames should be a list.')
2134
+ if constraint_name is not None:
2135
+ constraint_names = [ [ "placeholder", constraint_name ] ]
1788
2136
  return {
1789
2137
  'unique_columns': list(colnames),
1790
2138
  'names': constraint_names,
@@ -1983,9 +2331,41 @@ class ForeignKey (object):
1983
2331
  }
1984
2332
 
1985
2333
  @classmethod
1986
- def define(cls, fk_colnames, pk_sname, pk_tname, pk_colnames, on_update='NO ACTION', on_delete='NO ACTION', constraint_names=[], comment=None, acls={}, acl_bindings={}, annotations={}):
2334
+ def define(cls, fk_colnames, pk_sname, pk_tname, pk_colnames, on_update='NO ACTION', on_delete='NO ACTION', constraint_names=[], comment=None, acls={}, acl_bindings={}, annotations={}, constraint_name=None):
2335
+ """Define a foreign key.
2336
+
2337
+ :param fk_colnames: List of column names participating in the foreign key
2338
+ :param pk_sname: Schema name string of the referenced primary key
2339
+ :param pk_tname: Table name string of the referenced primary key
2340
+ :param pk_colnames: List of column names participating in the referenced primary key
2341
+ :param on_update: Constraint behavior when referenced primary keys are updated
2342
+ :param on_update: Constraint behavior when referenced primary keys are deleted
2343
+ :param constraint_names: Legacy input [ [ schema_name, constraint_name ] ] (for API backwards-compatibility)
2344
+ :param comment: Comment string
2345
+ :param acls: Dictionary of { acl_name: acl, ... }
2346
+ :param acl_bindings: Dictionary of { binding_name: acl_binding, ... }
2347
+ :param annotations: Dictionary of { annotation_uri: annotation_value, ... }
2348
+ :param constraint_name: Constraint name string
2349
+
2350
+ The contraint behavior values for on_update and on_delete must
2351
+ be one of the following literal strings:
2352
+
2353
+ 'NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT'
2354
+
2355
+ The constraint_name kwarg takes a bare constraint name string
2356
+ and acts the same as setting the legacy constraint_names kwarg
2357
+ to: [ [ "placeholder", constraint_name ] ]. This odd syntax
2358
+ is for backwards-compatibility with earlier API versions, and
2359
+ mirrors the structure of constraint names in ERMrest model
2360
+ description outputs. In those outputs, the "placeholder" field
2361
+ contains the schema name of the table containing the
2362
+ constraint.
2363
+
2364
+ """
1987
2365
  if len(fk_colnames) != len(pk_colnames):
1988
2366
  raise ValueError('The fk_colnames and pk_colnames lists must have the same length.')
2367
+ if constraint_name is not None:
2368
+ constraint_names = [ [ "placeholder", constraint_name ], ]
1989
2369
  return {
1990
2370
  'foreign_key_columns': [
1991
2371
  {
deriva/core/hatrac_cli.py CHANGED
@@ -185,14 +185,14 @@ class DerivaHatracCLI (BaseCLI):
185
185
 
186
186
  try:
187
187
  acls = self.store.get_acl(self.resource, args.access, args.role)
188
+ if acls is None:
189
+ raise DerivaHatracCLIException('No such object or namespace or ACL entry')
188
190
  for access in acls:
189
191
  print("%s:" % access)
190
192
  for role in acls.get(access, []):
191
193
  print(" %s" % role)
192
194
  except HTTPError as e:
193
- if e.response.status_code == requests.codes.not_found:
194
- raise ResourceException('No such object or namespace or ACL entry', e)
195
- elif e.response.status_code == requests.codes.bad_request:
195
+ if e.response.status_code == requests.codes.bad_request:
196
196
  raise ResourceException('Invalid ACL name %s' % args.access, e)
197
197
  else:
198
198
  raise e
@@ -316,6 +316,8 @@ class DerivaHatracCLI (BaseCLI):
316
316
  except HatracHashMismatch as e:
317
317
  logging.debug(format_exception(e))
318
318
  eprint(_resource_error_message('Checksum verification failed'))
319
+ except DerivaHatracCLIException as e:
320
+ eprint(e)
319
321
  except RuntimeError as e:
320
322
  logging.debug(format_exception(e))
321
323
  eprint('Unexpected runtime error occurred')
@@ -52,7 +52,7 @@ class GlobusAuthUtil:
52
52
  client_id = kwargs.get("client_id")
53
53
  client_secret = kwargs.get("client_secret")
54
54
  if not (client_id and client_secret):
55
- cred_file = kwargs.get("credential_file", CLIENT_CRED_FILE)
55
+ cred_file = kwargs.get("credential_file", CLIENT_CRED_FILE) or CLIENT_CRED_FILE
56
56
  if os.path.isfile(cred_file):
57
57
  creds = read_config(cred_file)
58
58
  if creds:
@@ -60,6 +60,8 @@ class GlobusAuthUtil:
60
60
  if client:
61
61
  client_id = client.get('client_id')
62
62
  client_secret = client.get('client_secret')
63
+ else:
64
+ logging.warning("No Globus client credential file found at: %s" % cred_file)
63
65
 
64
66
  if not (client_id and client_secret):
65
67
  logging.warning("Client ID and secret not specified and/or could not be determined.")
@@ -233,8 +233,11 @@ class DerivaUpload(object):
233
233
  self.credentials = credentials
234
234
  self.catalog.set_credentials(self.credentials, host)
235
235
  self.store.set_credentials(self.credentials, host)
236
- attributes = self.catalog.get_authn_session().json()
237
- self.identity = attributes.get("client", self.identity)
236
+ try:
237
+ attributes = self.catalog.get_authn_session().json()
238
+ self.identity = attributes.get("client", self.identity)
239
+ except Exception as e:
240
+ logger.warning("Unable to determine user identity: %s" % e)
238
241
 
239
242
  def setConfig(self, config_file):
240
243
  if not config_file:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deriva
3
- Version: 1.7.1
3
+ Version: 1.7.3
4
4
  Summary: Python APIs and CLIs (Command-Line Interfaces) for the DERIVA platform.
5
5
  Home-page: https://github.com/informatics-isi-edu/deriva-py
6
6
  Author: USC Information Sciences Institute, Informatics Systems Research Division
@@ -1,23 +1,23 @@
1
1
  deriva/__init__.py,sha256=h-QyvMVzDNpT3jyVskcSbUVFXxGCRxieFPrvTveZG9k,64
2
2
  deriva/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  deriva/config/acl_config.py,sha256=yZ0g_Cwv1MZXajUP2sVbc928XakUDu5Q97w9aK3VCCE,23946
4
- deriva/config/annotation_config.py,sha256=DQHPn4bKVYUxWPKyHkPfwLbdtOuBWpIyW_Uln-p10K0,11732
4
+ deriva/config/annotation_config.py,sha256=sWcJnRfS5vcb85IdUOd420cmFHRyb4br5OxLCOssbec,11714
5
5
  deriva/config/annotation_validate.py,sha256=ZN0jq6As49hfsAmWONUxAStqFG9TeYzvCmRU9TQ-zD8,5228
6
6
  deriva/config/base_config.py,sha256=Y5sFotuAWWyo-LZvzbzVY2ZDaC-zhwbMXusgBF1OJYM,20724
7
7
  deriva/config/dump_catalog_annotations.py,sha256=QzaWDLfWIAQ0eWVV11zeceWgwDBOYIePJnDQgRydwTE,9362
8
8
  deriva/config/rollback_annotation.py,sha256=EMVpuaMh2VaXXoHhsr3ldc7g7e92ngszEswdGNEmGFQ,3020
9
9
  deriva/config/examples/group_owner_policy.json,sha256=8v3GWM1F_BWnYD9x_f6Eo4kBDvyy8g7mRqujfoEKLNc,2408
10
10
  deriva/config/examples/self_serve_policy.json,sha256=pW-cqWz4rJNNXwY4eVZFkQ8gKCHclC9yDa22ylfcDqY,1676
11
- deriva/core/__init__.py,sha256=e2yjmArOhvRrFnmJxgjd_BPYIhiYsTVeYXe3iVQIKaU,4945
11
+ deriva/core/__init__.py,sha256=ZjDR3suQoZV7qrKaA7JdSLjopu6XDHXRC7_yfM6rgmE,4945
12
12
  deriva/core/annotation.py,sha256=PkAkPkxX1brQsb8_drR1Qj5QjQA5mjkpXhkq9NuZ1g8,13432
13
13
  deriva/core/base_cli.py,sha256=EkLXOTeaFWUbPaYV-eLuLGga1PbkFVWi3Jjo-e_Vb-U,2681
14
14
  deriva/core/catalog_cli.py,sha256=-6Bo6GLWFWap7y3VxkzPs73HAe_XzRXIJMW-Ri84m3M,23273
15
- deriva/core/datapath.py,sha256=4Q3snZ-rBqQV5x7ZAfU7fWdVcmQYu-8Ma6a7DcOL6zQ,81306
15
+ deriva/core/datapath.py,sha256=HmmV3dZtEe9lHr2b9dYyyVdvzZwy6p07ZUF9ocbFvo0,89248
16
16
  deriva/core/deriva_binding.py,sha256=_sA9HGrcVRqT-OhrneMDMOquyVOFOxLq3WzBQhasLIM,12970
17
17
  deriva/core/deriva_server.py,sha256=nsW3gwg1sIaHl3BTf-nL41AkSj3dEpcEBlatvjvN8CQ,200
18
- deriva/core/ermrest_catalog.py,sha256=B8XdzDScxad4PVUxRxT3GLUkV5vSsNnvMA5a76cUdsc,50817
19
- deriva/core/ermrest_model.py,sha256=opQunJOl4Vm62U5LBbO8w77dz9l11B0DvGDU6Sfe2FQ,85916
20
- deriva/core/hatrac_cli.py,sha256=l9QmneLRHSMiG_z9S83ea0QGVhTS3Wq1KGPEKEDpecM,14522
18
+ deriva/core/ermrest_catalog.py,sha256=_eqQg16i1aA95R99B7tLZxHWQlYk-rLpN_0zghfNWRc,54991
19
+ deriva/core/ermrest_model.py,sha256=pf8HC_ecm2NiMF0rg6UjbAuTR4ZKMhf5o5JmMYYuV8I,101855
20
+ deriva/core/hatrac_cli.py,sha256=J8Vg2BPMqNrRC3egr0IUml-_Eo3UxuhCvk_dInjbCiw,14554
21
21
  deriva/core/hatrac_store.py,sha256=t7gEsZ3SJqt1RefSGc3728PQoVs9E5UWEBNGGbbG6Bw,22582
22
22
  deriva/core/polling_ermrest_catalog.py,sha256=KsjiFqPQaHWnJZCVF5i77sdzfubqZHgMBbQ1p8V8D3s,10351
23
23
  deriva/core/schemas/app_links.schema.json,sha256=AxrkC2scxomM6N7jyjtdYA73BbZzPrmuqU8PYWe7okI,954
@@ -44,7 +44,7 @@ deriva/core/schemas/visible_foreign_keys.schema.json,sha256=K-oa2qzj5EbmJCEyN6mN
44
44
  deriva/core/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  deriva/core/utils/__init__.py,sha256=XSbGaWe44hebxYvoh5huFzZkMY6TSKPOCRSjUOvaY70,124
46
46
  deriva/core/utils/core_utils.py,sha256=5xUjJ-1QrPFKOiwfSWFDdH8Hm5a-AEhzCvy2GJPjx4A,19500
47
- deriva/core/utils/globus_auth_utils.py,sha256=M7u6rN1IeQDb3RRN6bM3YIJMitPEP3Usugp-cLgsAr8,55601
47
+ deriva/core/utils/globus_auth_utils.py,sha256=huPzKSr0d2NirpLzyp6CSyR1gIAdlRuP-X0jRo5W_p8,55732
48
48
  deriva/core/utils/hash_utils.py,sha256=JqUYVB3jXusCQYX9fkKmweUKBC0WQi8ZI2N8m-uKygQ,2299
49
49
  deriva/core/utils/mime_utils.py,sha256=ZT7pMjY2kQWBsgsGC3jY6kjfygdsIyiYW3BKNw_pPyg,1128
50
50
  deriva/core/utils/version_utils.py,sha256=HWNUQAZrPXu0oGjhG6gMNm3kWtfa3nR50vfH2uQxBA0,2954
@@ -85,7 +85,7 @@ deriva/transfer/restore/deriva_restore.py,sha256=s0h7cXit2USSdjrIfrj0dr7BJ0rrHHM
85
85
  deriva/transfer/restore/deriva_restore_cli.py,sha256=2ViZ1Lyl5ndXPKeJFCHHGnwzkg3DfHhTuRa_bN7eJm8,5603
86
86
  deriva/transfer/upload/__init__.py,sha256=4mlc_iUX-v7SpXzlCZmhxQtSiW5JeDGb2FX7bb1E6tY,304
87
87
  deriva/transfer/upload/__main__.py,sha256=hqnXtGpRqPthwpO6uvrnf_TQm7McheeyOt960hStSMY,340
88
- deriva/transfer/upload/deriva_upload.py,sha256=9NJbsPx1FANgGrCr5AP78AjtM41LJsanCJKEW4UH9ws,60376
88
+ deriva/transfer/upload/deriva_upload.py,sha256=GuQb_-PBKR0JstEifXqSOZpF1zTyaPLfM0PCDlu2Qw8,60500
89
89
  deriva/transfer/upload/deriva_upload_cli.py,sha256=-Q6xgiYabQziTQcMQdGNDAv-eLxCCHO-BCSo4umbDE4,5082
90
90
  deriva/transfer/upload/processors/__init__.py,sha256=sMM5xdJ82UIRdB1lGMKk7ft0BgtjS2oJ0sI4SQSqiIU,2481
91
91
  deriva/transfer/upload/processors/archive_processor.py,sha256=ID0lDwDn4vPe5nbxy6m28Ssj_TsZpK4df2xRrM6nJRQ,2015
@@ -98,9 +98,9 @@ deriva/utils/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
98
98
  tests/deriva/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
99
  tests/deriva/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
100
  tests/deriva/core/test_datapath.py,sha256=hC5PqyL9zqNOV4ydY5L4pHdt8r7Or7OgZnX-F52P2nU,37308
101
- deriva-1.7.1.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
102
- deriva-1.7.1.dist-info/METADATA,sha256=WAYjUe8xgfn45OweE56aX8q67MuxaBcdxdkrSRfZfO4,1623
103
- deriva-1.7.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
104
- deriva-1.7.1.dist-info/entry_points.txt,sha256=72BEmEE4Bes5QhVxUHrl7EvUARrgISWxI2KGa8BbNZ8,786
105
- deriva-1.7.1.dist-info/top_level.txt,sha256=_LHDie5-O53wFlexfrxjewpVkf04oydf3CqX5h75DXE,13
106
- deriva-1.7.1.dist-info/RECORD,,
101
+ deriva-1.7.3.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
102
+ deriva-1.7.3.dist-info/METADATA,sha256=u8fofL_SdxidGBInnPPmjbb5qDgZWN6iBFzFwGvsJT0,1623
103
+ deriva-1.7.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
104
+ deriva-1.7.3.dist-info/entry_points.txt,sha256=72BEmEE4Bes5QhVxUHrl7EvUARrgISWxI2KGa8BbNZ8,786
105
+ deriva-1.7.3.dist-info/top_level.txt,sha256=_LHDie5-O53wFlexfrxjewpVkf04oydf3CqX5h75DXE,13
106
+ deriva-1.7.3.dist-info/RECORD,,
File without changes