deriva 1.7.7__py3-none-any.whl → 1.7.9__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.
@@ -291,7 +291,8 @@ class AclConfig:
291
291
  elif group.startswith(self.ROBOT_PREFIX_FORMAT.format(server=self.server)):
292
292
  self.validate_webauthn_robot(group)
293
293
  else:
294
- warnings.warn("Can't determine format of group '{g}'".format(g=group))
294
+ if self.verbose:
295
+ warnings.warn("Can't determine format of group '{g}'".format(g=group))
295
296
 
296
297
  def validate_globus_group(self, group):
297
298
  guid = group[len(self.GLOBUS_PREFIX):]
deriva/core/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.7.7"
1
+ __version__ = "1.7.9"
2
2
 
3
3
  from deriva.core.utils.core_utils import *
4
4
  from deriva.core.base_cli import BaseCLI, KeyValuePairArgs
@@ -55,7 +55,7 @@ def get_credential(host,
55
55
  scope_map = globus_client.hosts_to_scope_map(hosts=[host], match_scope_tag=match_scope_tag,
56
56
  force_refresh=force_scope_lookup,
57
57
  warn_on_discovery_failure=True if not creds else False)
58
- tokens = globus_client.is_logged_in(exclude_defaults=True)
58
+ tokens = globus_client.is_logged_in(exclude_defaults=True, hosts_to_scope_map=scope_map)
59
59
  if tokens:
60
60
  # 1. look for the explicitly requested scope in the token store, if specified
61
61
  token = globus_client.find_access_token_for_scope(requested_scope, tokens)
@@ -6,7 +6,7 @@ import traceback
6
6
  from pprint import pp
7
7
  from requests.exceptions import HTTPError, ConnectionError
8
8
  from deriva.core import __version__ as VERSION, BaseCLI, KeyValuePairArgs, DerivaServer, DerivaPathError, \
9
- get_credential, format_credential, format_exception, DEFAULT_HEADERS
9
+ get_credential, format_credential, format_exception, read_config, DEFAULT_SESSION_CONFIG, DEFAULT_HEADERS
10
10
  from deriva.core.ermrest_model import nochange
11
11
  from deriva.core.utils import eprint
12
12
 
@@ -191,6 +191,11 @@ class DerivaCatalogCLI (BaseCLI):
191
191
  else:
192
192
  return get_credential(host_name)
193
193
 
194
+ @staticmethod
195
+ def _get_session_config():
196
+ config = read_config()
197
+ return config.get("session", DEFAULT_SESSION_CONFIG)
198
+
194
199
  def _post_parser_init(self, args):
195
200
  """Shared initialization for all sub-commands.
196
201
  """
@@ -202,7 +207,8 @@ class DerivaCatalogCLI (BaseCLI):
202
207
  credentials=DerivaCatalogCLI._get_credential(
203
208
  self.host,
204
209
  token=args.token,
205
- oauth2_token=args.oauth2_token))
210
+ oauth2_token=args.oauth2_token),
211
+ session_config=DerivaCatalogCLI._get_session_config())
206
212
 
207
213
  @staticmethod
208
214
  def _decorate_headers(headers, file_format, method="get"):
@@ -279,10 +285,10 @@ class DerivaCatalogCLI (BaseCLI):
279
285
  catalog = self.server.connect_ermrest(args.id)
280
286
  try:
281
287
  if args.output_file:
282
- catalog.getAsFile(args.path,
283
- destfilename=args.output_file,
284
- headers=headers,
285
- delete_if_empty=args.auto_delete)
288
+ catalog.get_as_file(args.path,
289
+ destfilename=args.output_file,
290
+ headers=headers,
291
+ delete_if_empty=args.auto_delete)
286
292
  else:
287
293
  pp(catalog.get(args.path, headers=headers).json())
288
294
  except HTTPError as e:
@@ -139,9 +139,10 @@ class DerivaBinding (object):
139
139
  server
140
140
  )
141
141
  self._server_uri = self._base_server_uri
142
+ self._auth_uri = self._base_server_uri + "/authn/session"
142
143
 
143
- self.session_config = DEFAULT_SESSION_CONFIG if not session_config else session_config
144
144
  self._session = None
145
+ self.session_config = DEFAULT_SESSION_CONFIG if not session_config else session_config
145
146
  self._get_new_session(self.session_config)
146
147
 
147
148
  self._caching = caching
@@ -160,9 +161,6 @@ class DerivaBinding (object):
160
161
  self._close_session()
161
162
  self._session = get_new_requests_session(self._server_uri + '/',
162
163
  session_config if session_config else self.session_config)
163
- # allow loopback requests to bypass SSL cert verification
164
- if "https://localhost" in self._server_uri:
165
- self._session.verify = False
166
164
 
167
165
  def _pre_get(self, path, headers):
168
166
  self.check_path(path)
@@ -222,19 +220,21 @@ class DerivaBinding (object):
222
220
  self._session.headers.update({'Authorization': 'Bearer {token}'.format(token=credentials['bearer-token'])})
223
221
  elif 'cookie' in credentials:
224
222
  cname, cval = credentials['cookie'].split('=', 1)
225
- self._session.cookies.set(cname, cval, domain=server, path='/')
223
+ # Fix for cookielib domain rewrite to *.local when no "." in hostname. In this case, don't set the domain.
224
+ # Covers "localhost" and other dev/test scenarios. See "https://github.com/psf/requests/issues/5388"
225
+ self._session.cookies.set(cname, cval, domain="" if "." not in server else server, path='/')
226
226
  elif 'username' in credentials and 'password' in credentials:
227
227
  self.post_authn_session(credentials)
228
228
 
229
229
  def get_authn_session(self):
230
230
  headers = { 'deriva-client-context': self.dcctx.encoded() }
