Skip to content

Commit f39fc5d

Browse files
authored
Merge branch 'master' into dependabot/pip/sphinx-lt-6.0
2 parents 2461511 + 62c9a1b commit f39fc5d

File tree

11 files changed

+199
-143
lines changed

11 files changed

+199
-143
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
fail-fast: false
8181
matrix:
8282
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
83-
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"]
83+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-beta.1", "pypy-3.7", "pypy-3.8"]
8484
exclude:
8585
- os: "macos-latest"
8686
python-version: "pypy-3.7"

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
* Add `include_existing_files` parameter to `ReplicationSetupHelper`
11+
12+
### Fixed
13+
* Fix `AccountInfo.is_master_key()`
14+
* Fix docstring of `SqliteAccountInfo`
15+
916
### Infrastructure
17+
* Add 3.11.0-beta.1 to CI
1018
* Change Sphinx major version from 5 to 6
1119

1220
## [1.16.0] - 2022-04-27

b2sdk/account_info/abstract.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,17 @@ def is_same_account(self, account_id: str, realm: str) -> bool:
127127
return False
128128

129129
def is_master_key(self) -> bool:
130-
return self.get_account_id() == self.get_application_key_id()
130+
application_key_id = self.get_application_key_id()
131+
account_id = self.get_account_id()
132+
new_style_master_key_suffix = '0000000000'
133+
if account_id == application_key_id:
134+
return True # old style
135+
if len(application_key_id
136+
) == (3 + len(account_id) + len(new_style_master_key_suffix)): # 3 for cluster id
137+
# new style
138+
if application_key_id.endswith(account_id + new_style_master_key_suffix):
139+
return True
140+
return False
131141

132142
@abstractmethod
133143
def get_account_id(self):

