Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BucketStructure introduced #218

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
449cc8f
BucketStructure introduced
mpnowacki-reef Apr 17, 2022
af80fc3
comment for a peculiar behaviour in BucketFactory
mpnowacki-reef Jun 13, 2022
00544a0
Replace ValueNotSet() with NOT_SET from module scope
agoncharov-reef Jun 16, 2022
8dd8ab1
Make BucketFactory.bucket_structure_from_dict return type configurabl…
agoncharov-reef Jun 16, 2022
06d25e0
Allow any Iterable for create_key instead of List
agoncharov-reef Jun 16, 2022
5e35a71
Update CHANGELOG.md about making `create_key` accept iterable for `ca…
agoncharov-reef Jun 17, 2022
96365d5
Remove staticmethod from BUCKET_CLASS, BUCKET_STRUCTURE_CLASS
agoncharov-reef Jun 17, 2022
65f35bf
Fix BucketStructure cls.NOT_SET -> NOT_SET
agoncharov-reef Jun 17, 2022
8b2d3cf
Improve docstring of BucketStructure
agoncharov-reef Jun 17, 2022
2bdb765
Replication WIP checks
mpnowacki-reef Apr 17, 2022
b2250d9
Refactor replication/check.py
agoncharov-reef Aug 2, 2022
85d4f64
Refactor replication check
agoncharov-reef Aug 3, 2022
b253a99
Even more refactor replication/check.py; update tests
agoncharov-reef Aug 4, 2022
a9c7159
Refactor TwoWayReplicationCheck
agoncharov-reef Aug 4, 2022
4374c27
Fix RawSimulator not deleting key from `all_application_keys` in `del…
agoncharov-reef Aug 4, 2022
92a4cf8
Fix B2Api.get_key() returning wrong key if `key_id` is incorrect
agoncharov-reef Aug 4, 2022
346a24c
Fix Pawel using "it's" instead of "its"
agoncharov-reef Aug 4, 2022
774080d
Fix Michal using list(map(lambda...)) instead of list comprehension
agoncharov-reef Aug 4, 2022
3ce142d
Fix lint issues, update tests
agoncharov-reef Aug 4, 2022
a75267a
Bug fixes; add as_dict()
agoncharov-reef Aug 4, 2022
f264f38
Lint fix
agoncharov-reef Aug 5, 2022
d352d6b
Add is_sse_c_disabled check
agoncharov-reef Aug 15, 2022
fa7aa4b
Add troubleshooter tests
agoncharov-reef Aug 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
* Add `BucketStructure` to hold info about a bucket
* Add `include_existing_files` parameter to `ReplicationSetupHelper`
* Modify `create_key` to accept any iterable for `capabilities`, not only list

## [1.17.3] - 2022-07-15

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions b2sdk/_v3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from b2sdk.api import Services
from b2sdk.bucket import Bucket
from b2sdk.bucket import BucketFactory
from b2sdk.bucket import BucketStructure
from b2sdk.bucket import ValueNotSet
from b2sdk.raw_api import ALL_CAPABILITIES, REALM_URLS

# encryption
Expand Down Expand Up @@ -219,6 +221,12 @@
from b2sdk.replication.monitoring import ReplicationScanResult
from b2sdk.replication.monitoring import ReplicationReport
from b2sdk.replication.monitoring import ReplicationMonitor
from b2sdk.replication.check import TwoWayReplicationCheckGenerator
from b2sdk.replication.check import ReplicationSourceCheck
from b2sdk.replication.check import ReplicationDestinationCheck
from b2sdk.replication.check import TwoWayReplicationCheck
from b2sdk.replication.check import OtherPartyReplicationCheckData
from b2sdk.replication.check import CheckState

# other

Expand Down
14 changes: 9 additions & 5 deletions b2sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
######################################################################

from typing import Optional, Tuple, List, Generator
from contextlib import suppress

