half-orm-dev 0.16.0a9__py3-none-any.whl
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.
- half_orm_dev/__init__.py +1 -0
- half_orm_dev/cli/__init__.py +9 -0
- half_orm_dev/cli/commands/__init__.py +56 -0
- half_orm_dev/cli/commands/apply.py +13 -0
- half_orm_dev/cli/commands/clone.py +102 -0
- half_orm_dev/cli/commands/init.py +331 -0
- half_orm_dev/cli/commands/new.py +15 -0
- half_orm_dev/cli/commands/patch.py +317 -0
- half_orm_dev/cli/commands/prepare.py +21 -0
- half_orm_dev/cli/commands/prepare_release.py +119 -0
- half_orm_dev/cli/commands/promote_to.py +127 -0
- half_orm_dev/cli/commands/release.py +344 -0
- half_orm_dev/cli/commands/restore.py +14 -0
- half_orm_dev/cli/commands/sync.py +13 -0
- half_orm_dev/cli/commands/todo.py +73 -0
- half_orm_dev/cli/commands/undo.py +17 -0
- half_orm_dev/cli/commands/update.py +73 -0
- half_orm_dev/cli/commands/upgrade.py +191 -0
- half_orm_dev/cli/main.py +103 -0
- half_orm_dev/cli_extension.py +38 -0
- half_orm_dev/database.py +1389 -0
- half_orm_dev/hgit.py +1025 -0
- half_orm_dev/hop.py +167 -0
- half_orm_dev/manifest.py +43 -0
- half_orm_dev/modules.py +456 -0
- half_orm_dev/patch.py +281 -0
- half_orm_dev/patch_manager.py +1694 -0
- half_orm_dev/patch_validator.py +335 -0
- half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +34 -0
- half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +2 -0
- half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +3 -0
- half_orm_dev/patches/log +2 -0
- half_orm_dev/patches/sql/half_orm_meta.sql +208 -0
- half_orm_dev/release_manager.py +2841 -0
- half_orm_dev/repo.py +1562 -0
- half_orm_dev/templates/.gitignore +15 -0
- half_orm_dev/templates/MANIFEST.in +1 -0
- half_orm_dev/templates/Pipfile +13 -0
- half_orm_dev/templates/README +25 -0
- half_orm_dev/templates/conftest_template +42 -0
- half_orm_dev/templates/init_module_template +10 -0
- half_orm_dev/templates/module_template_1 +12 -0
- half_orm_dev/templates/module_template_2 +6 -0
- half_orm_dev/templates/module_template_3 +3 -0
- half_orm_dev/templates/relation_test +23 -0
- half_orm_dev/templates/setup.py +81 -0
- half_orm_dev/templates/sql_adapter +9 -0
- half_orm_dev/templates/warning +12 -0
- half_orm_dev/utils.py +49 -0
- half_orm_dev/version.txt +1 -0
- half_orm_dev-0.16.0a9.dist-info/METADATA +935 -0
- half_orm_dev-0.16.0a9.dist-info/RECORD +58 -0
- half_orm_dev-0.16.0a9.dist-info/WHEEL +5 -0
- half_orm_dev-0.16.0a9.dist-info/licenses/AUTHORS +3 -0
- half_orm_dev-0.16.0a9.dist-info/licenses/LICENSE +14 -0
- half_orm_dev-0.16.0a9.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +329 -0
half_orm_dev/patch.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"The patch module"
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import psycopg2
|
|
9
|
+
|
|
10
|
+
from half_orm import utils
|
|
11
|
+
from half_orm_dev import modules
|
|
12
|
+
|
|
13
|
+
from .utils import hop_version
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
PYTEST_OK = True
|
|
17
|
+
import pytest
|
|
18
|
+
except ImportError:
|
|
19
|
+
PYTEST_OK = False
|
|
20
|
+
|
|
21
|
+
class Patch:
|
|
22
|
+
"The Patch class..."
|
|
23
|
+
__levels = ['patch', 'minor', 'major']
|
|
24
|
+
|
|
25
|
+
def __init__(self, repo):
|
|
26
|
+
self.__repo = repo
|
|
27
|
+
self.__patches_base_dir = os.path.join(repo.base_dir, 'Patches')
|
|
28
|
+
if self.__repo.devel:
|
|
29
|
+
self.__changelog = Changelog(repo)
|
|
30
|
+
if not os.path.exists(self.__patches_base_dir):
|
|
31
|
+
os.makedirs(self.__patches_base_dir)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
@property
|
|
35
|
+
def levels(cls):
|
|
36
|
+
"Returns the levels"
|
|
37
|
+
return cls.__levels
|
|
38
|
+
|
|
39
|
+
def previous(self, release, index=0):
|
|
40
|
+
"Return .hop/CHANGELOG second to last line."
|
|
41
|
+
return self.__changelog.previous(release, index)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def __next_releases(self):
|
|
45
|
+
db_last_release = self.__repo.database.last_release_s
|
|
46
|
+
ch_last_release = self.__changelog.last_release
|
|
47
|
+
if db_last_release != ch_last_release:
|
|
48
|
+
utils.error(
|
|
49
|
+
f'Last release mismatch between database {db_last_release}'
|
|
50
|
+
f' and CHANGELOG {ch_last_release}!\n', 1)
|
|
51
|
+
current = dict(self.__repo.database.last_release)
|
|
52
|
+
releases_in_dev = self.__changelog.releases_in_dev
|
|
53
|
+
n_rels = {}
|
|
54
|
+
for level in self.__levels:
|
|
55
|
+
n_rel = dict(current)
|
|
56
|
+
n_rel[level] = current[level] + 1
|
|
57
|
+
if level == 'major':
|
|
58
|
+
n_rel['minor'] = n_rel['patch'] = 0
|
|
59
|
+
if level == 'minor':
|
|
60
|
+
n_rel['patch'] = 0
|
|
61
|
+
next_release_s = f"{n_rel['major']}.{n_rel['minor']}.{n_rel['patch']}"
|
|
62
|
+
n_rel['in_dev'] = ''
|
|
63
|
+
if next_release_s in releases_in_dev:
|
|
64
|
+
n_rel['in_dev'] = '(IN DEV)'
|
|
65
|
+
n_rels[level] = n_rel
|
|
66
|
+
return n_rels
|
|
67
|
+
|
|
68
|
+
def __assert_main_branch(self):
|
|
69
|
+
if str(self.__repo.hgit.branch) != 'hop_main':
|
|
70
|
+
utils.error(
|
|
71
|
+
'ERROR! Wrong branch. Please, switch to the hop_main branch before.\n', exit_code=1)
|
|
72
|
+
|
|
73
|
+
def prep_release(self, release_level, message=None):
|
|
74
|
+
"""LEGACY METHOD - No longer supported"""
|
|
75
|
+
raise NotImplementedError(
|
|
76
|
+
"Legacy release preparation removed. "
|
|
77
|
+
"Use new PatchManager via repo.patch_manager"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def __check_apply_or_re_apply(self):
|
|
81
|
+
"""Return True if it's the first time.
|
|
82
|
+
False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
if self.__repo.database.last_release_s == self.__repo.hgit.current_release:
|
|
85
|
+
return 're-apply'
|
|
86
|
+
return 'apply'
|
|
87
|
+
|
|
88
|
+
def __backup_file(self, directory, release, commit=None):
|
|
89
|
+
backup_dir = os.path.join(self.__repo.base_dir, directory)
|
|
90
|
+
if not os.path.isdir(backup_dir):
|
|
91
|
+
os.mkdir(backup_dir)
|
|
92
|
+
file_name = f'{self.__repo.name}-{release}'
|
|
93
|
+
if commit:
|
|
94
|
+
file_name = f'{file_name}-{commit}'
|
|
95
|
+
return os.path.join(backup_dir, f'{file_name}.sql')
|
|
96
|
+
|
|
97
|
+
def __save_db(self, release):
|
|
98
|
+
"""Save the database
|
|
99
|
+
"""
|
|
100
|
+
commit = None
|
|
101
|
+
if self.__repo.production:
|
|
102
|
+
commit = self.__repo.hgit.last_commit()
|
|
103
|
+
svg_file = self.__backup_file('Backups', release, commit)
|
|
104
|
+
print(f'Saving the database into {svg_file}')
|
|
105
|
+
if os.path.isfile(svg_file):
|
|
106
|
+
utils.error(
|
|
107
|
+
f"Oops! there is already a dump for the {release} release.\n")
|
|
108
|
+
utils.warning("Please remove it if you really want to proceed.\n")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
self.__repo.database.execute_pg_command(
|
|
112
|
+
'pg_dump', '-f', svg_file, stderr=subprocess.PIPE)
|
|
113
|
+
|
|
114
|
+
def __restore_db(self, release):
|
|
115
|
+
"""Restore the database to the release_s version.
|
|
116
|
+
"""
|
|
117
|
+
backup_file = self.__backup_file('Backups', release)
|
|
118
|
+
if not os.path.exists(backup_file):
|
|
119
|
+
raise FileNotFoundError(backup_file)
|
|
120
|
+
print(f'{utils.Color.green("Restoring the database to")} {utils.Color.bold(release)}')
|
|
121
|
+
self.__repo.model.disconnect()
|
|
122
|
+
self.__repo.database.execute_pg_command('dropdb')
|
|
123
|
+
self.__repo.database.execute_pg_command('createdb')
|
|
124
|
+
self.__repo.database.execute_pg_command(
|
|
125
|
+
'psql', '-f', backup_file, stdout=subprocess.DEVNULL)
|
|
126
|
+
self.__repo.model.ping()
|
|
127
|
+
|
|
128
|
+
def __restore_previous_release(self):
|
|
129
|
+
db_release = self.__repo.database.last_release_s
|
|
130
|
+
self.__restore_db(db_release)
|
|
131
|
+
os.remove(self.__backup_file('Backups', db_release))
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
def __execute_sql(self, file_):
|
|
135
|
+
"Execute sql query contained in sql file_"
|
|
136
|
+
query = utils.read(file_.path).replace('%', '%%')
|
|
137
|
+
if len(query) == 0:
|
|
138
|
+
return
|
|
139
|
+
try:
|
|
140
|
+
self.__repo.model.execute_query(query)
|
|
141
|
+
except psycopg2.Error as err:
|
|
142
|
+
utils.error(f'Problem with query in {file_.name}\n{err}\n')
|
|
143
|
+
self.__restore_previous_release()
|
|
144
|
+
|
|
145
|
+
def __execute_script(self, file_):
|
|
146
|
+
try:
|
|
147
|
+
python_path = os.environ.get('PYTHONPATH')
|
|
148
|
+
if python_path:
|
|
149
|
+
python_path = python_path.split(':')
|
|
150
|
+
else:
|
|
151
|
+
python_path = []
|
|
152
|
+
if self.__repo.base_dir:
|
|
153
|
+
os.environ.update({'PYTHONPATH': ':'.join([self.__repo.base_dir] + python_path)})
|
|
154
|
+
subprocess.run(
|
|
155
|
+
['python', file_.path],
|
|
156
|
+
cwd=self.__repo.base_dir,
|
|
157
|
+
env=os.environ,
|
|
158
|
+
shell=False, check=True)
|
|
159
|
+
except subprocess.CalledProcessError as err:
|
|
160
|
+
utils.error(f'Problem with script {file_}\n{err}\n')
|
|
161
|
+
self.__restore_previous_release()
|
|
162
|
+
|
|
163
|
+
def __apply(self, path):
|
|
164
|
+
"""LEGACY METHOD - No longer supported"""
|
|
165
|
+
raise NotImplementedError(
|
|
166
|
+
"Legacy Patches/ system removed in v0.16.0. "
|
|
167
|
+
"Use new patch-centric workflow with half_orm dev commands."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def apply(self, release, force=False, save_db=True):
|
|
171
|
+
"""Apply the release in 'path'
|
|
172
|
+
|
|
173
|
+
The history is first rebased on hop_main
|
|
174
|
+
"""
|
|
175
|
+
if self.__repo.hgit.repos_is_clean():
|
|
176
|
+
self.__repo.hgit.rebase('hop_main')
|
|
177
|
+
db_release = self.__repo.database.last_release_s
|
|
178
|
+
changelog_msg = ''
|
|
179
|
+
if self.__check_apply_or_re_apply() == 'apply' and save_db:
|
|
180
|
+
self.__save_db(db_release)
|
|
181
|
+
elif not self.__repo.production:
|
|
182
|
+
if not force:
|
|
183
|
+
okay = input(f'Do you want to re-apply the release {release} [y/N]?') or 'y'
|
|
184
|
+
if okay.upper() != 'Y':
|
|
185
|
+
sys.exit()
|
|
186
|
+
self.__restore_db(self.previous(db_release, 1))
|
|
187
|
+
app_upg = utils.Color.green('Upgrading to') if self.__repo.production else utils.Color.bold('Applying')
|
|
188
|
+
major, minor, patch = release.split('.')
|
|
189
|
+
print(utils.Color.green("Pre patch:"))
|
|
190
|
+
self.__apply(os.path.join(self.__patches_base_dir, 'pre'))
|
|
191
|
+
print(f'{app_upg} {utils.Color.green(release)}:')
|
|
192
|
+
self.__apply(os.path.join(self.__patches_base_dir, major, minor, patch))
|
|
193
|
+
if not self.__repo.production:
|
|
194
|
+
modules.generate(self.__repo)
|
|
195
|
+
print(utils.Color.green("Post patch:"))
|
|
196
|
+
self.__apply(os.path.join(self.__patches_base_dir, 'post'))
|
|
197
|
+
self.__repo.database.register_release(major, minor, patch, changelog_msg)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def state(self):
|
|
201
|
+
"The state of a patch"
|
|
202
|
+
if not self.__repo.devel:
|
|
203
|
+
return 'This repo is not in developement mode.'
|
|
204
|
+
if not self.__repo.production:
|
|
205
|
+
resp = ['[Releases in development]']
|
|
206
|
+
if len(self.__changelog.releases_in_dev) == 0:
|
|
207
|
+
resp.append("No release in development.\nUse `hop prepare`.")
|
|
208
|
+
for release in self.__changelog.releases_in_dev:
|
|
209
|
+
resp.append(f'- {release} (branch hop_{release})')
|
|
210
|
+
else:
|
|
211
|
+
resp = ['[Releases to apply]']
|
|
212
|
+
if len(self.__changelog.releases_to_apply_in_prod) == 0:
|
|
213
|
+
resp.append("No new release to apply.")
|
|
214
|
+
for release in self.__changelog.releases_to_apply_in_prod:
|
|
215
|
+
resp.append(f'- {release}')
|
|
216
|
+
return '\n'.join(resp)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def undo(self, database_only=False):
|
|
220
|
+
"Undo a patch."
|
|
221
|
+
db_release = self.__repo.database.last_release_s
|
|
222
|
+
previous_release = self.previous(db_release, 1)
|
|
223
|
+
self.__restore_db(previous_release)
|
|
224
|
+
if not database_only:
|
|
225
|
+
modules.generate(self.__repo)
|
|
226
|
+
os.remove(self.__backup_file('Backups', previous_release))
|
|
227
|
+
|
|
228
|
+
def sync_package(self):
|
|
229
|
+
"Synchronise the package with the current database model"
|
|
230
|
+
modules.generate(self.__repo)
|
|
231
|
+
|
|
232
|
+
def release(self, push):
|
|
233
|
+
"Release a patch"
|
|
234
|
+
raise Exception("Deprecated legacy release")
|
|
235
|
+
# We must be on the first branch in devel (see CHANGELOG)
|
|
236
|
+
next_release = self.__repo.changelog.releases_in_dev[0]
|
|
237
|
+
next_branch = f'hop_{next_release}'
|
|
238
|
+
if next_branch != self.__repo.hgit.branch:
|
|
239
|
+
utils.error(f'Next release is {next_release} Please switch to the branch {next_branch}!\n', 1)
|
|
240
|
+
# Git repo must be clean
|
|
241
|
+
if not self.__repo.hgit.repos_is_clean():
|
|
242
|
+
utils.error(
|
|
243
|
+
f'Please `git commit` your changes before releasing {next_release}.\n', exit_code=1)
|
|
244
|
+
# The patch must be applied and the last to apply
|
|
245
|
+
if self.__repo.database.last_release_s != next_release:
|
|
246
|
+
utils.error(f'Please `hop apply` before releasing {next_release}.\n', exit_code=1)
|
|
247
|
+
# If we undo the patch (db only) and re-apply it the repo must still be clear.
|
|
248
|
+
self.undo(database_only=True)
|
|
249
|
+
self.apply(next_release, force=True)
|
|
250
|
+
if not self.__repo.hgit.repos_is_clean():
|
|
251
|
+
utils.error(
|
|
252
|
+
'Something has changed when re-applying the release. This should not happen.\n',
|
|
253
|
+
exit_code=1)
|
|
254
|
+
# do we have pytest
|
|
255
|
+
if PYTEST_OK:
|
|
256
|
+
try:
|
|
257
|
+
subprocess.run(['pytest', self.__repo.name], check=True)
|
|
258
|
+
except subprocess.CalledProcessError:
|
|
259
|
+
utils.error('Tests must pass in order to release.\n', exit_code=1)
|
|
260
|
+
# So far, so good
|
|
261
|
+
svg_file = self.__backup_file('Releases', next_release)
|
|
262
|
+
print(f'Saving the database into {svg_file}')
|
|
263
|
+
self.__repo.database.execute_pg_command(
|
|
264
|
+
'pg_dump', '-xO', '-f', svg_file, stderr=subprocess.PIPE)
|
|
265
|
+
self.__repo.hgit.add(svg_file)
|
|
266
|
+
self.__repo.hgit.commit("-m", f"Add sql for release {next_release}")
|
|
267
|
+
self.__repo.hgit.rebase_to_hop_main(push)
|
|
268
|
+
else:
|
|
269
|
+
utils.error('pytest is not installed!\n', 1)
|
|
270
|
+
|
|
271
|
+
def upgrade_prod(self):
|
|
272
|
+
"Upgrade the production"
|
|
273
|
+
self.__assert_main_branch()
|
|
274
|
+
self.__save_db(self.__repo.database.last_release_s)
|
|
275
|
+
for release in self.__repo.changelog.releases_to_apply_in_prod:
|
|
276
|
+
self.apply(release, save_db=False)
|
|
277
|
+
|
|
278
|
+
def restore(self, release):
|
|
279
|
+
"Restore the database and package to a release (in production)"
|
|
280
|
+
self.__restore_db(release)
|
|
281
|
+
# Do we have the backup
|