Skip to content

Commit 3a3ef32

Browse files
authored
feat(python): add tag parameter to Branch.transact() (#9334)
Add optional tag parameter to automatically create tags after successful transaction completion, eliminating manual commit SHA retrieval. Usage: ```py with branch.transact(commit_message='update', tag='v1.0.0') as tx: tx.object('file.txt').upload('data') # Tag 'v1.0.0' created on merge commit ``` - Tag created only on transaction success (fail-fast validation) - Empty tag names treated as None - Tag accessible via tx.tag attribute Closes: #8200
1 parent 5c44e89 commit 3a3ef32

File tree

2 files changed

+78
-10
lines changed

2 files changed

+78
-10
lines changed

clients/python-wrapper/lakefs/branch.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
from lakefs.import_manager import ImportManager
1717
from lakefs.reference import Reference, ReferenceType, generate_listing
1818
from lakefs.models import Change, Commit
19+
from lakefs.tag import Tag
1920
from lakefs.exceptions import (
2021
api_exception_handler,
2122
ConflictException,
2223
LakeFSException,
23-
TransactionException
24+
TransactionException,
2425
)
2526

2627

@@ -289,7 +290,7 @@ def import_data(self, commit_message: str = "", metadata: Optional[dict] = None)
289290

290291
@contextmanager
291292
def transact(self, commit_message: str = "", commit_metadata: Optional[Dict] = None,
292-
delete_branch_on_error: bool = True) -> _Transaction:
293+
delete_branch_on_error: bool = True, tag: Optional[str] = None) -> _Transaction:
293294
"""
294295
Create a transaction for multiple operations.
295296
Transaction allows for multiple modifications to be performed atomically on a branch,
@@ -323,10 +324,11 @@ def transact(self, commit_message: str = "", commit_metadata: Optional[Dict] = N
323324
:param commit_message: once the transaction is committed, a commit is created with this message
324325
:param commit_metadata: user metadata for the transaction commit
325326
:param delete_branch_on_error: Defaults to True. Ensures ephemeral branch is deleted on error.
327+
:param tag: Optional tag name to create after the transaction is successfully completed
326328
:return: a Transaction object to perform the operations on
327329
"""
328330
with Transaction(self._repo_id, self._id, commit_message, commit_metadata, delete_branch_on_error,
329-
self._client) as tx:
331+
self._client, tag) as tx:
330332
yield tx
331333

332334

@@ -336,10 +338,11 @@ def _get_tx_name() -> str:
336338
return f"tx-{uuid.uuid4()}" # Don't rely on source branch name as this might exceed valid branch length
337339

338340
def __init__(self, repository_id: str, branch_id: str, commit_message: str = "",
339-
commit_metadata: Optional[Dict] = None, client: Client = None):
341+
commit_metadata: Optional[Dict] = None, client: Client = None, tag: Optional[str] = None):
340342
self._commit_message = commit_message
341343
self._commit_metadata = commit_metadata
342344
self._source_branch = branch_id
345+
self._tag = tag
343346

