yhttp-dbmanager 7.0.2__tar.gz → 8.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {yhttp_dbmanager-7.0.2/yhttp_dbmanager.egg-info → yhttp_dbmanager-8.0.2}/PKG-INFO +1 -1
- yhttp_dbmanager-7.0.2/tests/test_cli.py → yhttp_dbmanager-8.0.2/tests/test_cli_createdrop.py +2 -17
- yhttp_dbmanager-8.0.2/tests/test_cli_migration.py +78 -0
- yhttp_dbmanager-8.0.2/tests/test_cli_usercommand.py +30 -0
- yhttp_dbmanager-8.0.2/tests/test_migrator.py +68 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp/ext/dbmanager/__init__.py +2 -1
- yhttp_dbmanager-8.0.2/yhttp/ext/dbmanager/cli.py +167 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp/ext/dbmanager/install.py +8 -1
- yhttp_dbmanager-8.0.2/yhttp/ext/dbmanager/migration.py +192 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2/yhttp_dbmanager.egg-info}/PKG-INFO +1 -1
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp_dbmanager.egg-info/SOURCES.txt +5 -1
- yhttp_dbmanager-7.0.2/yhttp/ext/dbmanager/cli.py +0 -86
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/LICENSE +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/README.md +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/setup.cfg +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/setup.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/tests/test_dbmanager.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/tests/test_dburi.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/tests/test_install.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp/ext/dbmanager/dbmanager.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp/ext/dbmanager/uri.py +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp_dbmanager.egg-info/dependency_links.txt +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp_dbmanager.egg-info/requires.txt +0 -0
- {yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp_dbmanager.egg-info/top_level.txt +0 -0
yhttp_dbmanager-7.0.2/tests/test_cli.py → yhttp_dbmanager-8.0.2/tests/test_cli_createdrop.py
RENAMED
|
@@ -2,30 +2,21 @@ import os
|
|
|
2
2
|
|
|
3
3
|
from bddcli import Given, Application as CLIApplication, status, stderr, \
|
|
4
4
|
when, stdout
|
|
5
|
-
import easycli
|
|
6
5
|
from yhttp.core import Application
|
|
7
6
|
|
|
8
7
|
from yhttp.ext.dbmanager import install
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
class Bar(easycli.SubCommand):
|
|
12
|
-
__command__ = 'bar'
|
|
13
|
-
|
|
14
|
-
def __call__(self, args):
|
|
15
|
-
print('bar')
|
|
16
|
-
|
|
17
|
-
|
|
18
10
|
app = Application('0.1.0', 'foo')
|
|
19
11
|
app.settings.merge('''
|
|
20
12
|
db:
|
|
21
13
|
url: postgres://:@/foo
|
|
22
14
|
''')
|
|
23
|
-
install(app
|
|
15
|
+
install(app)
|
|
24
16
|
|
|
25
17
|
|
|
26
18
|
def test_applicationcli(cicd):
|
|
27
|
-
app.
|
|
28
|
-
cliapp = CLIApplication('example', 'tests.test_cli:app.climain')
|
|
19
|
+
cliapp = CLIApplication('example', f'{__name__}:app.climain')
|
|
29
20
|
env = os.environ.copy()
|
|
30
21
|
if cicd:
|
|
31
22
|
env.setdefault('YHTTP_DB_DEFAULT_HOST', 'localhost')
|
|
@@ -45,12 +36,6 @@ def test_applicationcli(cicd):
|
|
|
45
36
|
assert status == 0
|
|
46
37
|
assert stderr == ''
|
|
47
38
|
|
|
48
|
-
# Custom Command line interface
|
|
49
|
-
when('db bar')
|
|
50
|
-
assert status == 0
|
|
51
|
-
assert stderr == ''
|
|
52
|
-
assert stdout == 'bar\n'
|
|
53
|
-
|
|
54
39
|
when('db c')
|
|
55
40
|
assert status == 0
|
|
56
41
|
assert stderr == ''
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
from bddcli import Given, Application as CLIApplication, status, stderr, \
|
|
5
|
+
when, stdout
|
|
6
|
+
from yhttp.core import Application
|
|
7
|
+
|
|
8
|
+
from yhttp.ext.dbmanager import install
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
vdir = tempfile.mkdtemp()
|
|
12
|
+
app = Application('0.1.0', 'foo')
|
|
13
|
+
app.settings.merge(f'''
|
|
14
|
+
db:
|
|
15
|
+
url: postgres://:@/foo
|
|
16
|
+
migration:
|
|
17
|
+
directory: {vdir}
|
|
18
|
+
''')
|
|
19
|
+
if os.environ.get('CI') and os.environ.get('GITHUB_RUN_ID'):
|
|
20
|
+
app.settings.db.url = 'postgres://postgres:postgres@localhost/foo'
|
|
21
|
+
|
|
22
|
+
install(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
foo_content = '''
|
|
26
|
+
def upgrade(db):
|
|
27
|
+
db.execute("""
|
|
28
|
+
CREATE TABLE foo (
|
|
29
|
+
id SERIAL PRIMARY KEY,
|
|
30
|
+
title VARCHAR(50) NOT NULL
|
|
31
|
+
);
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def downgrade(db):
|
|
36
|
+
db.execute("DROP TABLE foo")
|
|
37
|
+
'''
|
|
38
|
+
with open(f'{vdir}/0001-foo.py', 'w') as f:
|
|
39
|
+
f.write(foo_content)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cli_migration(cicd):
|
|
43
|
+
cliapp = CLIApplication('foo', f'{__name__}:app.climain')
|
|
44
|
+
env = os.environ.copy()
|
|
45
|
+
if cicd:
|
|
46
|
+
env.setdefault('YHTTP_DB_DEFAULT_HOST', 'localhost')
|
|
47
|
+
env.setdefault('YHTTP_DB_DEFAULT_ADMINUSER', 'postgres')
|
|
48
|
+
env.setdefault('YHTTP_DB_DEFAULT_ADMINPASS', 'postgres')
|
|
49
|
+
env.setdefault('YHTTP_DB_DEFAULT_USER', 'postgres')
|
|
50
|
+
env.setdefault('YHTTP_DB_DEFAULT_PASS', 'postgres')
|
|
51
|
+
|
|
52
|
+
with Given(cliapp, 'db migration', environ=env):
|
|
53
|
+
when('db drop')
|
|
54
|
+
when('db create')
|
|
55
|
+
assert status == 0
|
|
56
|
+
|
|
57
|
+
when('db migration upgrade 1')
|
|
58
|
+
assert stderr == ''
|
|
59
|
+
assert status == 0
|
|
60
|
+
assert stdout == 'database successfully upgraded to version 0001.\n'
|
|
61
|
+
|
|
62
|
+
when('db migration downgrade 0')
|
|
63
|
+
assert stderr == ''
|
|
64
|
+
assert stdout == 'database successfully downgraded to version 0000.\n'
|
|
65
|
+
assert status == 0
|
|
66
|
+
|
|
67
|
+
when('db migration upgrade')
|
|
68
|
+
assert stderr == ''
|
|
69
|
+
assert status == 0
|
|
70
|
+
assert stdout == 'database successfully upgraded to version 0001.\n'
|
|
71
|
+
|
|
72
|
+
when('db migration new bar')
|
|
73
|
+
assert stderr == ''
|
|
74
|
+
assert status == 0
|
|
75
|
+
assert stdout.endswith('/0002-bar.py.\n')
|
|
76
|
+
|
|
77
|
+
when('db drop')
|
|
78
|
+
assert status == 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from bddcli import Given, Application as CLIApplication, status, stderr, \
|
|
2
|
+
stdout
|
|
3
|
+
import easycli
|
|
4
|
+
from yhttp.core import Application
|
|
5
|
+
|
|
6
|
+
from yhttp.ext.dbmanager import install
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Bar(easycli.SubCommand):
|
|
10
|
+
__command__ = 'bar'
|
|
11
|
+
|
|
12
|
+
def __call__(self, args):
|
|
13
|
+
print('bar')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app = Application('0.1.0', 'foo')
|
|
17
|
+
app.settings.merge('''
|
|
18
|
+
db:
|
|
19
|
+
url: postgres://:@/foo
|
|
20
|
+
''')
|
|
21
|
+
install(app, cliarguments=[Bar])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_applicationcli():
|
|
25
|
+
cliapp = CLIApplication('example', f'{__name__}:app.climain')
|
|
26
|
+
|
|
27
|
+
with Given(cliapp, 'db bar'):
|
|
28
|
+
assert status == 0
|
|
29
|
+
assert stderr == ''
|
|
30
|
+
assert stdout == 'bar\n'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
foo_content = '''
|
|
5
|
+
def upgrade(db):
|
|
6
|
+
db.execute("""
|
|
7
|
+
CREATE TABLE foo (
|
|
8
|
+
id SERIAL PRIMARY KEY,
|
|
9
|
+
title VARCHAR(50) NOT NULL
|
|
10
|
+
);
|
|
11
|
+
""")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def downgrade(db):
|
|
15
|
+
db.execute("DROP TABLE foo")
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
bar_content = '''
|
|
20
|
+
def upgrade(db):
|
|
21
|
+
db.execute("""
|
|
22
|
+
CREATE TABLE bar (
|
|
23
|
+
id SERIAL PRIMARY KEY,
|
|
24
|
+
title VARCHAR(50) NOT NULL
|
|
25
|
+
);
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def downgrade(db):
|
|
30
|
+
db.execute("DROP TABLE bar")
|
|
31
|
+
'''
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_migrator_upgrade_downgrade(migrator, mktmptree):
|
|
35
|
+
root = mktmptree({
|
|
36
|
+
'0001-foo.py': foo_content,
|
|
37
|
+
'0002-bar.py': bar_content,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
with pytest.raises(ValueError):
|
|
41
|
+
migrator.versions_directory
|
|
42
|
+
|
|
43
|
+
migrator.settings.directory = root
|
|
44
|
+
|
|
45
|
+
assert migrator.dbversion() == 0
|
|
46
|
+
assert migrator.lastversion == 2
|
|
47
|
+
|
|
48
|
+
assert 2 == migrator.upgrade()
|
|
49
|
+
assert migrator.db.table_exists('foo')
|
|
50
|
+
assert migrator.db.table_exists('bar')
|
|
51
|
+
|
|
52
|
+
assert 0 == migrator.downgrade()
|
|
53
|
+
assert not migrator.db.table_exists('foo')
|
|
54
|
+
assert not migrator.db.table_exists('bar')
|
|
55
|
+
|
|
56
|
+
with pytest.raises(FileNotFoundError):
|
|
57
|
+
migrator.upgrade(3)
|
|
58
|
+
|
|
59
|
+
migrator.dbversion_set(3)
|
|
60
|
+
with pytest.raises(FileNotFoundError):
|
|
61
|
+
migrator.downgrade(2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_migrator_newversion(migrator, tmpdir):
|
|
65
|
+
migrator.settings.directory = f'{tmpdir}/versions'
|
|
66
|
+
|
|
67
|
+
assert f'{tmpdir}/versions/0001-foo.py' == migrator.newversion('foo')
|
|
68
|
+
assert f'{tmpdir}/versions/0002-bar.py' == migrator.newversion('bar')
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import functools
|
|
3
|
+
import getpass
|
|
4
|
+
|
|
5
|
+
from easycli import SubCommand, Argument
|
|
6
|
+
|
|
7
|
+
from . import dbmanager, migration
|
|
8
|
+
from .uri import DatabaseURI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
getdbpass = functools.partial(getpass.getpass, 'Enter db password: ')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_DBUSER = os.environ['USER']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseAdministrativeCommand(SubCommand):
|
|
18
|
+
__arguments__ = [
|
|
19
|
+
Argument(
|
|
20
|
+
'-H',
|
|
21
|
+
'--host',
|
|
22
|
+
default=os.environ.get('YHTTP_DB_DEFAULT_HOST', ''),
|
|
23
|
+
help='DB hostname, default: empty.'
|
|
24
|
+
),
|
|
25
|
+
Argument('-d', '--database', default='postgres', help='DB name'),
|
|
26
|
+
Argument(
|
|
27
|
+
'-u',
|
|
28
|
+
'--user',
|
|
29
|
+
default=os.environ.get(
|
|
30
|
+
'YHTTP_DB_DEFAULT_ADMINUSER',
|
|
31
|
+
DEFAULT_DBUSER
|
|
32
|
+
),
|
|
33
|
+
help=f'DB administrator username, default: ${DEFAULT_DBUSER}'
|
|
34
|
+
),
|
|
35
|
+
Argument(
|
|
36
|
+
'-p', '--password',
|
|
37
|
+
nargs='?',
|
|
38
|
+
default=os.environ.get(
|
|
39
|
+
'YHTTP_DB_DEFAULT_ADMINPASS',
|
|
40
|
+
'postgres'
|
|
41
|
+
),
|
|
42
|
+
help='DB administrator password'
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def getdbmanager(self, args):
|
|
47
|
+
password = args.password or getdbpass()
|
|
48
|
+
|
|
49
|
+
return dbmanager.PostgresqlManager(
|
|
50
|
+
user=args.user,
|
|
51
|
+
password=password,
|
|
52
|
+
host=args.host,
|
|
53
|
+
database=args.database
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def getappdbinfo(self, args):
|
|
57
|
+
dbsettings = args.application.settings.db
|
|
58
|
+
url = DatabaseURI.loads(dbsettings.url)
|
|
59
|
+
return url
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CreateDatabaseCommand(DatabaseAdministrativeCommand):
|
|
63
|
+
__command__ = 'create'
|
|
64
|
+
__aliases__ = ['c']
|
|
65
|
+
|
|
66
|
+
def __call__(self, args):
|
|
67
|
+
uri = self.getappdbinfo(args)
|
|
68
|
+
self.getdbmanager(args).create(uri.database, owner=uri.user)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DropDatabaseCommand(DatabaseAdministrativeCommand):
|
|
72
|
+
__command__ = 'drop'
|
|
73
|
+
__aliases__ = ['d']
|
|
74
|
+
|
|
75
|
+
def __call__(self, args):
|
|
76
|
+
uri = self.getappdbinfo(args)
|
|
77
|
+
self.getdbmanager(args).drop(uri.database)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MigrationSubCommand(SubCommand):
|
|
81
|
+
def migrator(self, args):
|
|
82
|
+
app = args.application
|
|
83
|
+
migrator = migration.Migrator(
|
|
84
|
+
app.settings.db.url,
|
|
85
|
+
app.settings.db.migration
|
|
86
|
+
)
|
|
87
|
+
return migrator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UpgradeCommand(MigrationSubCommand):
|
|
91
|
+
__command__ = 'upgrade'
|
|
92
|
+
__aliases__ = ['u', 'up']
|
|
93
|
+
__arguments__ = [
|
|
94
|
+
Argument(
|
|
95
|
+
'version',
|
|
96
|
+
nargs='?',
|
|
97
|
+
metavar='VERSION',
|
|
98
|
+
type=int,
|
|
99
|
+
help='Upgrade to this version, default: last version'
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def __call__(self, args):
|
|
104
|
+
with self.migrator(args) as m:
|
|
105
|
+
ver = m.upgrade(args.version)
|
|
106
|
+
|
|
107
|
+
print(f'database successfully upgraded to version {ver:04d}.')
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DowngradeCommand(MigrationSubCommand):
|
|
111
|
+
__command__ = 'downgrade'
|
|
112
|
+
__aliases__ = ['d', 'down']
|
|
113
|
+
__arguments__ = [
|
|
114
|
+
Argument(
|
|
115
|
+
'version',
|
|
116
|
+
metavar='VERSION',
|
|
117
|
+
type=int,
|
|
118
|
+
help='Upgrade to this version.'
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def __call__(self, args):
|
|
123
|
+
with self.migrator(args) as m:
|
|
124
|
+
ver = m.downgrade(args.version)
|
|
125
|
+
|
|
126
|
+
print(f'database successfully downgraded to version {ver:04d}.')
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class NewVersionCommand(MigrationSubCommand):
|
|
130
|
+
__command__ = 'new'
|
|
131
|
+
__aliases__ = ['n']
|
|
132
|
+
__arguments__ = [
|
|
133
|
+
Argument(
|
|
134
|
+
'name',
|
|
135
|
+
metavar='TITLE',
|
|
136
|
+
help='Creates a new database version file'
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
def __call__(self, args):
|
|
141
|
+
with self.migrator(args) as m:
|
|
142
|
+
ver = m.newversion(args.name)
|
|
143
|
+
|
|
144
|
+
print(f'File generated successfully: {ver}.')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class MigrationCommand(SubCommand):
|
|
148
|
+
__command__ = 'migration'
|
|
149
|
+
__aliases__ = ['mi']
|
|
150
|
+
__arguments__ = [
|
|
151
|
+
UpgradeCommand,
|
|
152
|
+
DowngradeCommand,
|
|
153
|
+
NewVersionCommand
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
def __call__(self, args):
|
|
157
|
+
self._parser.print_help()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class DatabaseCommand(SubCommand):
|
|
161
|
+
__command__ = 'database'
|
|
162
|
+
__aliases__ = ['db']
|
|
163
|
+
__arguments__ = [
|
|
164
|
+
CreateDatabaseCommand,
|
|
165
|
+
DropDatabaseCommand,
|
|
166
|
+
MigrationCommand
|
|
167
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .cli import DatabaseCommand
|
|
2
|
+
from .migration import Migrator
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def install(app, cliarguments=None):
|
|
@@ -6,9 +7,15 @@ def install(app, cliarguments=None):
|
|
|
6
7
|
if cliarguments:
|
|
7
8
|
DatabaseCommand.__arguments__.extend(cliarguments)
|
|
8
9
|
|
|
10
|
+
app.settings.merge('''
|
|
11
|
+
db:
|
|
12
|
+
migration: {}
|
|
13
|
+
''')
|
|
14
|
+
app.settings.db.migration.merge(Migrator.default_settings)
|
|
15
|
+
|
|
9
16
|
@app.when
|
|
10
17
|
def ready(app):
|
|
11
|
-
if '
|
|
18
|
+
if 'url' not in app.settings.db:
|
|
12
19
|
raise ValueError(
|
|
13
20
|
'Please provide db.url configuration entry, for example: '
|
|
14
21
|
'postgres://:@/dbname'
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import importlib
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
|
|
7
|
+
import psycopg2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
VERSIONFILE_TEMPLATE = '''\
|
|
11
|
+
def upgrade(db):
|
|
12
|
+
raise NotImplementedError()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def downgrade(db):
|
|
16
|
+
raise NotImplementedError()
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Database:
|
|
21
|
+
def __init__(self, url):
|
|
22
|
+
self.url = url
|
|
23
|
+
|
|
24
|
+
@cached_property
|
|
25
|
+
def connection(self):
|
|
26
|
+
return psycopg2.connect(self.url)
|
|
27
|
+
|
|
28
|
+
def commit(self):
|
|
29
|
+
self.connection.commit()
|
|
30
|
+
|
|
31
|
+
def close(self):
|
|
32
|
+
self.connection.close()
|
|
33
|
+
|
|
34
|
+
def execute(self, query):
|
|
35
|
+
with self.connection.cursor() as c:
|
|
36
|
+
c.execute(query)
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def select(self, query):
|
|
40
|
+
with self.connection.cursor() as c:
|
|
41
|
+
c.execute(query)
|
|
42
|
+
yield c
|
|
43
|
+
|
|
44
|
+
def exists(self, query):
|
|
45
|
+
query = f'SELECT EXISTS ({query});'
|
|
46
|
+
|
|
47
|
+
with self.connection.cursor() as c:
|
|
48
|
+
c.execute(query)
|
|
49
|
+
r = c.fetchone()
|
|
50
|
+
return r[0]
|
|
51
|
+
|
|
52
|
+
def table_exists(self, name):
|
|
53
|
+
return self.exists(f'''
|
|
54
|
+
SELECT 1
|
|
55
|
+
FROM information_schema.tables
|
|
56
|
+
WHERE table_schema = 'public'
|
|
57
|
+
AND table_name = '{name}'
|
|
58
|
+
''')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Migrator:
|
|
62
|
+
default_settings = '''
|
|
63
|
+
versiontable: yhttp_version
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
def __init__(self, url, settings):
|
|
67
|
+
self.url = url
|
|
68
|
+
self.settings = settings
|
|
69
|
+
|
|
70
|
+
def __enter__(self):
|
|
71
|
+
self.db
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, exctype, excvalue, traceback):
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
@cached_property
|
|
78
|
+
def db(self):
|
|
79
|
+
return Database(self.url)
|
|
80
|
+
|
|
81
|
+
def close(self):
|
|
82
|
+
self.db.close()
|
|
83
|
+
|
|
84
|
+
@cached_property
|
|
85
|
+
def vtable(self):
|
|
86
|
+
return self.settings.versiontable
|
|
87
|
+
|
|
88
|
+
def ensure_versiontable(self):
|
|
89
|
+
if self.db.table_exists(self.vtable):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
self.db.execute(f'''
|
|
93
|
+
CREATE TABLE {self.vtable} (
|
|
94
|
+
version INTEGER NOT NULL
|
|
95
|
+
);
|
|
96
|
+
''')
|
|
97
|
+
|
|
98
|
+
self.db.execute(f'''
|
|
99
|
+
INSERT INTO {self.vtable} (version) VALUES (0);
|
|
100
|
+
''')
|
|
101
|
+
self.db.commit()
|
|
102
|
+
|
|
103
|
+
def dbversion(self):
|
|
104
|
+
self.ensure_versiontable()
|
|
105
|
+
with self.db.select(f'SELECT version from {self.vtable}') as c:
|
|
106
|
+
return c.fetchone()[0]
|
|
107
|
+
|
|
108
|
+
def dbversion_set(self, ver):
|
|
109
|
+
self.db.execute(f'UPDATE {self.vtable} SET version={ver}')
|
|
110
|
+
self.db.commit()
|
|
111
|
+
|
|
112
|
+
@cached_property
|
|
113
|
+
def versions_directory(self):
|
|
114
|
+
try:
|
|
115
|
+
vdir = self.settings.directory
|
|
116
|
+
except AttributeError:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
'Please provide db.migration.directory configuration entry.'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not os.path.exists(vdir):
|
|
122
|
+
os.mkdir(vdir)
|
|
123
|
+
|
|
124
|
+
return vdir
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def lastversion(self):
|
|
128
|
+
files = glob.glob(f'{self.versions_directory}/*-*.py')
|
|
129
|
+
if not files:
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
lastfile = sorted(files)[-1]
|
|
133
|
+
ver = os.path.basename(lastfile).split('-', 1)[0]
|
|
134
|
+
return int(ver)
|
|
135
|
+
|
|
136
|
+
def newversion(self, name):
|
|
137
|
+
ver = self.lastversion + 1
|
|
138
|
+
filename = f'{ver:04d}-{name}.py'
|
|
139
|
+
filepath = os.path.join(self.versions_directory, filename)
|
|
140
|
+
with open(filepath, 'w') as f:
|
|
141
|
+
f.write(VERSIONFILE_TEMPLATE)
|
|
142
|
+
|
|
143
|
+
return filepath
|
|
144
|
+
|
|
145
|
+
def loadmodule(self, ver):
|
|
146
|
+
files = glob.glob(f'{self.versions_directory}/{ver:04d}-*.py')
|
|
147
|
+
if not files:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
modfile = files[0]
|
|
151
|
+
modname = os.path.splitext(os.path.basename(modfile))[0]
|
|
152
|
+
spec = importlib.util.spec_from_file_location(modname, modfile)
|
|
153
|
+
mod = importlib.util.module_from_spec(spec)
|
|
154
|
+
# sys.modules[module_name] = module
|
|
155
|
+
spec.loader.exec_module(mod)
|
|
156
|
+
return mod
|
|
157
|
+
|
|
158
|
+
def filenotfound(self, ver):
|
|
159
|
+
return FileNotFoundError(
|
|
160
|
+
f'script {ver:04d}_*.py not found within directory: '
|
|
161
|
+
f'{self.versions_directory}.'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def upgrade(self, targetver=None):
|
|
165
|
+
dbver = self.dbversion()
|
|
166
|
+
if targetver is None:
|
|
167
|
+
targetver = self.lastversion
|
|
168
|
+
|
|
169
|
+
while dbver < targetver:
|
|
170
|
+
cver = dbver + 1
|
|
171
|
+
mod = self.loadmodule(cver)
|
|
172
|
+
if mod is None:
|
|
173
|
+
raise self.filenotfound(cver)
|
|
174
|
+
|
|
175
|
+
mod.upgrade(self.db)
|
|
176
|
+
self.dbversion_set(cver)
|
|
177
|
+
dbver = cver
|
|
178
|
+
|
|
179
|
+
return targetver
|
|
180
|
+
|
|
181
|
+
def downgrade(self, targetver=0):
|
|
182
|
+
dbver = self.dbversion()
|
|
183
|
+
while dbver > targetver:
|
|
184
|
+
mod = self.loadmodule(dbver)
|
|
185
|
+
if mod is None:
|
|
186
|
+
raise self.filenotfound(dbver)
|
|
187
|
+
|
|
188
|
+
mod.downgrade(self.db)
|
|
189
|
+
dbver -= 1
|
|
190
|
+
self.dbversion_set(dbver)
|
|
191
|
+
|
|
192
|
+
return targetver
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
setup.py
|
|
4
|
-
tests/
|
|
4
|
+
tests/test_cli_createdrop.py
|
|
5
|
+
tests/test_cli_migration.py
|
|
6
|
+
tests/test_cli_usercommand.py
|
|
5
7
|
tests/test_dbmanager.py
|
|
6
8
|
tests/test_dburi.py
|
|
7
9
|
tests/test_install.py
|
|
10
|
+
tests/test_migrator.py
|
|
8
11
|
yhttp/ext/dbmanager/__init__.py
|
|
9
12
|
yhttp/ext/dbmanager/cli.py
|
|
10
13
|
yhttp/ext/dbmanager/dbmanager.py
|
|
11
14
|
yhttp/ext/dbmanager/install.py
|
|
15
|
+
yhttp/ext/dbmanager/migration.py
|
|
12
16
|
yhttp/ext/dbmanager/uri.py
|
|
13
17
|
yhttp_dbmanager.egg-info/PKG-INFO
|
|
14
18
|
yhttp_dbmanager.egg-info/SOURCES.txt
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import functools
|
|
3
|
-
import getpass
|
|
4
|
-
|
|
5
|
-
from easycli import SubCommand, Argument
|
|
6
|
-
|
|
7
|
-
from . import dbmanager
|
|
8
|
-
from .uri import DatabaseURI
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
getdbpass = functools.partial(getpass.getpass, 'Enter db password: ')
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
DEFAULT_DBUSER = os.environ['USER']
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class DatabaseAdministrativeCommand(SubCommand):
|
|
18
|
-
__arguments__ = [
|
|
19
|
-
Argument(
|
|
20
|
-
'-H',
|
|
21
|
-
'--host',
|
|
22
|
-
default=os.environ.get('YHTTP_DB_DEFAULT_HOST', ''),
|
|
23
|
-
help='DB hostname, default: empty.'
|
|
24
|
-
),
|
|
25
|
-
Argument('-d', '--database', default='postgres', help='DB name'),
|
|
26
|
-
Argument(
|
|
27
|
-
'-u',
|
|
28
|
-
'--user',
|
|
29
|
-
default=os.environ.get(
|
|
30
|
-
'YHTTP_DB_DEFAULT_ADMINUSER',
|
|
31
|
-
DEFAULT_DBUSER
|
|
32
|
-
),
|
|
33
|
-
help=f'DB administrator username, default: ${DEFAULT_DBUSER}'
|
|
34
|
-
),
|
|
35
|
-
Argument(
|
|
36
|
-
'-p', '--password',
|
|
37
|
-
nargs='?',
|
|
38
|
-
default=os.environ.get(
|
|
39
|
-
'YHTTP_DB_DEFAULT_ADMINPASS',
|
|
40
|
-
'postgres'
|
|
41
|
-
),
|
|
42
|
-
help='DB administrator password'
|
|
43
|
-
),
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
def getdbmanager(self, args):
|
|
47
|
-
password = args.password or getdbpass()
|
|
48
|
-
|
|
49
|
-
return dbmanager.PostgresqlManager(
|
|
50
|
-
user=args.user,
|
|
51
|
-
password=password,
|
|
52
|
-
host=args.host,
|
|
53
|
-
database=args.database
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
def getappdbinfo(self, args):
|
|
57
|
-
dbsettings = args.application.settings.db
|
|
58
|
-
url = DatabaseURI.loads(dbsettings.url)
|
|
59
|
-
return url
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class CreateDatabase(DatabaseAdministrativeCommand):
|
|
63
|
-
__command__ = 'create'
|
|
64
|
-
__aliases__ = ['c']
|
|
65
|
-
|
|
66
|
-
def __call__(self, args):
|
|
67
|
-
uri = self.getappdbinfo(args)
|
|
68
|
-
self.getdbmanager(args).create(uri.database, owner=uri.user)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class DropDatabase(DatabaseAdministrativeCommand):
|
|
72
|
-
__command__ = 'drop'
|
|
73
|
-
__aliases__ = ['d']
|
|
74
|
-
|
|
75
|
-
def __call__(self, args):
|
|
76
|
-
uri = self.getappdbinfo(args)
|
|
77
|
-
self.getdbmanager(args).drop(uri.database)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class DatabaseCommand(SubCommand):
|
|
81
|
-
__command__ = 'database'
|
|
82
|
-
__aliases__ = ['db']
|
|
83
|
-
__arguments__ = [
|
|
84
|
-
CreateDatabase,
|
|
85
|
-
DropDatabase
|
|
86
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{yhttp_dbmanager-7.0.2 → yhttp_dbmanager-8.0.2}/yhttp_dbmanager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|