231
- r = self._session.get(self._base_server_uri + "/authn/session", headers=headers)
231
+ r = self._session.get(self._auth_uri, headers=headers)
232
232
  _response_raise_for_status(r)
233
233
  return r
234
234
 
235
235
  def post_authn_session(self, credentials):
236
236
  headers = { 'deriva-client-context': self.dcctx.encoded() }
237
- r = self._session.post(self._base_server_uri + "/authn/session", data=credentials, headers=headers)
237
+ r = self._session.post(self._auth_uri, data=credentials, headers=headers)
238
238
  _response_raise_for_status(r)
239
239
  return r
240
240
 
@@ -481,12 +481,26 @@ class ErmrestCatalog(DerivaBinding):
481
481
  def getAsFile(self,
482
482
  path,
483
483
  destfilename,
484
- headers=DEFAULT_HEADERS,
485
- callback=None,
486
- delete_if_empty=False,
487
- paged=False,
488
- page_size=DEFAULT_PAGE_SIZE,
489
- page_sort_columns=frozenset(["RID"])):
484
+ headers = DEFAULT_HEADERS,
485
+ callback = None,
486
+ delete_if_empty = False,
487
+ paged = False,
488
+ page_size = DEFAULT_PAGE_SIZE,
489
+ page_sort_columns = frozenset(["RID"])):
490
+ """
491
+ Deprecated, call `get_as_file` instead.
492
+ """
493
+ self.get_as_file(path, destfilename, headers, callback, delete_if_empty, paged, page_size, page_sort_columns)
494
+
495
+ def get_as_file(self,
496
+ path,
497
+ destfilename,
498
+ headers=DEFAULT_HEADERS,
499
+ callback=None,
500
+ delete_if_empty=False,
501
+ paged=False,
502
+ page_size=DEFAULT_PAGE_SIZE,
503
+ page_sort_columns=frozenset(["RID"])):
490
504
  """
491
505
  Retrieve catalog data streamed to destination file.
492
506
  Caller is responsible to clean up file even on error, when the file may or may not exist.
@@ -188,7 +188,7 @@ def equivalent(doc1, doc2, method=None):
188
188
  def canon_cat_acls(d):
189
189
  return {
190
190
  k: d.get(k, [])
191
- for k in {'owner', 'read', 'write', 'insert', 'update', 'delete'}
191
+ for k in {'owner', 'enumerate', 'write', 'select', 'insert', 'update', 'delete'}
192
192
  }
193
193
  return equivalent(canon_cat_acls(doc1), canon_cat_acls(doc2), method='acls')
194
194
  elif method == 'foreign_key_acls':
@@ -48,7 +48,8 @@ DEFAULT_SESSION_CONFIG = {
48
48
  "allow_retry_on_all_methods": False,
49
49
  "cookie_jar": DEFAULT_COOKIE_JAR_FILE,
50
50
  "max_request_size": DEFAULT_MAX_REQUEST_SIZE,
51
- "max_chunk_limit": DEFAULT_MAX_CHUNK_LIMIT
51
+ "max_chunk_limit": DEFAULT_MAX_CHUNK_LIMIT,
52
+ "bypass_cert_verify_host_list": []
52
53
  }
53
54
  OAUTH2_SCOPES_KEY = "oauth2_scopes"
54
55
  DEFAULT_CONFIG = {
@@ -189,11 +190,15 @@ def get_new_requests_session(url=None, session_config=DEFAULT_SESSION_CONFIG):
189
190
  not session_config.get("allow_retry_on_all_methods", False) else False,
190
191
  raise_on_status=True)
191
192
  adapter = TimeoutHTTPAdapter(timeout=session_config.get("timeout", DEFAULT_REQUESTS_TIMEOUT), max_retries=retries)
193
+ session.mount('http://', adapter)
194
+ session.mount('https://', adapter)
195
+
192
196
  if url:
193
- session.mount(url, adapter)
194
- else:
195
- session.mount('http://', adapter)
196
- session.mount('https://', adapter)
197
+ # allow whitelisted hosts to bypass SSL cert verification
198
+ upr = urlparse(url)
199
+ bypass_cert_verify_host_list = session_config.get("bypass_cert_verify_host_list", [])
200
+ if upr.scheme == "https" and upr.hostname in bypass_cert_verify_host_list:
201
+ session.verify = False
197
202
 
198
203
  return session
199
204
 
@@ -301,7 +306,7 @@ def get_oauth_scopes_for_host(host,
301
306
  result = scopes
302
307
  break
303
308
  if not result or force_refresh:
304
- session = get_new_requests_session(session_config=DEFAULT_SESSION_CONFIG)
309
+ session = get_new_requests_session(url, session_config=config.get("session", DEFAULT_SESSION_CONFIG))
305
310
  try:
306
311
  r = session.get(url, headers=DEFAULT_HEADERS)
307
312
  r.raise_for_status()
@@ -5,7 +5,7 @@ import json
5
5
  import logging
6
6
  import platform
7
7
  import traceback
8
- import importlib
8
+ import time
9
9
  import datetime
10
10
  import tzlocal
11
11
  import webbrowser
@@ -15,7 +15,8 @@ from bdbag.fetch.auth import keychain as bdbkc
15
15
  from deriva.core import __version__ as VERSION, DEFAULT_CONFIG_PATH, DEFAULT_GLOBUS_CREDENTIAL_FILE, urlparse, urljoin,\
16
16
  read_config, format_exception, BaseCLI, get_oauth_scopes_for_host, get_new_requests_session
17
17
  from deriva.core.utils import eprint
18
- from globus_sdk import ConfidentialAppAuthClient, AuthClient, AccessTokenAuthorizer, GlobusError, GlobusAPIError
18
+ from globus_sdk import ConfidentialAppAuthClient, AuthClient, AccessTokenAuthorizer, RefreshTokenAuthorizer, \
19
+ GlobusError, GlobusAPIError, AuthAPIError
19
20
  from fair_research_login.client import NativeClient, LoadError, NoSavedTokens, TokensExpired
20
21
 
21
22
  NATIVE_APP_CLIENT_ID = "8ef15ba9-2b4a-469c-a163-7fd910c9d111"
@@ -586,12 +587,47 @@ class GlobusNativeLogin:
586
587
  if not scopes:
587
588
  return None
588
589
  try:
589
- return self.client.load_tokens(scopes)
590
+ tokens = self.client.load_tokens(list(scopes))
591
+ return self.refresh_tokens_preemptively(tokens, time_before_expiry=86400)
590
592
  except LoadError as e:
591
593
  logging.debug("Unable to load or find tokens for specified scopes [%s]: %s" %
592
594
  (scopes, format_exception(e)))
593
595
  return None
594
596
 
597
+ def refresh_tokens_preemptively(self, tokens, time_before_expiry=0):
598
+ needs_refresh = {}
599
+
600
+ for rs, ts in {t: ts for t, ts in tokens.items() if bool(ts['refresh_token'])}.items():
601
+ expiry_threshold = int(time.time()) + time_before_expiry
602
+ if ts['expires_at_seconds'] < expiry_threshold:
603
+ needs_refresh.update({rs:ts})
604
+
605
+ if not needs_refresh:
606
+ return tokens
607
+
608
+ for rs, ts in needs_refresh.items():
609
+ authorizer = RefreshTokenAuthorizer(
610
+ ts['refresh_token'],
611
+ self.client.client,
612
+ access_token=ts['access_token'],
613
+ expires_at=int(time.time()),
614
+ # setting the expires_at to the current time is vicious hack but necessary since the globus_sdk
615
+ # client doesn't allow you to refresh an access token unless it has already expired
616
+ )
617
+ try:
618
+ authorizer.ensure_valid_token()
619
+ ts['access_token'] = authorizer.access_token
620
+ ts['expires_at_seconds'] = authorizer.expires_at
621
+ except AuthAPIError as e:
622
+ if e.message == 'invalid_grant':
623
+ logging.warning('Refresh Token expired for resource server: %s', rs)
624
+
625
+ self.client.save_tokens(needs_refresh)
626
+ result = {rs: ts for rs, ts in tokens.items() if rs not in needs_refresh}
627
+ result.update(needs_refresh)
628
+
629
+ return result
630
+
595
631
  def hosts_to_scope_map(self,
596
632
  hosts,
597
633
  match_scope_tag=None,
@@ -662,6 +698,7 @@ class GlobusNativeLogin:
662
698
  access_token = token.get("access_token")
663
699
  if (token_scope == scope) and (access_token is not None):
664
700
  return access_token
701
+ return None
665
702
 
666
703
  def update_bdbag_keychain(self, token=None, host=None, keychain_file=None, allow_redirects=False, delete=False):
667
704
  if (token is None) or (host is None):
@@ -699,7 +736,7 @@ class GlobusNativeLogin:
699
736
  prefill_named_grant = self.client.app_name + " with requested scopes [%s] " % ", ".join(scopes)
700
737
  tokens = self.client.login(no_local_server=no_local_server,
701
738
  no_browser=no_browser,
702
- requested_scopes=scopes,
739
+ requested_scopes=list(scopes),
703
740
  refresh_tokens=refresh_tokens,
704
741
  prefill_named_grant=prefill_named_grant,
705
742
  query_params=additional_params,
@@ -4,8 +4,8 @@ from deriva.transfer.download.deriva_download import DerivaDownload, GenericDown
4
4
  from deriva.transfer.download.deriva_download_cli import DerivaDownloadCLI
5
5
  from deriva.transfer.download.deriva_export import DerivaExport, DerivaExportCLI
6
6
 
7
- from deriva.transfer.upload.deriva_upload import DerivaUpload, GenericUploader, DerivaUploadError, DerivaUploadError, \
8
- DerivaUploadConfigurationError, DerivaUploadCatalogCreateError, DerivaUploadCatalogUpdateError, \
7
+ from deriva.transfer.upload.deriva_upload import DerivaUpload, GenericUploader, UploadState, DerivaUploadError, \
8
+ DerivaUploadError, DerivaUploadConfigurationError, DerivaUploadCatalogCreateError, DerivaUploadCatalogUpdateError, \
9
9
  DerivaUploadAuthenticationError
10
10
  from deriva.transfer.upload.deriva_upload_cli import DerivaUploadCLI
11
11
 
@@ -5,7 +5,6 @@ import traceback
5
5
  import requests
6
6
  import argparse
7
7
  import logging
8
- import certifi
9
8
  import datetime
10
9
  from collections.abc import Mapping, Iterable
11
10
  from requests.exceptions import HTTPError, ConnectionError, Timeout
@@ -46,6 +45,7 @@ class DerivaExport:
46
45
  self.timeout = kwargs.get("timeout")
47
46
  self.export_type = kwargs.get("export_type", "bdbag")
48
47
  self.base_server_uri = "https://" + self.host
48
+ self.auth_service_url = self.base_server_uri + "/authn/session"
49
49
  self.service_url = self.base_server_uri + EXPORT_SERVICE_PATH % self.export_type
50
50
  self.session_config = DEFAULT_SESSION_CONFIG.copy()
51
51
  if isinstance(self.timeout, tuple):
@@ -78,16 +78,23 @@ class DerivaExport:
78
78
  raise DerivaDownloadAuthenticationError(
79
79
  "The requested service requires authentication and a valid login credential could "
80
80
  "not be found (or was not provided) for the specified host.")
81
+
81
82
  if 'bearer-token' in self.credential:
82
83
  self.session.headers.update(
83
84
  {'Authorization': 'Bearer {token}'.format(token=self.credential['bearer-token'])})
84
85
  elif 'cookie' in self.credential:
85
86
  cname, cval = self.credential['cookie'].split('=', 1)
86
- self.session.cookies.set(cname, cval, domain=self.host, path='/')
87
+ self.session.cookies.set(cname, cval, domain="" if "." not in self.host else self.host, path='/')
88
+ else:
89
+ try:
90
+ r = self.session.post(self.auth_service_url, data=self.credential)
91
+ r.raise_for_status()
92
+ except HTTPError as e:
93
+ raise DerivaDownloadAuthenticationError(
94
+ "Exception during POST authentication flow: %s" % format_exception(e))
87
95
 
88
96
  def validate_authn_session(self):
89
- url = self.base_server_uri + "/authn/session"
90
- r = self.session.get(url)
97
+ r = self.session.get(self.auth_service_url)
91
98
  if r.status_code == requests.codes.not_found or r.status_code == requests.codes.unauthorized:
92
99
  logger.warning("Unable to authenticate. Check for missing or expired credentials.")
93
100
  r.raise_for_status()
@@ -125,7 +132,7 @@ class DerivaExport:
125
132
 
126
133
  filename = parse_content_disposition(content_disposition)
127
134
  output_path = os.path.abspath(os.path.join(self.output_dir, filename))
128
- with self.session.get(url, stream=True, verify=certifi.where()) as r:
135
+ with self.session.get(url, stream=True) as r:
129
136
  if r.status_code != 200:
130
137
  file_error = "File [%s] transfer failed." % output_path
131
138
  url_error = 'HTTP GET Failed for url: %s' % url
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  import errno
3
- import certifi
4
3
  import requests
5
4
  from deriva.core import urlsplit, get_new_requests_session, stob, make_dirs, format_exception, DEFAULT_SESSION_CONFIG
6
5
  from deriva.transfer.download import DerivaDownloadError, DerivaDownloadConfigurationError, \
@@ -72,13 +71,13 @@ class BaseQueryProcessor(BaseProcessor):
72
71
  make_dirs(output_dir)
73
72
  try:
74
73
  if as_file:
75
- return self.catalog.getAsFile(self.query, self.output_abspath,
76
- headers=headers,
77
- callback=self.callback,
78
- delete_if_empty=True,
79
- paged=self.paged_query,
80
- page_size=self.paged_query_size,
81
- page_sort_columns=self.paged_query_sort_columns)
74
+ return self.catalog.get_as_file(self.query, self.output_abspath,
75
+ headers=headers,
76
+ callback=self.callback,
77
+ delete_if_empty=True,
78
+ paged=self.paged_query,
79
+ page_size=self.paged_query_size,
80
+ page_sort_columns=self.paged_query_sort_columns)
82
81
  else:
83
82
  return self.catalog.get(self.query, headers=headers).json()
84
83
  except requests.HTTPError as e:
@@ -143,31 +142,31 @@ class BaseQueryProcessor(BaseProcessor):
143
142
 
144
143
  return url
145
144
 
146
- def getExternalSession(self, host):
147
- sessions = self.sessions
145
+ def getExternalSession(self, url):
148
146
  auth_params = self.kwargs.get("auth_params", dict())
149
147
  cookies = auth_params.get("cookies")
150
148
  auth_url = auth_params.get("auth_url")
151
149
  login_params = auth_params.get("login_params")
152
150
  session_config = self.kwargs.get("session_config")
153
151
 
154
- session = sessions.get(host)
152
+ host = urlsplit(url).hostname
153
+ session = self.sessions.get(host)
155
154
  if session is not None:
156
155
  return session
157
156
 
158
157
  if not session_config:
159
158
  session_config = DEFAULT_SESSION_CONFIG
160
- session = get_new_requests_session(session_config=session_config)
159
+ session = get_new_requests_session(url, session_config=session_config)
161
160
 
162
161
  if cookies:
163
162
  session.cookies.update(cookies)
164
163
  if login_params and auth_url:
165
- r = session.post(auth_url, data=login_params, verify=certifi.where())
164
+ r = session.post(auth_url, data=login_params)
166
165
  if r.status_code > 203:
167
166
  raise DerivaDownloadError(
168
167
  'GetExternalSession Failed with Status Code: %s\n%s\n' % (r.status_code, r.text))
169
168
 
170
- sessions[host] = session
169
+ self.sessions[host] = session
171
170
  return session
172
171
 
173
172
  def create_default_paths(self):
@@ -4,7 +4,6 @@ import uuid
4
4
  import datetime
5
5
  import logging
6
6
  import requests
7
- import certifi
8
7
  from bdbag import bdbag_ro as ro
9
8
  from deriva.core import urlsplit, format_exception, get_transfer_summary, make_dirs, DEFAULT_CHUNK_SIZE
10
9
  from deriva.core.utils.mime_utils import parse_content_disposition
@@ -32,14 +31,13 @@ class FileDownloadQueryProcessor(BaseQueryProcessor):
32
31
  return self.outputs
33
32
 
34
33
  def getExternalFile(self, url, output_path, headers=None):
35
- host = urlsplit(url).netloc
36
34
  if output_path:
37
35
  if not headers:
38
36
  headers = self.HEADERS.copy()
39
37
  else:
40
38
  headers.update(self.HEADERS)
41
- session = self.getExternalSession(host)
42
- with session.get(url, headers=headers, stream=True, verify=certifi.where()) as r:
39
+ session = self.getExternalSession(url)
40
+ with session.get(url, headers=headers, stream=True) as r:
43
41
  if r.status_code != 200:
44
42
  file_error = "File [%s] transfer failed." % output_path
45
43
  url_error = 'HTTP GET Failed for url: %s' % url
@@ -59,6 +57,7 @@ class FileDownloadQueryProcessor(BaseQueryProcessor):
59
57
  length = int(r.headers.get('Content-Length'))
60
58
  content_type = r.headers.get("Content-Type")
61
59
  return output_path, length, content_type
60
+ return None
62
61
 
63
62
  def downloadFiles(self, input_manifest):
64
63
  logging.info("Attempting to download file(s) based on the results of query: %s" % self.query)
@@ -157,8 +157,12 @@ class DerivaUpload(object):
157
157
 
158
158
  # determine identity
159
159
  if self.credentials:
160
- attributes = self.catalog.get_authn_session().json()
161
- self.identity = attributes.get("client", self.identity)
160
+ try:
161
+ attributes = self.catalog.get_authn_session().json()
162
+ self.identity = attributes.get("client", self.identity)
163
+ except Exception as e:
164
+ # not a big deal since the credential token being used could be expired
165
+ logger.debug("Unable to determine user identity from existing credential (may be expired): %s" % e)
162
166
 
163
167
  # init dcctx cid to a default
164
168
  self.set_dcctx_cid(self.dcctx_cid)
@@ -342,7 +346,7 @@ class DerivaUpload(object):
342
346
  return '%s:%s' % (urlquote(schema_name), urlquote(table_name))
343
347
 
344
348
  @staticmethod
345
- def interpolateDict(src, dst, allowNone=False):
349
+ def interpolateDict(src, dst, allow_none_column_list=[]):
346
350
  if not (isinstance(src, dict) and isinstance(dst, dict)):
347
351
  raise ValueError("Invalid input parameter type(s): (src = %s, dst = %s), expected (dict, dict)" % (
348
352
  type(src).__name__, type(dst).__name__))
@@ -356,26 +360,27 @@ class DerivaUpload(object):
356
360
  for k, v in dst.items():
357
361
  try:
358
362
  value = v.format(**src)
363
+ if k in src:
364
+ v_type = type(src[k])
365
+ value = v_type(value)
359
366
  except KeyError:
360
367
  value = v
361
368
  if value:
362
369
  if value.startswith('{') and value.endswith('}'):
363
370
  value = None
364
371
  dst.update({k: value})
365
- # remove all None valued entries in the dest, if disallowed
366
- if not allowNone:
367
- empty = [k for k, v in dst.items() if v is None]
368
- for k in empty:
369
- del dst[k]
372
+ # remove all None valued entries in the dest, if column is not explicitly allowed
373
+ dst = {k: v for k, v in dst.items() if v is not None or k in allow_none_column_list}
370
374
 
371
375
  return dst
372
376
 
373
377
  @staticmethod
374
- def pruneDict(src, dst, stringify=True):
378
+ def pruneDict(src, dst, allow_none_column_list=[]):
375
379
  dst = dst.copy()
376
380
  for k in dst.keys():
377
381
  value = src.get(k)
378
- dst[k] = str(value) if (stringify and value is not None) else value
382
+ if value is not None or (value is None and k in allow_none_column_list):
383
+ dst[k] = value
379
384
  return dst
380
385
 
381
386
  def getCurrentConfigFilePath(self):
@@ -712,7 +717,8 @@ class DerivaUpload(object):
712
717
  self.metadata["URI"] = versioned_uri
713
718
  else:
714
719
  self.metadata["URI"] = versioned_uri.rsplit(":")[0]
715
- self.metadata["URI_urlencoded"] = urlquote(self.metadata["URI"])
720
+ safe_overrides = asset_mapping.get("url_encoding_safe_overrides", {}).get("URI", "")
721
+ self.metadata["URI_urlencoded"] = urlquote(self.metadata["URI"], safe=safe_overrides)
716
722
 
717
723
  # 7. Check for an existing record and create a new one if necessary
718
724
  if not record:
@@ -720,7 +726,8 @@ class DerivaUpload(object):
720
726
 
721
727
  # 8. Update an existing record, if necessary
722
728
  column_map = asset_mapping.get("column_map", {})
723
- updated_record = self.interpolateDict(self.metadata, column_map)
729
+ allow_none_col_list = asset_mapping.get("allow_empty_columns_on_update", [])
730
+ updated_record = self.interpolateDict(self.metadata, column_map, allow_none_col_list)
724
731
  if updated_record != record:
725
732
  record_update_template = asset_mapping.get("record_update_template")
726
733
  require_record_update_template = stob(asset_mapping.get("require_record_update_template", False))
@@ -778,6 +785,7 @@ class DerivaUpload(object):
778
785
  """
