Skip to content

Commit a223ab8

Browse files
authored
Merge pull request #314 from reef-technologies/sqlite-account-info-profiles
Allow SqliteAccountInfo to use different profile files
2 parents d1a9027 + 6b78914 commit a223ab8

File tree

5 files changed

+128
-24
lines changed

5 files changed

+128
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
* Add an option to set a custom file version class to FileVersionFactory
2323
* Add an option for B2Api to turn off hash checking for downloaded files
2424
* Add an option for B2Api to set write buffer size for DownloadedFile.save_to method
25+
* Add support for multiple profile files for SqliteAccountInfo
2526

2627
### Fixed
2728
* Fix copying objects larger than 1TB

b2sdk/_v3/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
from b2sdk.account_info.abstract import AbstractAccountInfo
3535
from b2sdk.account_info.in_memory import InMemoryAccountInfo
3636
from b2sdk.account_info.sqlite_account_info import SqliteAccountInfo
37-
from b2sdk.account_info.sqlite_account_info import B2_ACCOUNT_INFO_ENV_VAR, B2_ACCOUNT_INFO_DEFAULT_FILE, XDG_CONFIG_HOME_ENV_VAR
37+
from b2sdk.account_info.sqlite_account_info import B2_ACCOUNT_INFO_ENV_VAR
38+
from b2sdk.account_info.sqlite_account_info import B2_ACCOUNT_INFO_DEFAULT_FILE
39+
from b2sdk.account_info.sqlite_account_info import B2_ACCOUNT_INFO_PROFILE_FILE
40+
from b2sdk.account_info.sqlite_account_info import XDG_CONFIG_HOME_ENV_VAR
3841
from b2sdk.account_info.stub import StubAccountInfo
3942
from b2sdk.account_info.upload_url_pool import UploadUrlPool
4043
from b2sdk.account_info.upload_url_pool import UrlPoolAccountInfo

b2sdk/account_info/sqlite_account_info.py

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@
1111
import json
1212
import logging
1313
import os
14+
import re
15+
import sqlite3
1416
import stat
1517
import threading
16-
from typing import Optional, List
1718

18-
from .exception import (CorruptAccountInfo, MissingAccountData)
19-
from .upload_url_pool import UrlPoolAccountInfo
19+
from typing import List, Optional
2020

21-
import sqlite3
21+
from .exception import CorruptAccountInfo, MissingAccountData
22+
from .upload_url_pool import UrlPoolAccountInfo
2223

2324
logger = logging.getLogger(__name__)
2425

2526
B2_ACCOUNT_INFO_ENV_VAR = 'B2_ACCOUNT_INFO'
26-
B2_ACCOUNT_INFO_DEFAULT_FILE = '~/.b2_account_info'
27+
B2_ACCOUNT_INFO_DEFAULT_FILE = os.path.join('~', '.b2_account_info')
28+
B2_ACCOUNT_INFO_PROFILE_FILE = os.path.join('~', '.b2db-{profile}.sqlite')
29+
B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP = re.compile(r'[a-zA-Z0-9_\-]{1,64}')
2730
XDG_CONFIG_HOME_ENV_VAR = 'XDG_CONFIG_HOME'
2831

