deriva 1.7.10__py3-none-any.whl → 1.7.12__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.
- deriva/core/__init__.py +1 -1
- deriva/core/catalog_cli.py +16 -18
- deriva/core/ermrest_catalog.py +1 -1
- deriva/core/ermrest_model.py +92 -1
- deriva/core/hatrac_cli.py +30 -2
- deriva/core/hatrac_store.py +88 -33
- deriva/core/utils/core_utils.py +312 -35
- deriva/core/utils/credenza_auth_utils.py +645 -0
- deriva/core/utils/globus_auth_utils.py +7 -13
- deriva/transfer/backup/deriva_backup.py +2 -2
- deriva/transfer/backup/deriva_backup_cli.py +5 -0
- deriva/transfer/upload/deriva_upload.py +18 -2
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/METADATA +4 -3
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/RECORD +18 -28
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/WHEEL +1 -1
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/entry_points.txt +1 -0
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/top_level.txt +0 -1
- tests/deriva/__init__.py +0 -0
- tests/deriva/core/__init__.py +0 -0
- tests/deriva/core/mmo/__init__.py +0 -0
- tests/deriva/core/mmo/base.py +0 -300
- tests/deriva/core/mmo/test_mmo_drop.py +0 -252
- tests/deriva/core/mmo/test_mmo_find.py +0 -90
- tests/deriva/core/mmo/test_mmo_prune.py +0 -196
- tests/deriva/core/mmo/test_mmo_rename.py +0 -222
- tests/deriva/core/mmo/test_mmo_replace.py +0 -180
- tests/deriva/core/test_datapath.py +0 -893
- tests/deriva/core/test_ermrest_model.py +0 -782
- {deriva-1.7.10.dist-info → deriva-1.7.12.dist-info}/licenses/LICENSE +0 -0
deriva/core/__init__.py
CHANGED
deriva/core/catalog_cli.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
+
import json
|
|
4
5
|
import requests
|
|
5
6
|
import traceback
|
|
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
9
|
get_credential, format_credential, format_exception, read_config, DEFAULT_SESSION_CONFIG, DEFAULT_HEADERS
|
|
@@ -12,7 +12,7 @@ from deriva.core.utils import eprint
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class DerivaCatalogCLIException (Exception):
|
|
15
|
-
"""Base exception class for
|
|
15
|
+
"""Base exception class for DerivaCatalogCli.
|
|
16
16
|
"""
|
|
17
17
|
def __init__(self, message):
|
|
18
18
|
"""Initializes the exception.
|
|
@@ -40,7 +40,7 @@ class ResourceException (DerivaCatalogCLIException):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class DerivaCatalogCLI (BaseCLI):
|
|
43
|
-
"""Deriva
|
|
43
|
+
"""Deriva Catalog Utility Command-line Interface.
|
|
44
44
|
"""
|
|
45
45
|
def __init__(self, description, epilog):
|
|
46
46
|
"""Initializes the CLI.
|
|
@@ -193,7 +193,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
193
193
|
|
|
194
194
|
@staticmethod
|
|
195
195
|
def _get_session_config():
|
|
196
|
-
config = read_config()
|
|
196
|
+
config = read_config(create_default=True)
|
|
197
197
|
return config.get("session", DEFAULT_SESSION_CONFIG)
|
|
198
198
|
|
|
199
199
|
def _post_parser_init(self, args):
|
|
@@ -233,7 +233,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
233
233
|
"""Implements the catalog_exists sub-command.
|
|
234
234
|
"""
|
|
235
235
|
catalog = self.server.connect_ermrest(self.id)
|
|
236
|
-
|
|
236
|
+
print(catalog.exists())
|
|
237
237
|
|
|
238
238
|
def catalog_create(self, args):
|
|
239
239
|
"""Implements the catalog_create sub-command.
|
|
@@ -248,8 +248,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
248
248
|
model = catalog.getCatalogModel()
|
|
249
249
|
model.configure_baseline_catalog(**args.configure_args)
|
|
250
250
|
if not args.quiet:
|
|
251
|
-
print(
|
|
252
|
-
pp(catalog.get('/').json())
|
|
251
|
+
print(json.dumps(catalog.get('/').json()))
|
|
253
252
|
except HTTPError as e:
|
|
254
253
|
if e.response.status_code == requests.codes.not_found:
|
|
255
254
|
raise ResourceException('Catalog not found', e)
|
|
@@ -290,15 +289,16 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
290
289
|
headers=headers,
|
|
291
290
|
delete_if_empty=args.auto_delete)
|
|
292
291
|
else:
|
|
293
|
-
|
|
292
|
+
print(json.dumps(catalog.get(args.path, headers=headers).json()))
|
|
294
293
|
except HTTPError as e:
|
|
295
294
|
if e.response.status_code == requests.codes.not_found:
|
|
296
295
|
raise ResourceException('Catalog not found', e)
|
|
297
|
-
|
|
296
|
+
else:
|
|
297
|
+
raise e
|
|
298
|
+
finally:
|
|
298
299
|
if args.output_file and os.path.isfile(args.output_file):
|
|
299
|
-
logging.info("Deleting empty file: %s" % args.output_file)
|
|
300
|
+
logging.info("Deleting empty output file: %s" % args.output_file)
|
|
300
301
|
os.remove(args.output_file)
|
|
301
|
-
raise
|
|
302
302
|
|
|
303
303
|
def catalog_put(self, args):
|
|
304
304
|
"""Implements the catalog_put sub-command.
|
|
@@ -311,7 +311,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
311
311
|
with open(args.input_file, "rb") as input_file:
|
|
312
312
|
resp = catalog.put(args.path, data=input_file, headers=headers)
|
|
313
313
|
if not args.quiet:
|
|
314
|
-
|
|
314
|
+
print(json.dumps(resp.json()))
|
|
315
315
|
except HTTPError as e:
|
|
316
316
|
if e.response.status_code == requests.codes.not_found:
|
|
317
317
|
raise ResourceException('Catalog not found', e)
|
|
@@ -329,7 +329,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
329
329
|
with open(args.input_file, "rb") as input_file:
|
|
330
330
|
resp = catalog.post(args.path, data=input_file, headers=headers)
|
|
331
331
|
if not args.quiet:
|
|
332
|
-
|
|
332
|
+
print(json.dumps(resp.json()))
|
|
333
333
|
except HTTPError as e:
|
|
334
334
|
if e.response.status_code == requests.codes.not_found:
|
|
335
335
|
raise ResourceException('Catalog not found', e)
|
|
@@ -380,8 +380,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
380
380
|
owner = args.owner if args.owner else None
|
|
381
381
|
alias = self.server.create_ermrest_alias(args.id, owner, args.alias_target)
|
|
382
382
|
if not args.quiet:
|
|
383
|
-
print(
|
|
384
|
-
pp(alias.retrieve())
|
|
383
|
+
print(json.dumps(alias.retrieve()))
|
|
385
384
|
except HTTPError as e:
|
|
386
385
|
if e.response.status_code == requests.codes.not_found:
|
|
387
386
|
raise ResourceException('Catalog alias not found', e)
|
|
@@ -397,7 +396,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
397
396
|
alias = self.server.connect_ermrest_alias(args.id)
|
|
398
397
|
response = alias.retrieve()
|
|
399
398
|
if not args.quiet:
|
|
400
|
-
|
|
399
|
+
print(json.dumps(response))
|
|
401
400
|
except HTTPError as e:
|
|
402
401
|
if e.response.status_code == requests.codes.not_found:
|
|
403
402
|
raise ResourceException('Catalog alias not found', e)
|
|
@@ -409,8 +408,7 @@ class DerivaCatalogCLI (BaseCLI):
|
|
|
409
408
|
owner = args.owner if args.owner else None
|
|
410
409
|
alias = self.server.connect_ermrest_alias(args.id)
|
|
411
410
|
response = alias.update(owner, args.alias_target, args.id)
|
|
412
|
-
print(
|
|
413
|
-
pp(response)
|
|
411
|
+
print(json.dumps(response))
|
|
414
412
|
except HTTPError as e:
|
|
415
413
|
if e.response.status_code == requests.codes.not_found:
|
|
416
414
|
raise ResourceException('Catalog alias not found', e)
|
deriva/core/ermrest_catalog.py
CHANGED
|
@@ -1138,7 +1138,7 @@ class ErmrestAlias(DerivaBinding):
|
|
|
1138
1138
|
)
|
|
1139
1139
|
|
|
1140
1140
|
@classmethod
|
|
1141
|
-
def _digest_alias_args(cls, id, owner, alias_target, name, description):
|
|
1141
|
+
def _digest_alias_args(cls, id, owner, alias_target, name=None, description=None):
|
|
1142
1142
|
rep = ErmrestCatalog._digest_catalog_args(id, owner, name, description)
|
|
1143
1143
|
|
|
1144
1144
|
if isinstance(alias_target, (str, type(None))):
|
deriva/core/ermrest_model.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import datetime
|
|
4
5
|
import base64
|
|
5
6
|
import hashlib
|
|
6
7
|
import json
|
|
@@ -10,7 +11,10 @@ from collections.abc import Iterable
|
|
|
10
11
|
from enum import Enum
|
|
11
12
|
|
|
12
13
|
from . import AttrDict, tag, urlquote, stob, mmo
|
|
13
|
-
|
|
14
|
+
from . import \
|
|
15
|
+
crockford_b32encode, crockford_b32decode, \
|
|
16
|
+
int_to_uintX, uintX_to_int, \
|
|
17
|
+
datetime_to_epoch_microseconds, epoch_microseconds_to_datetime
|
|
14
18
|
|
|
15
19
|
class NoChange (object):
|
|
16
20
|
"""Special class used to distinguish no-change default arguments to methods.
|
|
@@ -111,6 +115,93 @@ def sql_literal(v):
|
|
|
111
115
|
s = '%s' % v
|
|
112
116
|
return "'%s'" % (s.replace("'", "''"))
|
|
113
117
|
|
|
118
|
+
def timestamptz_to_datetime(ts: str) -> datetime.datetime:
|
|
119
|
+
"""Convert an ERMrest (i.e. PostgreSQL) timestamptz string to native datetime.
|
|
120
|
+
|
|
121
|
+
:param ts: A string in ISO format as serialized by ERMrest.
|
|
122
|
+
"""
|
|
123
|
+
# Workaround for fromisoformat() limitations in older Python 3.x:
|
|
124
|
+
# Make sure ISO sring has exactly 6 digits of fractional second
|
|
125
|
+
# precision. We can remove this rewrite phase once we bump our
|
|
126
|
+
# minimum language version requirement.
|
|
127
|
+
m = re.match(
|
|
128
|
+
# groups: basetimestamp fracseconds tzoffset
|
|
129
|
+
"^([-0-9]+[T ][:0-9]+)([.][0-9]+)?([-+][:0-9]+)$",
|
|
130
|
+
ts,)
|
|
131
|
+
if m:
|
|
132
|
+
parts = list(m.group(1, 2, 3))
|
|
133
|
+
if parts[1]:
|
|
134
|
+
frac = float(parts[1])
|
|
135
|
+
parts[1] = ("%.6f" % frac).lstrip('0')
|
|
136
|
+
else:
|
|
137
|
+
parts[1] = '.000000'
|
|
138
|
+
ts = '%s%s%s' % tuple(parts)
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f'bad operand to timestamptz_to_datetime() {ts=}')
|
|
141
|
+
|
|
142
|
+
return datetime.datetime.fromisoformat(ts)
|
|
143
|
+
|
|
144
|
+
def datetime_to_timestamptz(dt: datetime.datetime) -> str:
|
|
145
|
+
"""Convert a native datetime to an ERMrest timestamptz string.
|
|
146
|
+
|
|
147
|
+
:param dt: A timezone-aware datetime.datetime instance.
|
|
148
|
+
"""
|
|
149
|
+
return dt.isoformat(' ')
|
|
150
|
+
|
|
151
|
+
def epoch_microseconds_to_snaptime(us: int) -> str:
|
|
152
|
+
"""Convert microseconds-since-epoch to ERMrest snaptime format.
|
|
153
|
+
|
|
154
|
+
:param us: Signed integer microseconds-since-epoch.
|
|
155
|
+
|
|
156
|
+
This function includes a bit-shift necessary to introduce the
|
|
157
|
+
padding that ERMrest uses to pack a 64-bit value into a 65-bit
|
|
158
|
+
base32 encoding scheme.
|
|
159
|
+
"""
|
|
160
|
+
if not isinstance(us, int) or us > 2**63-1 or us < -2**63:
|
|
161
|
+
raise ValueError(us)
|
|
162
|
+
|
|
163
|
+
return crockford_b32encode(int_to_uintX(us, 64) << 1)
|
|
164
|
+
|
|
165
|
+
def snaptime_to_epoch_microseconds(s: str) -> int:
|
|
166
|
+
"""Convert ERMrest snaptime format to integer microseconds-since-epoch.
|
|
167
|
+
|
|
168
|
+
:param s: The ERMrest snaptime string.
|
|
169
|
+
|
|
170
|
+
This function includes a bit-shift necessary to strip the
|
|
171
|
+
padding that ERMrest uses to pack a 64-bit value into a 65-bit
|
|
172
|
+
base32 encoding scheme.
|
|
173
|
+
"""
|
|
174
|
+
return uintX_to_int(crockford_b32decode(s) >> 1, 64)
|
|
175
|
+
|
|
176
|
+
def datetime_to_snaptime(dt: datetime.datetime) -> str:
|
|
177
|
+
"""Convert a datetime to ERMrest snaptime format.
|
|
178
|
+
|
|
179
|
+
:param dt: A timezone-aware datetime.datetime instance.
|
|
180
|
+
"""
|
|
181
|
+
return epoch_microseconds_to_snaptime(datetime_to_epoch_microseconds(dt))
|
|
182
|
+
|
|
183
|
+
def snaptime_to_datetime(s: str) -> datetime.datetime:
|
|
184
|
+
"""Convert ERMrest snatime format to datetime.
|
|
185
|
+
|
|
186
|
+
:param s: The ERMrest snaptime string.
|
|
187
|
+
"""
|
|
188
|
+
return epoch_microseconds_to_datetime(snaptime_to_epoch_microseconds(s))
|
|
189
|
+
|
|
190
|
+
def snaptime_to_timestamptz(s: str) -> str:
|
|
191
|
+
"""Convert ERMrest stamptime format to ERMrest timestamptz string.
|
|
192
|
+
|
|
193
|
+
:param s: The ERMrest snaptime string.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
return datetime_to_timestamptz(snaptime_to_datetime(s))
|
|
197
|
+
|
|
198
|
+
def timestamptz_to_snaptime(ts: str) -> str:
|
|
199
|
+
"""Convert ERMrest timestamptz str to ERMrest snaptime format.
|
|
200
|
+
|
|
201
|
+
:param ts: A string in ISO datetime format as serialized by ERMrest.
|
|
202
|
+
"""
|
|
203
|
+
return datetime_to_snaptime(timestamptz_to_datetime(ts))
|
|
204
|
+
|
|
114
205
|
def presence_annotation(tag_uri):
|
|
115
206
|
"""Decorator to establish property getter/setter/deleter for presence annotations.
|
|
116
207
|
|
deriva/core/hatrac_cli.py
CHANGED
|
@@ -112,6 +112,8 @@ class DerivaHatracCLI (BaseCLI):
|
|
|
112
112
|
help="Chunk size in bytes")
|
|
113
113
|
putobj_parser.add_argument("--parents", action="store_true",
|
|
114
114
|
help="Create intermediate parent namespaces as required")
|
|
115
|
+
putobj_parser.add_argument("--force", action="store_true",
|
|
116
|
+
help="Force upload even if same object content already exists")
|
|
115
117
|
putobj_parser.set_defaults(func=self.putobj)
|
|
116
118
|
|
|
117
119
|
# delobj parser
|
|
@@ -119,6 +121,13 @@ class DerivaHatracCLI (BaseCLI):
|
|
|
119
121
|
delobj_parser.add_argument("resource", metavar="<path>", type=str, help="object path")
|
|
120
122
|
delobj_parser.set_defaults(func=self.delobj)
|
|
121
123
|
|
|
124
|
+
# renobj parser
|
|
125
|
+
renobj_parser = subparsers.add_parser('rename', help="rename object")
|
|
126
|
+
renobj_parser.add_argument('source_name', metavar="<source_name>", type=str, help="source object path")
|
|
127
|
+
renobj_parser.add_argument("resource", metavar="<path>", type=str, help="object path")
|
|
128
|
+
renobj_parser.add_argument("--copy-acls", action="store_true", help="copy source ACLs to new resource")
|
|
129
|
+
renobj_parser.set_defaults(func=self.renobj)
|
|
130
|
+
|
|
122
131
|
@staticmethod
|
|
123
132
|
def _get_credential(host_name, token=None, oauth2_token=None):
|
|
124
133
|
if token or oauth2_token:
|
|
@@ -131,7 +140,7 @@ class DerivaHatracCLI (BaseCLI):
|
|
|
131
140
|
"""
|
|
132
141
|
self.host = args.host if args.host else 'localhost'
|
|
133
142
|
self.resource = args.resource
|
|
134
|
-
self.store = HatracStore('https',
|
|
143
|
+
self.store = HatracStore('https', self.host, DerivaHatracCLI._get_credential(self.host,
|
|
135
144
|
token=args.token,
|
|
136
145
|
oauth2_token=args.oauth2_token))
|
|
137
146
|
|
|
@@ -256,7 +265,8 @@ class DerivaHatracCLI (BaseCLI):
|
|
|
256
265
|
content_type=content_type,
|
|
257
266
|
chunked=True if file_size > args.chunk_size else False,
|
|
258
267
|
chunk_size=args.chunk_size,
|
|
259
|
-
create_parents=args.parents
|
|
268
|
+
create_parents=args.parents,
|
|
269
|
+
force=args.force)
|
|
260
270
|
print(loc)
|
|
261
271
|
except HTTPError as e:
|
|
262
272
|
if e.response.status_code == requests.codes.not_found:
|
|
@@ -278,6 +288,24 @@ class DerivaHatracCLI (BaseCLI):
|
|
|
278
288
|
else:
|
|
279
289
|
raise e
|
|
280
290
|
|
|
291
|
+
def renobj(self, args):
|
|
292
|
+
"""Implements the renobj sub-command.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
loc = self.store.rename_obj(
|
|
296
|
+
args.source_name,
|
|
297
|
+
self.resource,
|
|
298
|
+
copy_acls=args.copy_acls)
|
|
299
|
+
print(loc)
|
|
300
|
+
except HTTPError as e:
|
|
301
|
+
if e.response.status_code == requests.codes.conflict:
|
|
302
|
+
raise ResourceException(
|
|
303
|
+
f'Source name "{args.source_name}" must name an existing object', e)
|
|
304
|
+
elif e.response.status_code == requests.codes.method_not_allowed:
|
|
305
|
+
raise UsageException('operation not support by host. To enable this feature, upgrade Hatrac service.')
|
|
306
|
+
else:
|
|
307
|
+
raise e
|
|
308
|
+
|
|
281
309
|
def main(self):
|
|
282
310
|
"""Main routine of the CLI.
|
|
283
311
|
"""
|
deriva/core/hatrac_store.py
CHANGED
|
@@ -160,7 +160,8 @@ class HatracStore(DerivaBinding):
|
|
|
160
160
|
parents=True,
|
|
161
161
|
content_type=None,
|
|
162
162
|
content_disposition=None,
|
|
163
|
-
allow_versioning=True
|
|
163
|
+
allow_versioning=True,
|
|
164
|
+
force=False):
|
|
164
165
|
"""Idempotent upload of object, returning object location URI.
|
|
165
166
|
|
|
166
167
|
Arguments:
|
|
@@ -173,6 +174,7 @@ class HatracStore(DerivaBinding):
|
|
|
173
174
|
content_type: the content-type of the object (optional)
|
|
174
175
|
content_disposition: the preferred content-disposition of the object (optional)
|
|
175
176
|
allow_versioning: reject with NotModified if content already exists (optional)
|
|
177
|
+
force: upload object even if it already exists and/or matches content (optional)
|
|
176
178
|
Automatically computes and sends Content-MD5 if no digests provided.
|
|
177
179
|
|
|
178
180
|
If an object-version already exists under the same name
|
|
@@ -204,22 +206,23 @@ class HatracStore(DerivaBinding):
|
|
|
204
206
|
raise ValueError("The PUT request payload size of %d bytes is larger than the currently allowed maximum "
|
|
205
207
|
"payload size of %d bytes for single request PUT operations. Use the 'put_loc' function "
|
|
206
208
|
"to perform chunked uploads of large data objects." % (file_size, max_request_size))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
209
|
+
if not force:
|
|
210
|
+
try:
|
|
211
|
+
r = self.head(path)
|
|
212
|
+
if r.status_code == 200:
|
|
213
|
+
if (md5 and r.headers.get('Content-MD5') == md5 or
|
|
214
|
+
sha256 and r.headers.get('Content-SHA256') == sha256):
|
|
215
|
+
# object already has same content so skip upload
|
|
216
|
+
if file_opened:
|
|
217
|
+
f.close()
|
|
218
|
+
return r.headers.get('Content-Location')
|
|
219
|
+
elif not allow_versioning:
|
|
220
|
+
raise NotModified("The data cannot be uploaded because content already exists for this object "
|
|
221
|
+
"and multiple versions are not allowed.")
|
|
222
|
+
except requests.HTTPError as e:
|
|
223
|
+
if e.response.status_code != 404:
|
|
224
|
+
logging.debug("HEAD request failed: %s" % format_exception(e))
|
|
225
|
+
pass
|
|
223
226
|
|
|
224
227
|
# TODO: verify incoming hashes if supplied?
|
|
225
228
|
headers['Content-MD5'] = md5
|
|
@@ -250,6 +253,54 @@ class HatracStore(DerivaBinding):
|
|
|
250
253
|
self.delete(path)
|
|
251
254
|
logging.debug('Deleted object "%s%s".' % (self._server_uri, path))
|
|
252
255
|
|
|
256
|
+
def rename_obj(self,
|
|
257
|
+
source_path,
|
|
258
|
+
path,
|
|
259
|
+
versions=None,
|
|
260
|
+
copy_acls=False,
|
|
261
|
+
headers=DEFAULT_HEADERS):
|
|
262
|
+
"""Rename object optionally specifying versions to process and whether to copy acls.
|
|
263
|
+
|
|
264
|
+
:param source_path: source object path
|
|
265
|
+
:param path: new path for object
|
|
266
|
+
:param versions: list of object versions to process, by default it processes all versions
|
|
267
|
+
:param copy_acls: if true, copy all source acls, else default to set basic ownership like a new object version
|
|
268
|
+
:param headers: headers to send to hatrac
|
|
269
|
+
:return: resource location
|
|
270
|
+
"""
|
|
271
|
+
self.check_path(source_path)
|
|
272
|
+
self.check_path(path)
|
|
273
|
+
|
|
274
|
+
headers = headers.copy()
|
|
275
|
+
headers['deriva-client-context'] = self.dcctx.merged(headers.get('deriva-client-context', {})).encoded()
|
|
276
|
+
headers['Content-Type'] = 'application/json'
|
|
277
|
+
|
|
278
|
+
obj = {
|
|
279
|
+
"command": "rename_from",
|
|
280
|
+
"source_name": source_path.removeprefix('/hatrac'),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if versions:
|
|
284
|
+
if not isinstance(versions, list):
|
|
285
|
+
raise TypeError('"versions" argument must be a list')
|
|
286
|
+
if not all(isinstance(v, str) for v in versions):
|
|
287
|
+
raise TypeError('elements of "versions" argument must be strings')
|
|
288
|
+
obj["versions"] = versions
|
|
289
|
+
|
|
290
|
+
if copy_acls:
|
|
291
|
+
if copy_acls is not True:
|
|
292
|
+
raise TypeError('"copy_acls" argument must be a boolean')
|
|
293
|
+
obj["copy_acls"] = copy_acls
|
|
294
|
+
|
|
295
|
+
logging.debug(f'rename object message body: {str(obj)}')
|
|
296
|
+
|
|
297
|
+
r = self._session.post(self._server_uri + path, json=obj, headers=headers)
|
|
298
|
+
self._response_raise_for_status(r)
|
|
299
|
+
loc = r.text.strip() or r.url
|
|
300
|
+
if loc.startswith(self._server_uri):
|
|
301
|
+
loc = loc[len(self._server_uri):]
|
|
302
|
+
return loc
|
|
303
|
+
|
|
253
304
|
def put_loc(self,
|
|
254
305
|
path,
|
|
255
306
|
file_path,
|
|
@@ -263,7 +314,8 @@ class HatracStore(DerivaBinding):
|
|
|
263
314
|
create_parents=True,
|
|
264
315
|
allow_versioning=True,
|
|
265
316
|
callback=None,
|
|
266
|
-
cancel_job_on_error=True
|
|
317
|
+
cancel_job_on_error=True,
|
|
318
|
+
force=False):
|
|
267
319
|
"""
|
|
268
320
|
:param path:
|
|
269
321
|
:param file_path:
|
|
@@ -278,6 +330,7 @@ class HatracStore(DerivaBinding):
|
|
|
278
330
|
:param allow_versioning:
|
|
279
331
|
:param callback:
|
|
280
332
|
:param cancel_job_on_error:
|
|
333
|
+
:param force:
|
|
281
334
|
:return:
|
|
282
335
|
"""
|
|
283
336
|
self.check_path(path)
|
|
@@ -291,25 +344,27 @@ class HatracStore(DerivaBinding):
|
|
|
291
344
|
content_type=content_type,
|
|
292
345
|
content_disposition=content_disposition,
|
|
293
346
|
parents=create_parents,
|
|
294
|
-
allow_versioning=allow_versioning
|
|
347
|
+
allow_versioning=allow_versioning,
|
|
348
|
+
force=force)
|
|
295
349
|
|
|
296
350
|
if not (md5 or sha256):
|
|
297
351
|
md5 = hu.compute_file_hashes(file_path, hashes=['md5'])['md5'][1]
|
|
298
352
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
if not force:
|
|
354
|
+
try:
|
|
355
|
+
r = self.head(path)
|
|
356
|
+
if r.status_code == 200:
|
|
357
|
+
if (md5 and r.headers.get('Content-MD5') == md5 or
|
|
358
|
+
sha256 and r.headers.get('Content-SHA256') == sha256):
|
|
359
|
+
# object already has same content so skip upload
|
|
360
|
+
return r.headers.get('Content-Location')
|
|
361
|
+
elif not allow_versioning:
|
|
362
|
+
raise NotModified("The file [%s] cannot be uploaded because content already exists for this object "
|
|
363
|
+
"and multiple versions are not allowed." % file_path)
|
|
364
|
+
except requests.HTTPError as e:
|
|
365
|
+
if e.response.status_code != 404:
|
|
366
|
+
logging.debug("HEAD request failed: %s" % format_exception(e))
|
|
367
|
+
pass
|
|
313
368
|
|
|
314
369
|
job_id = self.create_upload_job(path,
|
|
315
370
|
file_path,
|