Skip to content

Commit e69ea94

Browse files
committed
Python HL SDK: use IfNoneMatch to support conditional write (aka exclusive creation)
Closes #7505. This replaces existing obj.exists() check with conditional writes with 'IfNoneMatch: *'. I have tried to maintain compat in terms of error raised, by raising `ObjectExistsError` on pre-condition failure.
1 parent dc49df9 commit e69ea94

File tree

2 files changed

+19
-9
lines changed

2 files changed

+19
-9
lines changed

clients/python-wrapper/lakefs/object.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import base64
88
import binascii
9+
import http
910
import io
1011
import json
1112
import os
@@ -399,9 +400,6 @@ def __init__(self,
399400
metadata: Optional[dict[str, str]] = None,
400401
client: Optional[Client] = None) -> None:
401402

402-
if 'x' in mode and obj.exists(): # Requires explicit create
403-
raise ObjectExistsException
404-
405403
if mode not in get_args(WriteModes):
406404
raise ValueError(f"invalid write mode: '{mode}'. WriteModes: {WriteModes}")
407405

@@ -520,6 +518,8 @@ def _upload_raw(self) -> lakefs_sdk.ObjectStats:
520518
"Accept": "application/json",
521519
"Content-Type": self.content_type if self.content_type is not None else "application/octet-stream"
522520
}
521+
if self._mode.startswith("x"):
522+
headers["If-None-Match"] = "*"
523523

524524
# Create user metadata headers
525525
if self.metadata is not None:
@@ -538,6 +538,8 @@ def _upload_raw(self) -> lakefs_sdk.ObjectStats:
538538
headers=headers,
539539
body=self._fd)
540540

541+
if self._mode.startswith("x") and resp.status == http.HTTPStatus.PRECONDITION_FAILED:
542+
raise ObjectExistsException(resp.status, resp.reason, resp.data)
541543
handle_http_error(resp)
542544
return lakefs_sdk.ObjectStats(**json.loads(resp.data))
543545

@@ -568,10 +570,17 @@ def _upload_presign(self) -> lakefs_sdk.ObjectStats:
568570
checksum=etag,
569571
user_metadata=self.metadata,
570572
content_type=self.content_type)
571-
return self._client.sdk_client.staging_api.link_physical_address(self._obj.repo,
572-
self._obj.ref,
573-
self._obj.path,
574-
staging_metadata=staging_metadata)
573+
if_none_match = "*" if self._mode.startswith("x") else None
574+
try:
575+
return self._client.sdk_client.staging_api.link_physical_address(self._obj.repo,
576+
self._obj.ref,
577+
self._obj.path,
578+
staging_metadata=staging_metadata,
579+
if_none_match=if_none_match)
580+
except lakefs_sdk.ApiException as e:
581+
if self._mode.startswith("x") and e.status == http.HTTPStatus.PRECONDITION_FAILED:
582+
raise ObjectExistsException(e.status, e.reason, e.body) from e
583+
raise
575584

576585
def readable(self) -> bool:
577586
"""

clients/python-wrapper/tests/integration/test_object.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,14 @@ def test_object_read_seek(setup_repo, pre_sign):
8383
fd.close()
8484

8585

86-
def test_object_upload_exists(setup_repo):
86+
@pytest.mark.parametrize("pre_sign", (True, False))
87+
def test_object_upload_exists(setup_repo, pre_sign):
8788
clt, repo = setup_repo
8889
data = b"test_data"
8990
obj = WriteableObject(repository_id=repo.properties.id, reference_id="main", path="test_obj", client=clt).upload(
9091
data=data)
9192
with expect_exception_context(ObjectExistsException):
92-
obj.upload(data="some_other_data", mode='xb')
93+
obj.upload(data="some_other_data", mode='xb', pre_sign=pre_sign)
9394

9495
with obj.reader() as fd:
9596
assert fd.read() == data

0 commit comments

Comments
 (0)