779
786
  record = None
780
787
  column_map = asset_mapping.get("column_map", {})
788
+ allow_none_col_list = asset_mapping.get("allow_empty_columns_on_update", [])
781
789
  rqt = asset_mapping['record_query_template']
782
790
  try:
783
791
  path = rqt.format(**self.metadata)
@@ -787,14 +795,14 @@ class DerivaUpload(object):
787
795
  if result:
788
796
  record = result[0]
789
797
  self._updateFileMetadata(record, no_overwrite=True)
790
- return self.pruneDict(record, column_map), record
798
+ return self.pruneDict(record, column_map, allow_none_col_list), record
791
799
  else:
792
800
  row = self.interpolateDict(self.metadata, column_map)
793
801
  result = self._catalogRecordCreate(self.metadata['target_table'], row)
794
802
  if result:
795
803
  record = result[0]
796
804
  self._updateFileMetadata(record)
797
- return self.interpolateDict(self.metadata, column_map, allowNone=True), record
805
+ return self.interpolateDict(self.metadata, column_map, allow_none_column_list=allow_none_col_list), record
798
806
 
799
807
  def _urlEncodeMetadata(self, safe_overrides=None):
800
808
  urlencoded = dict()
@@ -886,12 +894,14 @@ class DerivaUpload(object):
886
894
  def _getFileHatracMetadata(self, asset_mapping):
