Skip to content

Commit cdba79b

Browse files
Merge branch 'master' into add_ls_wildcard
* master: pep8 fix Lint fix PR fixes Changelog updated advanced.rst updated after codespell Tests ensuring that list bucket is not called if authorised for bucket Additional documentation for returning empty responses API takes care of managing bucket cache Forcing a single response from empty list bucket to ensure multiple empty entries Fix for typo in documentation (thanks to codespell) Lint and comment fix Additional tests for listing files / versions WIP
2 parents 2c3eafa + f7c1626 commit cdba79b

File tree

7 files changed

+176
-27
lines changed

7 files changed

+176
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Added
10+
* Authorizing a key for a single bucket ensures that this bucket is cached
1011
* `Bucket.ls` operation supports wildcard matching strings
1112

1213
### Infrastructure
14+
* Additional tests for listing files/versions
1315
* Ensured that changelog validation only happens on pull requests
1416
* Upgraded GitHub actions checkout to v3, python-setup to v4
1517

b2sdk/_v3/exception.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from b2sdk.exception import NotAllowedByAppKeyError
5555
from b2sdk.exception import PartSha1Mismatch
5656
from b2sdk.exception import RestrictedBucket
57+
from b2sdk.exception import RestrictedBucketMissing
5758
from b2sdk.exception import RetentionWriteError
5859
from b2sdk.exception import SSECKeyError
5960
from b2sdk.exception import SSECKeyIdMismatchInCopy
@@ -134,6 +135,7 @@
134135
'NotAllowedByAppKeyError',
135136
'PartSha1Mismatch',
136137
'RestrictedBucket',
138+
'RestrictedBucketMissing',
137139
'RetentionWriteError',
138140
'ServiceError',
139141
'SourceReplicationConflict',

b2sdk/api.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
from contextlib import suppress
1313

1414
from .account_info.abstract import AbstractAccountInfo
15+
from .account_info.exception import MissingAccountData
1516
from .api_config import B2HttpApiConfig, DEFAULT_HTTP_API_CONFIG
1617
from .application_key import ApplicationKey, BaseApplicationKey, FullApplicationKey
1718
from .cache import AbstractCache
1819
from .bucket import Bucket, BucketFactory
1920
from .encryption.setting import EncryptionSetting
2021
from .replication.setting import ReplicationConfiguration
21-
from .exception import BucketIdNotFound, NonExistentBucket, RestrictedBucket
22+
from .exception import BucketIdNotFound, NonExistentBucket, RestrictedBucket, RestrictedBucketMissing
2223
from .file_lock import FileRetentionSetting, LegalHold
2324
from .file_version import DownloadVersionFactory, FileIdAndName, FileVersion, FileVersionFactory
2425
from .large_file.services import LargeFileServices
@@ -201,6 +202,7 @@ def authorize_account(self, realm, application_key_id, application_key):
201202
:param str application_key: user's :term:`application key`
202203
"""
203204
self.session.authorize_account(realm, application_key_id, application_key)
205+
self._populate_bucket_cache_from_key()
204206

205207
def get_account_id(self):
206208
"""
@@ -595,3 +597,23 @@ def _check_bucket_restrictions(self, key, value):
595597
if allowed_bucket_identifier is not None:
596598
if allowed_bucket_identifier != value:
597599
raise RestrictedBucket(allowed_bucket_identifier)
600+
601+
def _populate_bucket_cache_from_key(self):
602+
# If the key is restricted to the bucket, pre-populate the cache with it
603+
try:
604+
allowed = self.account_info.get_allowed()
605+
except MissingAccountData:
606+
return
607+
608+
allowed_bucket_id = allowed.get('bucketId')
609+
if allowed_bucket_id is None:
610+
return
611+
612+
allowed_bucket_name = allowed.get('bucketName')
613+
614+
# If we have bucketId set we still need to check bucketName. If the bucketName is None,
615+
# it means that the bucketId belongs to a bucket that was already removed.
616+
if allowed_bucket_name is None:
617+
raise RestrictedBucketMissing()
618+
619+
self.cache.save_bucket(self.BUCKET_CLASS(self, allowed_bucket_id, name=allowed_bucket_name))