344347
tx_name = self._get_tx_name()
345348
self._tx_branch = Branch(repository_id, tx_name, client).create(branch_id, hidden=True)
@@ -382,6 +385,21 @@ def commit_metadata(self, metadata: Optional[Dict]) -> None:
382385
"""
383386
self._commit_metadata = metadata
384387

388+
@property
389+
def tag(self) -> Optional[str]:
390+
"""
391+
Return the tag name configured for this transaction completion
392+
"""
393+
return self._tag
394+
395+
@tag.setter
396+
def tag(self, tag: str) -> None:
397+
"""
398+
Set the tag name for this transaction completion
399+
:param tag: The tag name to create after the transaction is successfully completed
400+
"""
401+
self._tag = tag
402+
385403

386404
class Transaction:
387405
"""
@@ -393,7 +411,8 @@ class Transaction:
393411
"""
394412

395413
def __init__(self, repository_id: str, branch_id: str, commit_message: str = "",
396-
commit_metadata: Optional[Dict] = None, delete_branch_on_error: bool = True, client: Client = None):
414+
commit_metadata: Optional[Dict] = None, delete_branch_on_error: bool = True, client: Client = None,
415+
tag: Optional[str] = None):
397416
self._repo_id = repository_id
398417
self._commit_message = commit_message
399418
self._commit_metadata = commit_metadata
@@ -402,10 +421,11 @@ def __init__(self, repository_id: str, branch_id: str, commit_message: str = "",
402421
self._tx = None
403422
self._tx_branch = None
404423
self._cleanup_branch = delete_branch_on_error
424+
self._tag = tag
405425

406426
def __enter__(self):
407427
self._tx = _Transaction(self._repo_id, self._source_branch, self._commit_message, self._commit_metadata,
408-
self._client)
428+
self._client, self._tag)
409429
self._tx_branch = Branch(self._repo_id, self._tx.id, self._client)
410430
return self._tx
411431

@@ -417,11 +437,15 @@ def __exit__(self, typ, value, traceback) -> bool:
417437

418438
try:
419439
self._tx_branch.commit(message=self._tx.commit_message, metadata=self._tx.commit_metadata)
420-
self._tx_branch.merge_into(self._source_branch, message=f"Merge transaction {self._tx.id} to branch")
440+
merge_commit_id = self._tx_branch.merge_into(
441+
self._source_branch, message=f"Merge transaction {self._tx.id} to branch")
421442
self._tx_branch.delete()
422-
423-
return False
424443
except LakeFSException as e:
425444
if self._cleanup_branch:
426445
self._tx_branch.delete()
427446
raise TransactionException(f"Failed committing transaction {self._tx.id}: {e}") from e
447+
448+
if self._tx.tag:
449+
tag = Tag(self._repo_id, self._tx.tag, self._client)
450+
tag.create(merge_commit_id)
451+
return False

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import ValidationError
77

88
import lakefs
9-
from lakefs.exceptions import NotFoundException, TransactionException
9+
from lakefs.exceptions import NotFoundException, TransactionException, ConflictException
1010
from tests.utests.common import expect_exception_context
1111

1212

@@ -129,6 +129,8 @@ def test_transaction(setup_repo):
129129
tx.reset_changes(path_type="common_prefix", path="foo")
130130
tx_id = tx.id
131131

132+
assert not list(repo.tags())
133+
132134
# Verify transaction branch was deleted
133135
with expect_exception_context(NotFoundException):
134136
repo.branch(tx.id).get_commit()
@@ -207,3 +209,45 @@ def test_transaction_failure(setup_repo, cleanup_branch):
207209
assert tx.get_commit()
208210

209211
assert test_branch.get_commit() == new_ref.get_commit()
212+
213+
214+
def test_transaction_with_tag(setup_repo):
215+
_, repo = setup_repo
216+
path_and_data = ["a", "b", "bar/a", "bar/b", "bar/c", "c"]
217+
test_branch = repo.branch("main")
218+
219+
with test_branch.transact(commit_message="my transaction with tag", tag="v1.0.0") as tx:
220+
assert tx.tag == "v1.0.0"
221+
upload_data(tx, path_and_data)
222+
assert repo.tag("v1.0.0").get_commit() == test_branch.get_commit()
223+
224+
225+
def test_transaction_with_explicit_none_tag(setup_repo):
226+
_, repo = setup_repo
227+
path_and_data = ["a", "b", "bar/a", "bar/b", "bar/c", "c"]
228+
test_branch = repo.branch("main")
229+
230+
with test_branch.transact(commit_message="my transaction with tag", tag=None) as tx:
231+
assert tx.tag is None
232+
upload_data(tx, path_and_data)
233+
assert not list(repo.tags())
234+
235+
236+
def test_transaction_with_existing_tag(setup_repo):
237+
_, repo = setup_repo
238+
path_and_data = ["a", "b", "bar/a", "bar/b", "bar/c", "c"]
239+
test_branch = repo.branch("main")
240+
241+
repo: "lakefs.Repository"
242+
243+
test_branch.object("initial_file").upload("initial content")
244+
initial_commit = test_branch.commit("initial commit")
245+
repo.tag("v1.0.0").create(initial_commit)
246+
247+
with expect_exception_context(ConflictException, "tag already exists"):
248+
with test_branch.transact(commit_message="my transaction with existing tag", tag="v1.0.0") as tx:
249+
upload_data(tx, path_and_data)
250+
251+
# Verify transaction branch was deleted
252+
with expect_exception_context(NotFoundException):
253+
repo.branch(tx.id).get_commit()

0 commit comments

Comments
 (0)