b2sdk/account_info/sqlite_account_info.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ def __init__(self, file_name=None, last_upgrade_to_run=None, profile: Optional[s
5353
SqliteAccountInfo currently checks locations in the following order:
5454
5555
If ``profile`` arg is provided:
56+
5657
* ``{XDG_CONFIG_HOME_ENV_VAR}/b2/db-<profile>.sqlite``, if ``{XDG_CONFIG_HOME_ENV_VAR}`` env var is set
5758
* ``{B2_ACCOUNT_INFO_PROFILE_FILE}``
5859
5960
Otherwise:
61+
6062
* ``file_name``, if truthy
6163
* ``{B2_ACCOUNT_INFO_ENV_VAR}`` env var's value, if set
6264
* ``{B2_ACCOUNT_INFO_DEFAULT_FILE}``, if it exists

b2sdk/replication/setting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ReplicationRule:
3333

3434
REPLICATION_RULE_REGEX: ClassVar = re.compile(r'^[a-zA-Z0-9_\-]{1,64}$')
3535
MIN_PRIORITY: ClassVar[int] = 1
36-
MAX_PRIORITY: ClassVar[int] = 2147483647
36+
MAX_PRIORITY: ClassVar[int] = 2**31 - 1
3737

3838
def __post_init__(self):
3939
if not self.destination_bucket_id:

b2sdk/replication/setup.py

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# b2 replication-accept destinationBucketName sourceKeyId [destinationKeyId]
1818
# b2 replication-deny destinationBucketName sourceKeyId
1919

20+
from collections.abc import Iterable
2021
from typing import ClassVar, List, Optional, Tuple
2122
import itertools
2223
import logging
@@ -29,9 +30,14 @@
2930

3031
logger = logging.getLogger(__name__)
3132

33+
try:
34+
Iterable[str]
35+
except TypeError:
36+
Iterable = List # Remove after dropping Python 3.8
37+
3238

3339
class ReplicationSetupHelper(metaclass=B2TraceMeta):
34-
""" class with various methods that help with repliction management """
40+
""" class with various methods that help with setting up repliction """
3541
PRIORITY_OFFSET: ClassVar[int] = 5 #: how far to to put the new rule from the existing rules
3642
DEFAULT_PRIORITY: ClassVar[
3743
int
@@ -50,39 +56,38 @@ class ReplicationSetupHelper(metaclass=B2TraceMeta):
5056
'deleteFiles',
5157
)
5258

53-
def __init__(self, source_b2api: B2Api = None, destination_b2api: B2Api = None):
54-
assert source_b2api is not None or destination_b2api is not None
55-
self.source_b2api = source_b2api
56-
self.destination_b2api = destination_b2api
57-
5859
def setup_both(
5960
self,
60-
source_bucket_name: str,
61+
source_bucket: Bucket,
6162
destination_bucket: Bucket,
6263
name: Optional[str] = None, #: name for the new replication rule
6364
priority: int = None, #: priority for the new replication rule
6465
prefix: Optional[str] = None,
66+
include_existing_files: bool = False,
6567
) -> Tuple[Bucket, Bucket]:
66-
source_bucket = self.setup_source(
67-
source_bucket_name,
68-
prefix,
68+
69+
new_source_bucket = self.setup_source(
70+
source_bucket,
6971
destination_bucket,
72+
prefix,
7073
name,
7174
priority,
75+
include_existing_files,
7276
)
73-
destination_bucket = self.setup_destination(
74-
source_bucket.replication.as_replication_source.source_application_key_id,
77+
78+
new_destination_bucket = self.setup_destination(
79+
new_source_bucket.replication.as_replication_source.source_application_key_id,
7580
destination_bucket,
7681
)
77-
return source_bucket, destination_bucket
82+
83+
return new_source_bucket, new_destination_bucket
7884

7985
def setup_destination(
8086
self,
8187
source_key_id: str,
8288
destination_bucket: Bucket,
8389
) -> Bucket:
8490
api: B2Api = destination_bucket.api
85-
destination_bucket = api.list_buckets(destination_bucket.name)[0] # fresh!
8691
if destination_bucket.replication is None or destination_bucket.replication.as_replication_source is None:
8792
source_configuration = None
8893
else:
@@ -145,28 +150,27 @@ def _get_destination_key(
145150
logger.debug("no matching key found, making a new one")
146151
key = cls._create_destination_key(
147152
name=destination_bucket.name[:91] + '-replidst',
148-
api=api,
149-
bucket_id=destination_bucket.id_,
153+
bucket=destination_bucket,
150154
prefix=None,
151155
)
152156
return keys_to_purge, key
153157

154158
def setup_source(
155159
self,
156-
source_bucket_name,
157-
prefix,
160+
source_bucket: Bucket,
158161
destination_bucket: Bucket,
159-
name,
160-
priority,
162+
prefix: Optional[str] = None,
163+
name: Optional[str] = None, #: name for the new replication rule
164+
priority: int = None, #: priority for the new replication rule
165+
include_existing_files: bool = False,
161166
) -> Bucket:
162-
source_bucket: Bucket = self.source_b2api.list_buckets(source_bucket_name)[0] # fresh!
163167
if prefix is None:
164168
prefix = ""
165169

166170
try:
167-
current_source_rrs = source_bucket.replication.as_replication_source.rules
171+
current_source_rules = source_bucket.replication.as_replication_source.rules
168172
except (NameError, AttributeError):
169-
current_source_rrs = []
173+
current_source_rules = []
170174
try:
171175
destination_configuration = source_bucket.replication.as_replication_destination
172176
except (NameError, AttributeError):
@@ -176,27 +180,28 @@ def setup_source(
176180
source_bucket,
177181
prefix,
178182
source_bucket.replication,
179-
current_source_rrs,
183+
current_source_rules,
180184
)
181185
priority = self._get_priority_for_new_rule(
186+
current_source_rules,
182187
priority,
183-
current_source_rrs,
184188
)
185-
name = self._get_name_for_new_rule(
189+
name = self._get_new_rule_name(
190+
current_source_rules,
191+
destination_bucket,
186192
name,
187-
current_source_rrs,
188-
destination_bucket.name,
189193
)
190194
new_rr = ReplicationRule(
191195
name=name,
192196
priority=priority,
193197
destination_bucket_id=destination_bucket.id_,
194198
file_name_prefix=prefix,
199+
include_existing_files=include_existing_files,
195200
)
196201
new_replication_configuration = ReplicationConfiguration(
197202
ReplicationSourceConfiguration(
198203
source_application_key_id=source_key.id_,
199-
rules=current_source_rrs + [new_rr],
204+
rules=current_source_rules + [new_rr],
200205
),
201206
destination_configuration,
202207
)
@@ -208,10 +213,10 @@ def setup_source(
208213
@classmethod
209214
def _get_source_key(
210215
cls,
211-
source_bucket,
212-
prefix,
216+
source_bucket: Bucket,
217+
prefix: str,
213218
current_replication_configuration: ReplicationConfiguration,
214-
current_source_rrs,
219+
current_source_rules: Iterable[ReplicationRule],
215220
) -> ApplicationKey:
216221
api = source_bucket.api
217222

@@ -227,9 +232,9 @@ def _get_source_key(
227232

228233
new_key = cls._create_source_key(
229234
name=source_bucket.name[:91] + '-replisrc',
230-
api=api,
231-
bucket_id=source_bucket.id_,
232-
) # no prefix!
235+
bucket=source_bucket,
236+
prefix=prefix,
237+
)
233238
return new_key
234239

235240
@classmethod
@@ -269,67 +274,74 @@ def _should_make_new_source_key(
269274
def _create_source_key(
270275
cls,
271276
name: str,
272-
api: B2Api,
273-
bucket_id: str,
277+
bucket: Bucket,
274278
prefix: Optional[str] = None,
275279
) -> ApplicationKey:
280+
# in this implementation we ignore the prefix and create a full key, because
281+
# if someone would need a different (wider) key later, all replication
282+
# destinations would have to start using new keys and it's not feasible
283+
# from organizational perspective, while the prefix of uploaded files can be
284+
# restricted on the rule level
285+
prefix = None
276286
capabilities = cls.DEFAULT_SOURCE_CAPABILITIES
277-
return cls._create_key(name, api, bucket_id, prefix, capabilities)
287+
return cls._create_key(name, bucket, prefix, capabilities)
278288

279289
@classmethod
280290
def _create_destination_key(
281291
cls,
282292
name: str,
283-
api: B2Api,
284-
bucket_id: str,
293+
bucket: Bucket,
285294
prefix: Optional[str] = None,
286295
) -> ApplicationKey:
287296
capabilities = cls.DEFAULT_DESTINATION_CAPABILITIES
288-
return cls._create_key(name, api, bucket_id, prefix, capabilities)
297+
return cls._create_key(name, bucket, prefix, capabilities)
289298

290299
@classmethod
291300
def _create_key(
292301
cls,
293302
name: str,
294-
api: B2Api,
295-
bucket_id: str,
303+
bucket: Bucket,
296304
prefix: Optional[str] = None,
297305
capabilities=tuple(),
298306
) -> ApplicationKey:
307+
api: B2Api = bucket.api
299308
return api.create_key(
300309
capabilities=capabilities,
301310
key_name=name,
302-
bucket_id=bucket_id,
311+
bucket_id=bucket.id_,
303312
name_prefix=prefix,
304313
)
305314

306315
@classmethod
307-
def _get_narrowest_common_prefix(cls, widen_to: List[str]) -> str:
308-
for path in widen_to:
309-
pass # TODO
310-
return ''
311-
312-
@classmethod
313-
def _get_priority_for_new_rule(cls, priority, current_source_rrs):
314-
# if there is no priority hint, look into current rules to determine the last priority and add a constant to it
316+
def _get_priority_for_new_rule(
317+
cls,
318+
current_rules: Iterable[ReplicationRule],
319+
priority: Optional[int] = None,
320+
):
315321
if priority is not None:
316322
return priority
317-
if current_source_rrs:
318-
# TODO: maybe handle a case where the existing rrs need to have their priorities decreased to make space
319-
existing_priority = max(rr.priority for rr in current_source_rrs)
323+
if current_rules:
324+
# ignore a case where the existing rrs need to have their priorities decreased to make space (max is 2**31-1)
325+
existing_priority = max(rr.priority for rr in current_rules)
320326
return min(existing_priority + cls.PRIORITY_OFFSET, cls.MAX_PRIORITY)
321327
return cls.DEFAULT_PRIORITY
322328

323329
@classmethod
324-
def _get_name_for_new_rule(
325-
cls, name: Optional[str], current_source_rrs, destination_bucket_name
330+
def _get_new_rule_name(
331+
cls,
332+
current_rules: Iterable[ReplicationRule],
333+
destination_bucket: Bucket,
334+
name: Optional[str] = None,
326335
):
327336
if name is not None:
328337
return name
329-
existing_names = set(rr.name for rr in current_source_rrs)
338+
existing_names = set(rr.name for rr in current_rules)
330339
suffixes = cls._get_rule_name_candidate_suffixes()
331340
while True:
332-
candidate = '%s%s' % (destination_bucket_name, next(suffixes))
341+
candidate = '%s%s' % (
342+
destination_bucket.name,
343+
next(suffixes),
344+
) # use := after dropping 3.7
333345
if candidate not in existing_names:
334346
return candidate
335347

noxfile.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
NOX_PYTHONS = os.environ.get('NOX_PYTHONS')
1919
SKIP_COVERAGE = os.environ.get('SKIP_COVERAGE') == 'true'
2020

21-
PYTHON_VERSIONS = ['3.7', '3.8', '3.9', '3.10'] if NOX_PYTHONS is None else NOX_PYTHONS.split(',')
21+
PYTHON_VERSIONS = [
22+
'3.7',
23+
'3.8',
24+
'3.9',
25+
'3.10',
26+
'3.11',
27+
] if NOX_PYTHONS is None else NOX_PYTHONS.split(',')
28+
2229
PYTHON_DEFAULT_VERSION = PYTHON_VERSIONS[-1]
2330

2431
PY_PATHS = ['b2sdk', 'test', 'noxfile.py', 'setup.py']

0 commit comments

Comments
 (0)