2932
DEFAULT_ABSOLUTE_MINIMUM_PART_SIZE = 5000000 # this value is used ONLY in migrating db, and in v1 wrapper, it is not
@@ -39,7 +42,7 @@ class SqliteAccountInfo(UrlPoolAccountInfo):
3942
completed.
4043
"""
4144

42-
def __init__(self, file_name=None, last_upgrade_to_run=None):
45+
def __init__(self, file_name=None, last_upgrade_to_run=None, profile: Optional[str] = None):
4346
"""
4447
Initialize SqliteAccountInfo.
4548
@@ -49,6 +52,11 @@ def __init__(self, file_name=None, last_upgrade_to_run=None):
4952
5053
SqliteAccountInfo currently checks locations in the following order:
5154
55+
If ``profile`` arg is provided:
56+
* ``{XDG_CONFIG_HOME_ENV_VAR}/b2/db-<profile>.sqlite``, if ``{XDG_CONFIG_HOME_ENV_VAR}`` env var is set
57+
* ``{B2_ACCOUNT_INFO_PROFILE_FILE}``
58+
59+
Otherwise:
5260
* ``file_name``, if truthy
5361
* ``{B2_ACCOUNT_INFO_ENV_VAR}`` env var's value, if set
5462
* ``{B2_ACCOUNT_INFO_DEFAULT_FILE}``, if it exists
@@ -62,21 +70,7 @@ def __init__(self, file_name=None, last_upgrade_to_run=None):
6270
"""
6371
self.thread_local = threading.local()
6472

65-
if file_name:
66-
user_account_info_path = file_name
67-
elif B2_ACCOUNT_INFO_ENV_VAR in os.environ:
68-
user_account_info_path = os.environ[B2_ACCOUNT_INFO_ENV_VAR]
69-
elif os.path.exists(os.path.expanduser(B2_ACCOUNT_INFO_DEFAULT_FILE)):
70-
user_account_info_path = B2_ACCOUNT_INFO_DEFAULT_FILE
71-
elif XDG_CONFIG_HOME_ENV_VAR in os.environ:
72-
config_home = os.environ[XDG_CONFIG_HOME_ENV_VAR]
73-
user_account_info_path = os.path.join(config_home, 'b2', 'account_info')
74-
if not os.path.exists(os.path.join(config_home, 'b2')):
75-
os.makedirs(os.path.join(config_home, 'b2'), mode=0o755)
76-
else:
77-
user_account_info_path = B2_ACCOUNT_INFO_DEFAULT_FILE
78-
79-
self.filename = os.path.expanduser(user_account_info_path)
73+
self.filename = self._get_user_account_info_path(file_name=file_name, profile=profile)
8074
logger.debug('%s file path to use: %s', self.__class__.__name__, self.filename)
8175

8276
self._validate_database(last_upgrade_to_run)
@@ -90,10 +84,42 @@ def __init__(self, file_name=None, last_upgrade_to_run=None):
9084
**dict(
9185
B2_ACCOUNT_INFO_ENV_VAR=B2_ACCOUNT_INFO_ENV_VAR,
9286
B2_ACCOUNT_INFO_DEFAULT_FILE=B2_ACCOUNT_INFO_DEFAULT_FILE,
87+
B2_ACCOUNT_INFO_PROFILE_FILE=B2_ACCOUNT_INFO_PROFILE_FILE,
9388
XDG_CONFIG_HOME_ENV_VAR=XDG_CONFIG_HOME_ENV_VAR,
9489
)
9590
)
9691

92+
@classmethod
93+
def _get_user_account_info_path(cls, file_name=None, profile=None):
94+
if profile and not B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP.match(profile):
95+
raise ValueError('Invalid profile name: {}'.format(profile))
96+
97+
if file_name:
98+
if profile:
99+
raise ValueError('Provide either file_name or profile, not both')
100+
user_account_info_path = file_name
101+
elif B2_ACCOUNT_INFO_ENV_VAR in os.environ:
102+
if profile:
103+
raise ValueError(
104+
'Provide either {} env var or profile, not both'.
105+
format(B2_ACCOUNT_INFO_ENV_VAR)
106+
)
107+
user_account_info_path = os.environ[B2_ACCOUNT_INFO_ENV_VAR]
108+
elif os.path.exists(os.path.expanduser(B2_ACCOUNT_INFO_DEFAULT_FILE)) and not profile:
109+
user_account_info_path = B2_ACCOUNT_INFO_DEFAULT_FILE
110+
elif XDG_CONFIG_HOME_ENV_VAR in os.environ:
111+
config_home = os.environ[XDG_CONFIG_HOME_ENV_VAR]
112+
file_name = 'db-{}.sqlite'.format(profile) if profile else 'account_info'
113+
user_account_info_path = os.path.join(config_home, 'b2', file_name)
114+
if not os.path.exists(os.path.join(config_home, 'b2')):
115+
os.makedirs(os.path.join(config_home, 'b2'), mode=0o755)
116+
elif profile:
117+
user_account_info_path = B2_ACCOUNT_INFO_PROFILE_FILE.format(profile=profile)
118+
else:
119+
user_account_info_path = B2_ACCOUNT_INFO_DEFAULT_FILE
120+
121+
return os.path.expanduser(user_account_info_path)
122+
97123
def _validate_database(self, last_upgrade_to_run=None):
98124
"""
99125
Make sure that the database is openable. Removes the file if it's not.

noxfile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"pytest-cov==3.0.0",
3131
"pytest-mock==3.6.1",
3232
'pytest-lazy-fixture==0.6.3',
33+
'pyfakefs==4.5.6',
3334
]
3435
REQUIREMENTS_BUILD = ['setuptools>=20.2']
3536

@@ -107,7 +108,7 @@ def unit(session):
107108
"""Run unit tests."""
108109
install_myself(session)
109110
session.install(*REQUIREMENTS_TEST)
110-
args = ['--doctest-modules']
111+
args = ['--doctest-modules', '-p', 'pyfakefs']
111112
if not SKIP_COVERAGE:
112113
args += ['--cov=b2sdk', '--cov-branch', '--cov-report=xml']
113114
# TODO: Use session.parametrize for apiver