887
895
  try:
888
896
  hatrac_templates = asset_mapping["hatrac_templates"]
897
+ # convert None values to empty strings for URI and content-disposition template replacement
898
+ metadata = {k: ('' if v is None else v) for k, v in self.metadata.items()}
889
899
  # URI is required
890
- self.metadata["URI"] = hatrac_templates["hatrac_uri"].format(**self.metadata)
900
+ self.metadata["URI"] = hatrac_templates["hatrac_uri"].format(**metadata)
891
901
  # overridden content-disposition is optional
892
902
  content_disposition = hatrac_templates.get("content-disposition")
893
903
  if content_disposition:
894
- filename = content_disposition.format(**self.metadata)
904
+ filename = content_disposition.format(**metadata)
895
905
  else:
896
906
  filename = urlparse(self.metadata["URI"]).path.rsplit("/", 1)[-1]
897
907
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: deriva
3
- Version: 1.7.7
3
+ Version: 1.7.9
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
@@ -25,14 +25,26 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: requests
28
- Requires-Dist: certifi
29
28
  Requires-Dist: pika
30
- Requires-Dist: urllib3 <3,>=1.26
31
- Requires-Dist: portalocker >=1.2.1
32
- Requires-Dist: bdbag >=1.7.3
33
- Requires-Dist: globus-sdk <4,>=3
34
- Requires-Dist: fair-research-login >=0.3.1
35
- Requires-Dist: fair-identifiers-client >=0.5.1
36
- Requires-Dist: jsonschema >=3.1
29
+ Requires-Dist: urllib3<3,>=1.26.20
30
+ Requires-Dist: portalocker>=1.2.1
31
+ Requires-Dist: bdbag>=1.7.5
32
+ Requires-Dist: globus_sdk<4,>=3
33
+ Requires-Dist: fair-research-login>=0.3.1
34
+ Requires-Dist: fair-identifiers-client>=0.5.1
35
+ Requires-Dist: jsonschema>=3.1
36
+ Dynamic: author
37
+ Dynamic: author-email
38
+ Dynamic: classifier
39
+ Dynamic: description
40
+ Dynamic: description-content-type
41
+ Dynamic: home-page
42
+ Dynamic: license
43
+ Dynamic: license-file
44
+ Dynamic: maintainer
45
+ Dynamic: maintainer-email
46
+ Dynamic: requires-dist
47
+ Dynamic: requires-python
48
+ Dynamic: summary
37
49
 
