diff --git a/CHANGELOG.md b/CHANGELOG.md index f66ae4e0a..a541549ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +* `get_fresh_state` method added to `FileVersion` and `Bucket` + ### Changed * `download_file_*` methods refactored to allow for inspecting DownloadVersion before downloading the whole file * `B2Api.get_file_info` returns a `FileVersion` object in v2 diff --git a/b2sdk/_v2/exception.py b/b2sdk/_v2/exception.py index f216c2e71..09ee20b8d 100644 --- a/b2sdk/_v2/exception.py +++ b/b2sdk/_v2/exception.py @@ -25,6 +25,7 @@ from b2sdk.exception import BadFileInfo from b2sdk.exception import BadJson from b2sdk.exception import BadUploadUrl +from b2sdk.exception import BucketIdNotFound from b2sdk.exception import BrokenPipe from b2sdk.exception import BucketNotAllowed from b2sdk.exception import CapabilityNotAllowed @@ -95,6 +96,7 @@ 'BadJson', 'BadUploadUrl', 'BrokenPipe', + 'BucketIdNotFound', 'BucketNotAllowed', 'CapabilityNotAllowed', 'CapExceeded', diff --git a/b2sdk/api.py b/b2sdk/api.py index 1cafd4bf6..a04a1a817 100644 --- a/b2sdk/api.py +++ b/b2sdk/api.py @@ -16,7 +16,7 @@ from .cache import AbstractCache from .bucket import Bucket, BucketFactory from .encryption.setting import EncryptionSetting -from .exception import NonExistentBucket, RestrictedBucket +from .exception import BucketIdNotFound, NonExistentBucket, RestrictedBucket from .file_lock import FileRetentionSetting, LegalHold from .file_version import DownloadVersionFactory, FileIdAndName, FileVersion, FileVersionFactory from .large_file.services import LargeFileServices @@ -286,7 +286,7 @@ def get_bucket_by_id(self, bucket_id: str) -> Bucket: return bucket # There is no such bucket. - raise NonExistentBucket(bucket_name) + raise BucketIdNotFound(bucket_id) def get_bucket_by_name(self, bucket_name): """ diff --git a/b2sdk/bucket.py b/b2sdk/bucket.py index 263f527d7..3ba962d2c 100644 --- a/b2sdk/bucket.py +++ b/b2sdk/bucket.py @@ -13,7 +13,7 @@ from .encryption.setting import EncryptionSetting, EncryptionSettingFactory from .encryption.types import EncryptionMode -from .exception import FileNotPresent, FileOrBucketNotFound, UnexpectedCloudBehaviour, UnrecognizedBucketType +from .exception import BucketIdNotFound, FileNotPresent, FileOrBucketNotFound, UnexpectedCloudBehaviour, UnrecognizedBucketType from .file_lock import ( BucketRetentionSetting, FileLockConfiguration, @@ -88,6 +88,16 @@ def __init__( self.default_retention = default_retention self.is_file_lock_enabled = is_file_lock_enabled + def get_fresh_state(self) -> 'Bucket': + """ + Fetch all the information about this bucket and return a new bucket object. + This method does NOT change the object it is called on. + """ + buckets_found = self.api.list_buckets(bucket_id=self.id_) + if not buckets_found: + raise BucketIdNotFound(self.id_) + return buckets_found[0] + def get_id(self): """ Return bucket ID. diff --git a/b2sdk/exception.py b/b2sdk/exception.py index ad2e123e6..d8fda86ad 100644 --- a/b2sdk/exception.py +++ b/b2sdk/exception.py @@ -244,6 +244,14 @@ def __str__(self): return 'Could not find %s within %s' % (file_str, bucket_str) +class BucketIdNotFound(ResourceNotFound): + def __init__(self, bucket_id): + self.bucket_id = bucket_id + + def __str__(self): + return 'Bucket with id=%s not found' % (self.bucket_id,) + + class FileAlreadyHidden(B2SimpleError): pass diff --git a/b2sdk/file_version.py b/b2sdk/file_version.py index 9d2440911..17fcd52e2 100644 --- a/b2sdk/file_version.py +++ b/b2sdk/file_version.py @@ -182,6 +182,13 @@ def as_dict(self): return result + def get_fresh_state(self) -> 'FileVersion': + """ + Fetch all the information about this file version and return a new FileVersion object. + This method does NOT change the object it is called on. + """ + return self.api.get_file_info(self.id_) + class DownloadVersion(BaseFileVersion): """ diff --git a/b2sdk/v1/file_version.py b/b2sdk/v1/file_version.py index 8be55fbbc..dbe2be192 100644 --- a/b2sdk/v1/file_version.py +++ b/b2sdk/v1/file_version.py @@ -19,6 +19,7 @@ # Override to retain legacy class name, __init__ signature, slots # and old formatting methods # and to omit 'api' property when doing __eq__ and __repr__ +# and to make get_fresh_state return proper objects, even though v1.B2Api.get_file_info returns dicts class FileVersionInfo(v2.FileVersion): __slots__ = ['_api'] @@ -92,6 +93,13 @@ def format_ls_entry(self): def format_folder_ls_entry(cls, name): return cls.LS_ENTRY_TEMPLATE % ('-', '-', '-', '-', 0, name) + def get_fresh_state(self) -> 'FileVersionInfo': + """ + Fetch all the information about this file version and return a new FileVersion object. + This method does NOT change the object it is called on. + """ + return self.api.file_version_factory.from_api_response(self.api.get_file_info(self.id_)) + def file_version_info_from_new_file_version(file_version: v2.FileVersion) -> FileVersionInfo: return FileVersionInfo( diff --git a/test/unit/bucket/test_bucket.py b/test/unit/bucket/test_bucket.py index 243530c91..4378cc4c3 100644 --- a/test/unit/bucket/test_bucket.py +++ b/test/unit/bucket/test_bucket.py @@ -22,6 +22,7 @@ AlreadyFailed, B2Error, B2RequestTimeoutDuringUpload, + BucketIdNotFound, InvalidAuthToken, InvalidMetadataDirective, InvalidRange, @@ -39,7 +40,7 @@ from apiver_deps import FileVersion as VFileVersionInfo from apiver_deps import B2Api from apiver_deps import B2HttpApiConfig -from apiver_deps import Bucket +from apiver_deps import Bucket, BucketFactory from apiver_deps import DownloadedFile from apiver_deps import DownloadVersion from apiver_deps import LargeFileUploadState @@ -477,6 +478,26 @@ def test_delete_file_version(self): self.assertBucketContents(expected, '', show_versions=True) +class TestGetFreshState(TestCaseWithBucket): + def test_ok(self): + same_but_different = self.api.get_bucket_by_id(self.bucket.id_) + same_but_different = same_but_different.get_fresh_state() + assert isinstance(same_but_different, Bucket) + assert id(same_but_different) != id(self.bucket) + assert same_but_different.as_dict() == self.bucket.as_dict() + same_but_different = same_but_different.update(bucket_info={'completely': 'new info'}) + if apiver_deps.V <= 1: + same_but_different = BucketFactory.from_api_bucket_dict(self.api, same_but_different) + assert same_but_different.as_dict() != self.bucket.as_dict() + refreshed_bucket = self.bucket.get_fresh_state() + assert same_but_different.as_dict() == refreshed_bucket.as_dict() + + def test_fail(self): + self.api.delete_bucket(self.bucket) + with pytest.raises(BucketIdNotFound): + self.bucket.get_fresh_state() + + class TestListVersions(TestCaseWithBucket): def test_single_version(self): data = b'hello world' diff --git a/test/unit/file_version/test_file_version.py b/test/unit/file_version/test_file_version.py index 9c92e7e89..252c5ea43 100644 --- a/test/unit/file_version/test_file_version.py +++ b/test/unit/file_version/test_file_version.py @@ -11,6 +11,12 @@ import pytest import apiver_deps +from apiver_deps import B2Api +from apiver_deps import B2HttpApiConfig +from apiver_deps import DummyCache +from apiver_deps import InMemoryAccountInfo +from apiver_deps import LegalHold +from apiver_deps import RawSimulator if apiver_deps.V <= 1: from apiver_deps import FileVersionInfo as VFileVersion @@ -19,6 +25,17 @@ class TestFileVersion: + @pytest.fixture(autouse=True) + def setUp(self): + self.account_info = InMemoryAccountInfo() + self.cache = DummyCache() + self.api = B2Api( + self.account_info, self.cache, api_config=B2HttpApiConfig(_raw_api_class=RawSimulator) + ) + self.raw_api = self.api.session.raw_api + (self.application_key_id, self.master_key) = self.raw_api.create_account() + self.api.authorize_account('production', self.application_key_id, self.master_key) + @pytest.mark.apiver(to_ver=1) def test_format_ls_entry(self): file_version_info = VFileVersion( @@ -30,3 +47,17 @@ def test_format_ls_entry(self): '00:00:02 200 inner/a.txt' ) assert expected_entry == file_version_info.format_ls_entry() + + def test_get_fresh_state(self): + self.bucket = self.api.create_bucket('testbucket', 'allPrivate', is_file_lock_enabled=True) + initial_file_version = self.bucket.upload_bytes(b'nothing', 'test_file') + self.api.update_file_legal_hold( + initial_file_version.id_, initial_file_version.file_name, LegalHold.ON + ) + fetched_version = self.api.get_file_info(initial_file_version.id_) + if apiver_deps.V <= 1: + fetched_version = self.api.file_version_factory.from_api_response(fetched_version) + assert initial_file_version.as_dict() != fetched_version.as_dict() + refreshed_version = initial_file_version.get_fresh_state() + assert isinstance(refreshed_version, VFileVersion) + assert refreshed_version.as_dict() == fetched_version.as_dict() diff --git a/test/unit/v_all/test_api.py b/test/unit/v_all/test_api.py index 55fdb4184..43ecee13f 100644 --- a/test/unit/v_all/test_api.py +++ b/test/unit/v_all/test_api.py @@ -18,7 +18,7 @@ from apiver_deps import EncryptionSetting from apiver_deps import InMemoryAccountInfo from apiver_deps import RawSimulator -from apiver_deps_exception import NonExistentBucket +from apiver_deps_exception import BucketIdNotFound from ..test_base import TestBase @@ -56,7 +56,7 @@ def test_get_bucket_by_id_up_to_v1(self): @pytest.mark.apiver(from_ver=2) def test_get_bucket_by_id_v2(self): self._authorize_account() - with pytest.raises(NonExistentBucket): + with pytest.raises(BucketIdNotFound): self.api.get_bucket_by_id("this id doesn't even exist") created_bucket = self.api.create_bucket('bucket1', 'allPrivate') read_bucket = self.api.get_bucket_by_id(created_bucket.id_)