test/unit/account_info/test_sqlite_account_info.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@
88
#
99
######################################################################
1010

11+
import os
12+
import unittest.mock as mock
13+
1114
import pytest
1215

13-
from apiver_deps import AbstractAccountInfo
16+
from apiver_deps import (
17+
B2_ACCOUNT_INFO_DEFAULT_FILE,
18+
B2_ACCOUNT_INFO_ENV_VAR,
19+
XDG_CONFIG_HOME_ENV_VAR,
20+
AbstractAccountInfo,
21+
SqliteAccountInfo,
22+
)
1423

1524
from .fixtures import *
1625

@@ -55,3 +64,67 @@ def test_migrate_to_4(self):
5564
"SELECT recommended_part_size, absolute_minimum_part_size from account"
5665
).fetchone()
5766
assert (100, 5000000) == sizes
67+
68+
69+
class TestSqliteAccountProfileFileLocation:
70+
@pytest.fixture(autouse=True)
71+
def setup(self, monkeypatch, fs):
72+
monkeypatch.delenv(B2_ACCOUNT_INFO_ENV_VAR, raising=False)
73+
monkeypatch.delenv(XDG_CONFIG_HOME_ENV_VAR, raising=False)
74+
75+
def test_invalid_profile_name(self):
76+
with pytest.raises(ValueError):
77+
SqliteAccountInfo._get_user_account_info_path(profile='&@(*$')
78+
79+
def test_profile_and_file_name_conflict(self):
80+
with pytest.raises(ValueError):
81+
SqliteAccountInfo._get_user_account_info_path(file_name='foo', profile='bar')
82+
83+
def test_profile_and_env_var_conflict(self, monkeypatch):
84+
monkeypatch.setenv(B2_ACCOUNT_INFO_ENV_VAR, 'foo')
85+
with pytest.raises(ValueError):
86+
SqliteAccountInfo._get_user_account_info_path(profile='bar')
87+
88+
def test_profile_and_xdg_config_env_var(self, monkeypatch):
89+
monkeypatch.setenv(XDG_CONFIG_HOME_ENV_VAR, os.path.join('~', 'custom'))
90+
account_info_path = SqliteAccountInfo._get_user_account_info_path(profile='secondary')
91+
assert account_info_path == os.path.expanduser(
92+
os.path.join('~', 'custom', 'b2', 'db-secondary.sqlite')
93+
)
94+
95+
def test_profile(self):
96+
account_info_path = SqliteAccountInfo._get_user_account_info_path(profile='foo')
97+
assert account_info_path == os.path.expanduser(os.path.join('~', '.b2db-foo.sqlite'))
98+
99+
def test_file_name(self):
100+
account_info_path = SqliteAccountInfo._get_user_account_info_path(
101+
file_name=os.path.join('~', 'foo')
102+
)
103+
assert account_info_path == os.path.expanduser(os.path.join('~', 'foo'))
104+
105+
def test_env_var(self, monkeypatch):
106+
monkeypatch.setenv(B2_ACCOUNT_INFO_ENV_VAR, os.path.join('~', 'foo'))
107+
account_info_path = SqliteAccountInfo._get_user_account_info_path()
108+
assert account_info_path == os.path.expanduser(os.path.join('~', 'foo'))
109+
110+
def test_default_file_if_exists(self, monkeypatch):
111+
# ensure that XDG_CONFIG_HOME_ENV_VAR doesn't matter if default file exists
112+
monkeypatch.setenv(XDG_CONFIG_HOME_ENV_VAR, 'some')
113+
account_file_path = os.path.expanduser(B2_ACCOUNT_INFO_DEFAULT_FILE)
114+
parent_dir = os.path.abspath(os.path.join(account_file_path, os.pardir))
115+
os.makedirs(parent_dir, exist_ok=True)
116+
with open(account_file_path, 'w') as account_file:
117+
account_file.write('')
118+
account_info_path = SqliteAccountInfo._get_user_account_info_path()
119+
assert account_info_path == os.path.expanduser(B2_ACCOUNT_INFO_DEFAULT_FILE)
120+
121+
def test_xdg_config_env_var(self, monkeypatch):
122+
monkeypatch.setenv(XDG_CONFIG_HOME_ENV_VAR, os.path.join('~', 'custom'))
123+
account_info_path = SqliteAccountInfo._get_user_account_info_path()
124+
assert account_info_path == os.path.expanduser(
125+
os.path.join('~', 'custom', 'b2', 'account_info')
126+
)
127+
128+
def test_default_file(self):
129+
account_info_path = SqliteAccountInfo._get_user_account_info_path()
130+
assert account_info_path == os.path.expanduser(B2_ACCOUNT_INFO_DEFAULT_FILE)

0 commit comments

Comments
 (0)