b2sdk/exception.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(self, *args, **kwargs):
3939
# If the exception is caused by a b2 server response,
4040
# the server MAY have included instructions to pause the thread before issuing any more requests
4141
self.retry_after_seconds = None
42-
super(B2Error, self).__init__(*args, **kwargs)
42+
super().__init__(*args, **kwargs)
4343

4444
@property
4545
def prefix(self):
@@ -82,7 +82,7 @@ class B2SimpleError(B2Error, metaclass=ABCMeta):
8282
"""
8383

8484
def __str__(self):
85-
return '%s: %s' % (self.prefix, super(B2SimpleError, self).__str__())
85+
return '%s: %s' % (self.prefix, super().__str__())
8686

8787

8888
class NotAllowedByAppKeyError(B2SimpleError, metaclass=ABCMeta):
@@ -134,7 +134,7 @@ class CapabilityNotAllowed(NotAllowedByAppKeyError):
134134

135135
class ChecksumMismatch(TransientErrorMixin, B2Error):
136136
def __init__(self, checksum_type, expected, actual):
137-
super(ChecksumMismatch, self).__init__()
137+
super().__init__()
138138
self.checksum_type = checksum_type
139139
self.expected = expected
140140
self.actual = actual
@@ -168,7 +168,7 @@ def __init__(self, clock_skew_seconds):
168168
"""
169169
:param int clock_skew_seconds: The difference: local_clock - server_clock
170170
"""
171-
super(ClockSkew, self).__init__()
171+
super().__init__()
172172
self.clock_skew_seconds = clock_skew_seconds
173173

174174
def __str__(self):
@@ -210,7 +210,7 @@ def should_retry_http(self):
210210

211211
class DestFileNewer(B2Error):
212212
def __init__(self, dest_path, source_path, dest_prefix, source_prefix):
213-
super(DestFileNewer, self).__init__()
213+
super().__init__()
214214
self.dest_path = dest_path
215215
self.source_path = source_path
216216
self.dest_prefix = dest_prefix
@@ -240,7 +240,7 @@ class ResourceNotFound(B2SimpleError):
240240

241241
class FileOrBucketNotFound(ResourceNotFound):
242242
def __init__(self, bucket_name=None, file_id_or_name=None):
243-
super(FileOrBucketNotFound, self).__init__()
243+
super().__init__()
244244
self.bucket_name = bucket_name
245245
self.file_id_or_name = file_id_or_name
246246

@@ -291,7 +291,7 @@ class SSECKeyIdMismatchInCopy(InvalidMetadataDirective):
291291

292292
class InvalidRange(B2Error):
293293
def __init__(self, content_length, range_):
294-
super(InvalidRange, self).__init__()
294+
super().__init__()
295295
self.content_length = content_length
296296
self.range_ = range_
297297

@@ -310,7 +310,7 @@ class InvalidUploadSource(B2SimpleError):
310310

311311
class BadRequest(B2Error):
312312
def __init__(self, message, code):
313-
super(BadRequest, self).__init__()
313+
super().__init__()
314314
self.message = message
315315
self.code = code
316316

@@ -326,7 +326,7 @@ def __init__(self, message, code, size: int):
326326

327327
class Unauthorized(B2Error):
328328
def __init__(self, message, code):
329-
super(Unauthorized, self).__init__()
329+
super().__init__()
330330
self.message = message
331331
self.code = code
332332

@@ -350,22 +350,29 @@ class InvalidAuthToken(Unauthorized):
350350
"""
351351

352352
def __init__(self, message, code):
353-
super(InvalidAuthToken,
354-
self).__init__('Invalid authorization token. Server said: ' + message, code)
353+
super().__init__('Invalid authorization token. Server said: ' + message, code)
355354

356355

357356
class RestrictedBucket(B2Error):
358357
def __init__(self, bucket_name):
359-
super(RestrictedBucket, self).__init__()
358+
super().__init__()
360359
self.bucket_name = bucket_name
361360

362361
def __str__(self):
363362
return 'Application key is restricted to bucket: %s' % self.bucket_name
364363

365364