38
50
  For further information, visit the project [homepage](https://github.com/informatics-isi-edu/deriva-py).
@@ -1,6 +1,6 @@
1
1
  deriva/__init__.py,sha256=h-QyvMVzDNpT3jyVskcSbUVFXxGCRxieFPrvTveZG9k,64
2
2
  deriva/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- deriva/config/acl_config.py,sha256=yZ0g_Cwv1MZXajUP2sVbc928XakUDu5Q97w9aK3VCCE,23946
3
+ deriva/config/acl_config.py,sha256=JK6OKJ9OEaloY2vFZzD9leA3p4yeAtc9QEVhTYEipc0,23979
4
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
@@ -8,15 +8,15 @@ deriva/config/dump_catalog_annotations.py,sha256=QzaWDLfWIAQ0eWVV11zeceWgwDBOYIe
8
8
  deriva/config/rollback_annotation.py,sha256=vqrIcen-KZX8LDpu2OVNivzIHpQoQgWkZAChZJctvtk,3015
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=D7aya8r3BTbewaSUmhbb4DB-Ltwe2aXV10CMfPo13rI,4945
11
+ deriva/core/__init__.py,sha256=NKiBln9VluAuOX25MSfa-xDpYY86NwJY1civQ_Kg5QY,4975
12
12
  deriva/core/annotation.py,sha256=PkAkPkxX1brQsb8_drR1Qj5QjQA5mjkpXhkq9NuZ1g8,13432
13
13
  deriva/core/base_cli.py,sha256=78Ilf3_f2xREQb3IIj6q0jwWAiXSObZszG0JURs36lA,2902
14
- deriva/core/catalog_cli.py,sha256=-6Bo6GLWFWap7y3VxkzPs73HAe_XzRXIJMW-Ri84m3M,23273
14
+ deriva/core/catalog_cli.py,sha256=CwfTf7C81SpU1J_aPsWiIbPOBAyekkIh384KUivq5H8,23550
15
15
  deriva/core/datapath.py,sha256=w8LvPAd_DuknKxHc_YS6NUHhTY6XpCSkfa0m_xQUdZE,89068
16
- deriva/core/deriva_binding.py,sha256=_sA9HGrcVRqT-OhrneMDMOquyVOFOxLq3WzBQhasLIM,12970
16
+ deriva/core/deriva_binding.py,sha256=6yGXIbnayDtb9LU-JVK43zk02-aQwshrKiaLi1pru-8,13086
17
17
  deriva/core/deriva_server.py,sha256=nsW3gwg1sIaHl3BTf-nL41AkSj3dEpcEBlatvjvN8CQ,200
18
- deriva/core/ermrest_catalog.py,sha256=_eqQg16i1aA95R99B7tLZxHWQlYk-rLpN_0zghfNWRc,54991
19
- deriva/core/ermrest_model.py,sha256=NdvrMLkmNTY4ncodzojpyV1XjgSlod7x-SXyUu65Z0Q,124921
18
+ deriva/core/ermrest_catalog.py,sha256=2mShQgo35DIQ-0UOEqu2VgCDTE3sUrQSWVDm1jW_tQ8,55547
19
+ deriva/core/ermrest_model.py,sha256=oydtO6IU54ihjWQXcq0nfCyqAFJSW2FI9Z_0FjU7Uk0,124936
20
20
  deriva/core/hatrac_cli.py,sha256=l9QmneLRHSMiG_z9S83ea0QGVhTS3Wq1KGPEKEDpecM,14522
21
21
  deriva/core/hatrac_store.py,sha256=NcVuO4h4hswbEAct8tTKZ1pNtXBn74Nn9TelKN_jr8Q,22323
22
22
  deriva/core/mmo.py,sha256=dcB8akgsqbYMi22ClbVpOKVL6so8FjSDjSb6gP4_jFo,17852
@@ -44,8 +44,8 @@ deriva/core/schemas/visible_columns.schema.json,sha256=-JKqvhfKT-5btKBtzKn3p6EgY
44
44
  deriva/core/schemas/visible_foreign_keys.schema.json,sha256=K-oa2qzj5EbmJCEyN6mN3vubblHENFxxbZEeQfFV_QQ,3364
45
45
  deriva/core/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  deriva/core/utils/__init__.py,sha256=XSbGaWe44hebxYvoh5huFzZkMY6TSKPOCRSjUOvaY70,124
47
- deriva/core/utils/core_utils.py,sha256=5xUjJ-1QrPFKOiwfSWFDdH8Hm5a-AEhzCvy2GJPjx4A,19500
48
- deriva/core/utils/globus_auth_utils.py,sha256=huPzKSr0d2NirpLzyp6CSyR1gIAdlRuP-X0jRo5W_p8,55732
47
+ deriva/core/utils/core_utils.py,sha256=bMPVBG0W5-OAPcY7rXYlIfPAofmN1We6h1uyzXYnR9s,19821
48
+ deriva/core/utils/globus_auth_utils.py,sha256=x5Dh4PlAMIKTm4b8nKUeuCMSFn7NMgj_NqtmnS_FM0I,57366
49
49
  deriva/core/utils/hash_utils.py,sha256=JqUYVB3jXusCQYX9fkKmweUKBC0WQi8ZI2N8m-uKygQ,2299
50
50
  deriva/core/utils/mime_utils.py,sha256=ZT7pMjY2kQWBsgsGC3jY6kjfygdsIyiYW3BKNw_pPyg,1128
51
51
  deriva/core/utils/version_utils.py,sha256=HWNUQAZrPXu0oGjhG6gMNm3kWtfa3nR50vfH2uQxBA0,2954
@@ -53,7 +53,7 @@ deriva/core/utils/webauthn_utils.py,sha256=rD0HQZAjUKp4NfqHQG1FhH3x7uKog2et7w7LB
53
53
  deriva/seo/__init__.py,sha256=dYn48A7blbeYf40b4T3KVofrQK4u5K5MfxXWfIGloig,54
54
54
  deriva/seo/sitemap_builder.py,sha256=Ht_AbodEERDofIoCcd4kPlrl1pVW670WN5dT4cc05LQ,13948
55
55
  deriva/seo/sitemap_cli.py,sha256=miCqRfpSj5Dx5BfJGSd8Pi2e4OOQjotDzP_JubukhCM,2654
56
- deriva/transfer/__init__.py,sha256=3a01U6e68kBEnFUp0dRMFLRDj2p50iIJF9RZaG3TXVI,1253
56
+ deriva/transfer/__init__.py,sha256=VRxmElD5SIOGzrB1biUU8bP3_iXPMvJuau-9alU5oy0,1266
57
57
  deriva/transfer/backup/__init__.py,sha256=vxsZiDLMTJQPybXT89G-07GsUoLhnItTCbLdXcDSyeA,465
58
58
  deriva/transfer/backup/__main__.py,sha256=dT12--8C6sKGEtMhsYuy013ebXKpVnBJfhcQNlVtv6Y,361
59
59
  deriva/transfer/backup/deriva_backup.py,sha256=IO9Tmzx6jHfUCkP-41nSsAeOFLn9T-0HwQcpRLpM_zs,5228
@@ -62,7 +62,7 @@ deriva/transfer/download/__init__.py,sha256=Pr7Zud4AFsIWwopTxeC_pupslgCG_lzycO9w
62
62
  deriva/transfer/download/__main__.py,sha256=YUg7AZ07t_xaOgtfJnU_l1nkEHCCPR8sU5X-l1An6SY,363
63
63
  deriva/transfer/download/deriva_download.py,sha256=ulFrHHEDj3oJA2pAo7MGvGyDF9rA3l8yCUoK3FMvHEk,17100
64
64
  deriva/transfer/download/deriva_download_cli.py,sha256=wN8tyQDv1AIE_aDqjECbmkoEWN050vlEdJyteYbdgSs,3940
65
- deriva/transfer/download/deriva_export.py,sha256=R6f08eJSt1iBeDNNQhThN3W1l4UwE0xzy2DjXbqNskE,13085
65
+ deriva/transfer/download/deriva_export.py,sha256=zvFVUUmtQfx8cDa35YiQXjZzjWXW0tpNolDwEC9q0JU,13449
66
66
  deriva/transfer/download/processors/__init__.py,sha256=evLp36tZn-Z_AMshdfV3JJO8w1es5owsnRN0IFJUwIo,4507
67
67
  deriva/transfer/download/processors/base_processor.py,sha256=R6IIHSa_euv4X2Dyhd8fvQAiVYDGJTWMQtPoukHQn-Q,3837
68
68
  deriva/transfer/download/processors/postprocess/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -71,8 +71,8 @@ deriva/transfer/download/processors/postprocess/transfer_post_processor.py,sha25
71
71
  deriva/transfer/download/processors/postprocess/url_post_processor.py,sha256=s68iIYqQSZHtbv4y-fCG8pjhApAeMEG6hYcKx2Pvf5Y,2745
72
72
  deriva/transfer/download/processors/query/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
73
  deriva/transfer/download/processors/query/bag_fetch_query_processor.py,sha256=tiQtfuy01YgOFFD5b_sP7TGjMnt0Jqcg2gp1KNWqeLE,5645
74
- deriva/transfer/download/processors/query/base_query_processor.py,sha256=0tGxTJKCEqm7ecdOcfd8szA94muc2jbYwbqYvTGwAac,10465
75
- deriva/transfer/download/processors/query/file_download_query_processor.py,sha256=Hg1NbKsaGJh9cB86yIyL7Fm7ywSNVop837Dv8aFXUes,7257
74
+ deriva/transfer/download/processors/query/base_query_processor.py,sha256=LlABWacU_hCySg1dhBJnWeauay0JSGW_f2103D3nU8Y,10459
75
+ deriva/transfer/download/processors/query/file_download_query_processor.py,sha256=_oxaf0r6kxxS8UMc_D96YDeGGumlUICbNAzzbU5PTvw,7201
76
76
  deriva/transfer/download/processors/transform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  deriva/transfer/download/processors/transform/base_transform_processor.py,sha256=Ddw5gsNpDANeuLvUaF4utp8psaxOtAzlgXtOg8gb-Pc,4109
78
78
  deriva/transfer/download/processors/transform/column_transform_processor.py,sha256=X98XSwpGrw0xhSlQ4i7_EPMvqU0Zz5aZp9Dam2AgYRU,2627
@@ -87,7 +87,7 @@ deriva/transfer/restore/deriva_restore.py,sha256=s0h7cXit2USSdjrIfrj0dr7BJ0rrHHM
87
87
  deriva/transfer/restore/deriva_restore_cli.py,sha256=2ViZ1Lyl5ndXPKeJFCHHGnwzkg3DfHhTuRa_bN7eJm8,5603
88
88
  deriva/transfer/upload/__init__.py,sha256=4mlc_iUX-v7SpXzlCZmhxQtSiW5JeDGb2FX7bb1E6tY,304
89
89
  deriva/transfer/upload/__main__.py,sha256=hqnXtGpRqPthwpO6uvrnf_TQm7McheeyOt960hStSMY,340
90
- deriva/transfer/upload/deriva_upload.py,sha256=_kndWxoAHb9vKJERCC9tjExp2EWISrOzhBXnSEbFIKI,60766
90
+ deriva/transfer/upload/deriva_upload.py,sha256=CFWhvsMl9Ndw6QJYvQoOsErwQuF1wOpgqGju8G0QgTQ,61708
91
91
  deriva/transfer/upload/deriva_upload_cli.py,sha256=-Q6xgiYabQziTQcMQdGNDAv-eLxCCHO-BCSo4umbDE4,5082
92
92
  deriva/transfer/upload/processors/__init__.py,sha256=sMM5xdJ82UIRdB1lGMKk7ft0BgtjS2oJ0sI4SQSqiIU,2481
93
93
  deriva/transfer/upload/processors/archive_processor.py,sha256=ID0lDwDn4vPe5nbxy6m28Ssj_TsZpK4df2xRrM6nJRQ,2015
@@ -97,6 +97,7 @@ deriva/transfer/upload/processors/metadata_update_processor.py,sha256=Hgu5huZf7Z
97
97
  deriva/transfer/upload/processors/rename_processor.py,sha256=UQ-JQuQgyYCGT-fU9kHA53kPdQ20kt-2Bb486od7B14,2423
98
98
  deriva/transfer/upload/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
99
  deriva/utils/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
100
+ deriva-1.7.9.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
100
101
  tests/deriva/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
102
  tests/deriva/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
103
  tests/deriva/core/test_datapath.py,sha256=0vUlQ48KYkDW5L7hzwyGAETa_40y0JmZDalk0W0fCq4,38722
@@ -108,9 +109,8 @@ tests/deriva/core/mmo/test_mmo_find.py,sha256=PcUN76sik68B3XKg0G3wHVpKcPEld_6Rtb
108
109
  tests/deriva/core/mmo/test_mmo_prune.py,sha256=4pYtYL8g1BgadlewNPVpVA5lT_gV6SPTDYf04ZKzBTA,6851
109
110
  tests/deriva/core/mmo/test_mmo_rename.py,sha256=4oSR1G3Od701Ss3AnolI1Z7CbMxKuQF2uSr2_IcoR6s,8512
110
111
  tests/deriva/core/mmo/test_mmo_replace.py,sha256=w-66LWyiQ_ajC7Ipmhc4kAKwIloPdQELeUPsvelTdX8,8439
111
- deriva-1.7.7.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
112
- deriva-1.7.7.dist-info/METADATA,sha256=V1dOlM-ESgLHfPMX_EOZ-kI6jvouKBIIzBcwwZ_bnnM,1623
113
- deriva-1.7.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
114
- deriva-1.7.7.dist-info/entry_points.txt,sha256=HmYCHlgbjYQ_aZX_j4_4tApH4tDTbYtS66jKlfytbn8,850
115
- deriva-1.7.7.dist-info/top_level.txt,sha256=_LHDie5-O53wFlexfrxjewpVkf04oydf3CqX5h75DXE,13
116
- deriva-1.7.7.dist-info/RECORD,,
112
+ deriva-1.7.9.dist-info/METADATA,sha256=QXs8Ca5GdL-Sw7SY41j0tYR5maXv16epbvoIoH6SnNU,1890
113
+ deriva-1.7.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
114
+ deriva-1.7.9.dist-info/entry_points.txt,sha256=HmYCHlgbjYQ_aZX_j4_4tApH4tDTbYtS66jKlfytbn8,850
115
+ deriva-1.7.9.dist-info/top_level.txt,sha256=_LHDie5-O53wFlexfrxjewpVkf04oydf3CqX5h75DXE,13
116
+ deriva-1.7.9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5