from .account_info.abstract import AbstractAccountInfo
from .api_config import B2HttpApiConfig, DEFAULT_HTTP_API_CONFIG
Expand Down Expand Up @@ -533,16 +534,19 @@ def list_keys(self, start_application_key_id: Optional[str] = None

def get_key(self, key_id: str) -> Optional[ApplicationKey]:
"""
Gets information about a single key: it's capabilities, prefix, name etc
Gets information about a single key: its capabilities, prefix, name etc

Returns `None` if the key does not exist.

Raises an exception if profile is not permitted to list keys.
"""
return next(
self.list_keys(start_application_key_id=key_id),
None,
)
with suppress(StopIteration):
key = next(self.list_keys(start_application_key_id=key_id))

# list_keys() may return some other key if `key_id` does not exist;
# thus manually check that we retrieved the right key
if key.id_ == key_id:
return key

# other
def get_file_info(self, file_id: str) -> FileVersion:
Expand Down
214 changes: 202 additions & 12 deletions b2sdk/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

import logging

from typing import Optional, Tuple
from typing import Optional, Tuple, Union, Type

if False:
from b2sdk.api import B2Api

from .encryption.setting import EncryptionSetting, EncryptionSettingFactory
from .encryption.types import EncryptionMode
Expand Down Expand Up @@ -48,16 +51,44 @@
logger = logging.getLogger(__name__)


class Bucket(metaclass=B2TraceMeta):
class ValueNotSet:
"""Sentry class for signifying no value for a property was supplied"""
pass


NOT_SET = ValueNotSet()


class BucketStructure(metaclass=B2TraceMeta):
"""
Provide access to a bucket in B2: listing files, uploading and downloading.
Structure holding all attributes of a bucket.

This structure doesn't hold reference to B2Api, so unlike `Bucket` class
it cannot be used to perform any actions. Instead, this class is used
to only hold Bucket's fields for serializing / deserializing.

Also important difference from `Bucket` is that this structure
allows storing subset of fields, setting others to `ValueNotSet`,
which preserves from serializing too much information.
"""

DEFAULT_CONTENT_TYPE = AUTO_CONTENT_TYPE
id_: Union[str, ValueNotSet]
account_id: Union[str, ValueNotSet]
name: Union[str, ValueNotSet]
type_: Union[str, ValueNotSet]
bucket_info: Union[dict, ValueNotSet]
cors_rules: Union[dict, ValueNotSet]
lifecycle_rules: Union[dict, ValueNotSet]
revision: Union[int, ValueNotSet]
bucket_dict: Union[dict, ValueNotSet]
options_set: Union[set, ValueNotSet]
default_server_side_encryption: Union[EncryptionSetting, ValueNotSet]
default_retention: Union[BucketRetentionSetting, ValueNotSet]
is_file_lock_enabled: Union[Optional[bool], ValueNotSet]
replication: Union[Optional[ReplicationConfiguration], ValueNotSet]

def __init__(
self,
api,
id_,
name=None,
type_=None,
Expand All @@ -73,9 +104,10 @@ def __init__(
default_retention: BucketRetentionSetting = UNKNOWN_BUCKET_RETENTION,
is_file_lock_enabled: Optional[bool] = None,
replication: Optional[ReplicationConfiguration] = None,
*,
account_id,
):
"""
:param b2sdk.v2.B2Api api: an API object
:param str id_: a bucket id
:param str name: a bucket name
:param str type_: a bucket type
Expand All @@ -89,9 +121,10 @@ def __init__(
:param b2sdk.v2.BucketRetentionSetting default_retention: default retention setting
:param bool is_file_lock_enabled: whether file locking is enabled or not
:param b2sdk.v2.ReplicationConfiguration replication: replication rules for the bucket
:param str account_id: id of the account owning the bucket
"""
self.api = api
self.id_ = id_
self.account_id = account_id
self.name = name
self.type_ = type_
self.bucket_info = bucket_info or {}
Expand All @@ -105,6 +138,45 @@ def __init__(
self.is_file_lock_enabled = is_file_lock_enabled
self.replication = replication

def __repr__(self):
return '%s<%s,%s,%s>' % (type(self).__name__, self.id_, self.name, self.type_)


class Bucket(BucketStructure):
"""
Provide access to a bucket in B2: listing files, uploading and downloading.
"""

api: 'B2Api'
id_: str
account_id: str
name: str
type_: str
bucket_info: dict
cors_rules: dict
lifecycle_rules: dict
revision: int
bucket_dict: dict
options_set: set
default_server_side_encryption: EncryptionSetting
default_retention: BucketRetentionSetting
is_file_lock_enabled: Optional[bool]
replication: Optional[ReplicationConfiguration]

DEFAULT_CONTENT_TYPE = AUTO_CONTENT_TYPE

def __init__(
self,
api,
*args,
**kwargs,
):
"""
:param b2sdk.v2.B2Api api: an API object
"""
self.api = api
super().__init__(*args, account_id=self.api.account_info.get_account_id(), **kwargs)

def get_fresh_state(self) -> 'Bucket':
"""
Fetch all the information about this bucket and return a new bucket object.
Expand Down Expand Up @@ -960,15 +1032,13 @@ def as_dict(self):

return result

def __repr__(self):
return 'Bucket<%s,%s,%s>' % (self.id_, self.name, self.type_)


class BucketFactory:
"""
This is a factory for creating bucket objects from different kind of objects.
"""
BUCKET_CLASS = staticmethod(Bucket)
BUCKET_CLASS = Bucket
BUCKET_STRUCTURE_CLASS = BucketStructure

@classmethod
def from_api_response(cls, api, response):
Expand All @@ -982,7 +1052,127 @@ def from_api_response(cls, api, response):
return [cls.from_api_bucket_dict(api, bucket_dict) for bucket_dict in response['buckets']]

@classmethod
def from_api_bucket_dict(cls, api, bucket_dict):
def bucket_structure_from_dict(cls, bucket_dict) -> Type[BUCKET_STRUCTURE_CLASS]:
"""
Turn a dictionary, like this:

.. code-block:: python

{
"bucketType": "allPrivate",
"accountId": "0991231",
"bucketId": "a4ba6a39d8b6b5fd561f0010",
"bucketName": "zsdfrtsazsdfafr",
"accountId": "4aa9865d6f00",
"bucketInfo": {},
"options": [],
"revision": 1,
"defaultServerSideEncryption": {
"isClientAuthorizedToRead" : true,
"value": {
"algorithm" : "AES256",
"mode" : "SSE-B2"
}
},
"fileLockConfiguration": {
"isClientAuthorizedToRead": true,
"value": {
"defaultRetention": {
"mode": null,
"period": null
},
"isFileLockEnabled": false
}
},
"replicationConfiguration": {
"clientIsAllowedToRead": true,
"value": {
"asReplicationSource": {
"replicationRules": [
{
"destinationBucketId": "c5f35d53a90a7ea284fb0719",
"fileNamePrefix": "",
"includeExistingFiles": True,
"isEnabled": true,
"priority": 1,
"replicationRuleName": "replication-us-west"
},
{
"destinationBucketId": "55f34d53a96a7ea284fb0719",
"fileNamePrefix": "",
"includeExistingFiles": True,
"isEnabled": true,
"priority": 2,
"replicationRuleName": "replication-us-west-2"
}
],
"sourceApplicationKeyId": "10053d55ae26b790000000006"
},
"asReplicationDestination": {
"sourceToDestinationKeyMapping": {
"10053d55ae26b790000000045": "10053d55ae26b790000000004",
"10053d55ae26b790000000046": "10053d55ae26b790030000004"
}
}
}
}
}

into a BucketStructure object.

:param dict bucket_dict: a dictionary with bucket properties
:rtype: BucketStructure

"""
type_ = bucket_dict.get('bucketType', NOT_SET)
bucket_name = bucket_dict.get('bucketName', NOT_SET)
bucket_id = bucket_dict.get('bucketId', NOT_SET)
bucket_info = bucket_dict.get('bucketInfo', NOT_SET)
cors_rules = bucket_dict.get('corsRules', NOT_SET)
lifecycle_rules = bucket_dict.get('lifecycleRules', NOT_SET)
revision = bucket_dict.get('revision', NOT_SET)
options = set(bucket_dict['options']) if 'options' in bucket_dict else NOT_SET
account_id = bucket_dict.get('accountId', NOT_SET)

# The existence of these complex settings is checked below, instead of inside of their respective factory
# classes, because those would either break or return objects indistinguishable from objects representing
# insufficient permission to read set values.
default_server_side_encryption = (
EncryptionSettingFactory.from_bucket_dict(bucket_dict)
if EncryptionSettingFactory.TOP_LEVEL_KEY in bucket_dict else NOT_SET
)
replication = (
ReplicationConfigurationFactory.from_bucket_dict(bucket_dict).value
if ReplicationConfigurationFactory.TOP_LEVEL_KEY in bucket_dict else NOT_SET
)

if FileLockConfiguration.TOP_LEVEL_KEY in bucket_dict:
agoncharov-reef marked this conversation as resolved.
Show resolved Hide resolved
file_lock_configuration = FileLockConfiguration.from_bucket_dict(bucket_dict)
default_retention = file_lock_configuration.default_retention
is_file_lock_enabled = file_lock_configuration.is_file_lock_enabled
else:
default_retention = NOT_SET
is_file_lock_enabled = NOT_SET

return cls.BUCKET_STRUCTURE_CLASS(
bucket_id,
bucket_name,
type_,
bucket_info,
cors_rules,
lifecycle_rules,
revision,
bucket_dict,
options,
default_server_side_encryption,
default_retention,
is_file_lock_enabled,
replication,
account_id=account_id,
)

@classmethod
def from_api_bucket_dict(cls, api, bucket_dict) -> Type[BUCKET_CLASS]:
"""
Turn a dictionary, like this:

Expand Down
3 changes: 2 additions & 1 deletion b2sdk/encryption/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ def __repr__(self):


class EncryptionSettingFactory:
TOP_LEVEL_KEY = 'defaultServerSideEncryption'
# 2021-03-17: for the bucket the response of the server is:
# if authorized to read:
# "mode": "none"
Expand Down Expand Up @@ -301,7 +302,7 @@ def from_bucket_dict(cls, bucket_dict: dict) -> Optional[EncryptionSetting]:

"""
default_sse = bucket_dict.get(
'defaultServerSideEncryption',
cls.TOP_LEVEL_KEY,
{'isClientAuthorizedToRead': False},
)

Expand Down
9 changes: 6 additions & 3 deletions b2sdk/file_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ def as_dict(self):
}
if self.period is not None:
result['period'] = self.period.as_dict()
else:
result['period'] = None
return result

def serialize_to_json_for_request(self):
Expand All @@ -301,6 +303,7 @@ def __repr__(self):
class FileLockConfiguration:
"""Represent bucket's file lock configuration, i.e. whether the file lock mechanism is enabled and default
file retention"""
TOP_LEVEL_KEY = 'fileLockConfiguration'

def __init__(
self,
Expand Down Expand Up @@ -339,12 +342,12 @@ def from_bucket_dict(cls, bucket_dict):
}
"""

if not bucket_dict['fileLockConfiguration']['isClientAuthorizedToRead']:
if not bucket_dict[cls.TOP_LEVEL_KEY]['isClientAuthorizedToRead']:
return cls(UNKNOWN_BUCKET_RETENTION, None)
retention = BucketRetentionSetting.from_bucket_retention_dict(
bucket_dict['fileLockConfiguration']['value']['defaultRetention']
bucket_dict[cls.TOP_LEVEL_KEY]['value']['defaultRetention']
)
is_file_lock_enabled = bucket_dict['fileLockConfiguration']['value']['isFileLockEnabled']
is_file_lock_enabled = bucket_dict[cls.TOP_LEVEL_KEY]['value']['isFileLockEnabled']
return cls(retention, is_file_lock_enabled)

def as_dict(self):
Expand Down
Loading