365+
class RestrictedBucketMissing(RestrictedBucket):
366+
def __init__(self):
367+
super().__init__('')
368+
369+
def __str__(self):
370+
return 'Application key is restricted to a bucket that doesn\'t exist'
371+
372+
366373
class MaxFileSizeExceeded(B2Error):
367374
def __init__(self, size, max_allowed_size):
368-
super(MaxFileSizeExceeded, self).__init__()
375+
super().__init__()
369376
self.size = size
370377
self.max_allowed_size = max_allowed_size
371378

@@ -378,7 +385,7 @@ def __str__(self):
378385

379386
class MaxRetriesExceeded(B2Error):
380387
def __init__(self, limit, exception_info_list):
381-
super(MaxRetriesExceeded, self).__init__()
388+
super().__init__()
382389
self.limit = limit
383390
self.exception_info_list = exception_info_list
384391

@@ -405,7 +412,7 @@ class FileSha1Mismatch(B2SimpleError):
405412

406413
class PartSha1Mismatch(B2Error):
407414
def __init__(self, key):
408-
super(PartSha1Mismatch, self).__init__()
415+
super().__init__()
409416
self.key = key
410417

411418
def __str__(self):
@@ -435,7 +442,7 @@ def __str__(self):
435442

436443
class TooManyRequests(B2Error):
437444
def __init__(self, retry_after_seconds=None):
438-
super(TooManyRequests, self).__init__()
445+
super().__init__()
439446
self.retry_after_seconds = retry_after_seconds
440447

441448
def __str__(self):
@@ -447,7 +454,7 @@ def should_retry_http(self):
447454

448455
class TruncatedOutput(TransientErrorMixin, B2Error):
449456
def __init__(self, bytes_read, file_size):
450-
super(TruncatedOutput, self).__init__()
457+
super().__init__()
451458
self.bytes_read = bytes_read
452459
self.file_size = file_size
453460

@@ -482,7 +489,7 @@ def __str__(self):
482489

483490
class UploadTokenUsedConcurrently(B2Error):
484491
def __init__(self, token):
485-
super(UploadTokenUsedConcurrently, self).__init__()
492+
super().__init__()
486493
self.token = token
487494

488495
def __str__(self):

b2sdk/raw_simulator.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import threading
1717
import time
1818

19-
from contextlib import contextmanager
19+
from contextlib import contextmanager, suppress
2020
from typing import Optional
2121

2222
from b2sdk.http_constants import FILE_INFO_HEADER_PREFIX, HEX_DIGITS_AT_END
@@ -91,6 +91,7 @@ def __init__(
9191
self.capabilities = capabilities
9292
self.expiration_timestamp_or_none = expiration_timestamp_or_none
9393
self.bucket_id_or_none = bucket_id_or_none
94+
self.bucket_name_or_none = bucket_name_or_none
9495
self.name_prefix_or_none = name_prefix_or_none
9596

9697
def as_key(self):
@@ -121,6 +122,7 @@ def get_allowed(self):
121122
"""
122123
return dict(
123124
bucketId=self.bucket_id_or_none,
125+
bucketName=self.bucket_name_or_none,
124126
capabilities=self.capabilities,
125127
namePrefix=self.name_prefix_or_none,
126128
)
@@ -1305,10 +1307,13 @@ def create_key(
13051307
self.app_key_counter += 1
13061308
application_key_id = 'appKeyId%d' % (index,)
13071309
app_key = 'appKey%d' % (index,)
1308-
if bucket_id is None:
1309-
bucket_name_or_none = None
1310-
else:
1311-
bucket_name_or_none = self._get_bucket_by_id(bucket_id).bucket_name
1310+
bucket_name_or_none = None
1311+
if bucket_id is not None:
1312+
# It is possible for bucketId to be filled and bucketName to be empty.
1313+
# It can happen when the bucket was deleted.
1314+
with suppress(NonExistentBucket):
1315+
bucket_name_or_none = self._get_bucket_by_id(bucket_id).bucket_name
1316+
13121317
key_sim = KeySimulator(
13131318
account_id=account_id,
13141319
name=key_name,

b2sdk/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def authorize_account(self, realm, application_key_id, application_key):
131131
realm=realm,
132132
s3_api_url=response['s3ApiUrl'],
133133
allowed=allowed,
134-
application_key_id=application_key_id
134+
application_key_id=application_key_id,
135135
)
136136

137137
def cancel_large_file(self, file_id):

0 commit comments

Comments
 (0)