half-orm-dev 1.0.0a2__tar.gz → 1.0.0a5__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.
- {half_orm_dev-1.0.0a2/half_orm_dev.egg-info → half_orm_dev-1.0.0a5}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/__init__.py +2 -0
- half_orm_dev-1.0.0a5/half_orm_dev/cli/commands/revert_migration.py +49 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/main.py +2 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/hgit.py +13 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migration_manager.py +137 -8
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/modules.py +152 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/release_manager.py +45 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/repo.py +19 -3
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/module_template_1 +3 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/module_template_3 +4 -1
- half_orm_dev-1.0.0a5/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5/half_orm_dev.egg-info}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev.egg-info/SOURCES.txt +2 -1
- half_orm_dev-1.0.0a5/half_orm_dev.egg-info/entry_points.txt +2 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/pyproject.toml +4 -2
- half_orm_dev-1.0.0a2/half_orm_dev/migrations/half_orm/BREAKING_CHANGES-1.0.0.md +0 -50
- half_orm_dev-1.0.0a2/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/LICENSE +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/README.md +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/bootstrap_manager.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patch_manager.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/setup.py +0 -0
|
@@ -15,6 +15,7 @@ from .upgrade import upgrade
|
|
|
15
15
|
from .check import check
|
|
16
16
|
from .set_git_origin import set_git_origin
|
|
17
17
|
from .migrate import migrate
|
|
18
|
+
from .revert_migration import revert_migration
|
|
18
19
|
from .bootstrap import bootstrap
|
|
19
20
|
from .todo import apply_release
|
|
20
21
|
from .todo import rollback
|
|
@@ -35,6 +36,7 @@ ALL_COMMANDS = {
|
|
|
35
36
|
'check': check, # Project health check and updates
|
|
36
37
|
'set-git-origin': set_git_origin, # Update git remote origin URL
|
|
37
38
|
'migrate': migrate, # Repository migration after upgrade
|
|
39
|
+
'revert-migration': revert_migration, # Revert last migration
|
|
38
40
|
'bootstrap': bootstrap, # Execute data initialization scripts
|
|
39
41
|
# 🚧 (stubs)
|
|
40
42
|
'apply_release': apply_release,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
revert-migration command - Revert the last migration.
|
|
3
|
+
|
|
4
|
+
Uses the annotated git tag ho-migration/<version> created during migration
|
|
5
|
+
to identify the exact commits to revert on each branch.
|
|
6
|
+
|
|
7
|
+
Not available after a production promotion (the tag is deleted at that point).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from half_orm_dev.repo import Repo, RepoError
|
|
12
|
+
from half_orm_dev.migration_manager import MigrationManagerError
|
|
13
|
+
from half_orm import utils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command('revert-migration')
|
|
17
|
+
def revert_migration() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Revert the last migration applied by 'half_orm dev migrate'.
|
|
20
|
+
|
|
21
|
+
Uses the annotated tag ho-migration/<version> to locate the exact
|
|
22
|
+
commits and runs 'git revert --no-edit' on each affected branch.
|
|
23
|
+
|
|
24
|
+
\b
|
|
25
|
+
Constraints:
|
|
26
|
+
• Must be on ho-prod branch
|
|
27
|
+
• Not possible after a production promotion
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
Multiple migrations:
|
|
31
|
+
Call repeatedly to roll back a chain of migrations (LIFO order).
|
|
32
|
+
Each call reverts the migration with the highest version number.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
repo = Repo()
|
|
36
|
+
|
|
37
|
+
if not repo.checked:
|
|
38
|
+
click.echo(utils.Color.red("❌ Not in a hop repository"), err=True)
|
|
39
|
+
raise click.Abort()
|
|
40
|
+
|
|
41
|
+
repo.revert_migration()
|
|
42
|
+
click.echo(f"✓ {utils.Color.green('Migration reverted successfully.')}")
|
|
43
|
+
|
|
44
|
+
except (RepoError, MigrationManagerError) as e:
|
|
45
|
+
click.echo(utils.Color.red(f"❌ {e}"), err=True)
|
|
46
|
+
raise click.Abort()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
click.echo(utils.Color.red(f"❌ Unexpected error: {e}"), err=True)
|
|
49
|
+
raise click.Abort()
|
|
@@ -62,7 +62,8 @@ class Hop:
|
|
|
62
62
|
return ['update', 'upgrade', 'bootstrap']
|
|
63
63
|
else:
|
|
64
64
|
# DEVELOPMENT ENVIRONMENT - Patch development
|
|
65
|
-
return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin'
|
|
65
|
+
return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
|
|
66
|
+
'revert-migration']
|
|
66
67
|
|
|
67
68
|
@property
|
|
68
69
|
def repo_checked(self):
|
|
@@ -411,6 +411,19 @@ class HGit:
|
|
|
411
411
|
origin = self.__git_repo.remote('origin')
|
|
412
412
|
origin.push(tag_name)
|
|
413
413
|
|
|
414
|
+
def delete_remote_tag(self, tag_name: str) -> None:
|
|
415
|
+
"""
|
|
416
|
+
Delete tag from remote.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
tag_name: Tag name to delete (e.g., "ho-migration/1.0.0")
|
|
420
|
+
|
|
421
|
+
Examples:
|
|
422
|
+
hgit.delete_remote_tag("ho-migration/1.0.0")
|
|
423
|
+
"""
|
|
424
|
+
origin = self.__git_repo.remote('origin')
|
|
425
|
+
origin.push(refspec=f':refs/tags/{tag_name}')
|
|
426
|
+
|
|
414
427
|
def fetch_from_origin(self) -> None:
|
|
415
428
|
"""
|
|
416
429
|
Fetch all references from origin remote with pruning.
|
|
@@ -31,6 +31,11 @@ from typing import List, Dict, Optional, Tuple
|
|
|
31
31
|
from half_orm import utils
|
|
32
32
|
from half_orm_dev.decorators import with_dynamic_branch_lock
|
|
33
33
|
|
|
34
|
+
try:
|
|
35
|
+
from half_orm.migrations import get_breaking_changes_dir
|
|
36
|
+
except (ImportError, AttributeError):
|
|
37
|
+
get_breaking_changes_dir = None # type: ignore[assignment]
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
class MigrationManagerError(Exception):
|
|
36
41
|
"""Base exception for MigrationManager operations."""
|
|
@@ -195,22 +200,38 @@ class MigrationManager:
|
|
|
195
200
|
f"Migration {migration_file.name} missing migrate() function"
|
|
196
201
|
)
|
|
197
202
|
|
|
198
|
-
# Execute migration
|
|
203
|
+
# Execute migration.
|
|
204
|
+
# The repo is guaranteed clean before migration starts, so the
|
|
205
|
+
# git index is empty here. Everything staged by this script is
|
|
206
|
+
# exclusively a migration-induced change.
|
|
199
207
|
migration_result = module.migrate(self._repo)
|
|
200
208
|
|
|
201
209
|
result['applied_files'].append(migration_file.name)
|
|
202
210
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
# Auto-detect files staged by the script (via hgit.add()).
|
|
212
|
+
# Since the index was empty before migration, git diff --cached
|
|
213
|
+
# returns exactly the files this script modified.
|
|
214
|
+
git_repo = self._repo.hgit._HGit__git_repo
|
|
215
|
+
auto_staged = set(
|
|
216
|
+
git_repo.git.diff('--cached', '--name-only').splitlines()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Merge with sync_files explicitly declared by the script.
|
|
220
|
+
declared = (
|
|
221
|
+
list(migration_result.get('sync_files', []))
|
|
222
|
+
if isinstance(migration_result, dict) else []
|
|
223
|
+
)
|
|
224
|
+
all_sync = list(dict.fromkeys(declared + list(auto_staged)))
|
|
225
|
+
if all_sync:
|
|
226
|
+
result['sync_files'].extend(all_sync)
|
|
208
227
|
|
|
209
228
|
except Exception as e:
|
|
210
229
|
error_msg = f"Error in {migration_file.name}: {e}"
|
|
211
230
|
result['errors'].append(error_msg)
|
|
212
231
|
raise MigrationManagerError(error_msg) from e
|
|
213
232
|
|
|
233
|
+
# Deduplicate across all scripts (git diff --cached is cumulative)
|
|
234
|
+
result['sync_files'] = list(dict.fromkeys(result['sync_files']))
|
|
214
235
|
return result
|
|
215
236
|
|
|
216
237
|
@with_dynamic_branch_lock(lambda self, *args, **kwargs: 'ho-prod')
|
|
@@ -344,6 +365,11 @@ class MigrationManager:
|
|
|
344
365
|
result['commit_pushed'] = True
|
|
345
366
|
result['sync_result'] = sync_result
|
|
346
367
|
|
|
368
|
+
# Create annotated tag encoding all commit SHAs for potential revert
|
|
369
|
+
self._create_migration_tag(
|
|
370
|
+
current_version, target_version, sync_result
|
|
371
|
+
)
|
|
372
|
+
|
|
347
373
|
except Exception as e:
|
|
348
374
|
# Don't fail migration if commit fails
|
|
349
375
|
result['errors'].append(f"Failed to create commit: {e}")
|
|
@@ -355,6 +381,67 @@ class MigrationManager:
|
|
|
355
381
|
|
|
356
382
|
return result
|
|
357
383
|
|
|
384
|
+
@with_dynamic_branch_lock(lambda self, *args, **kwargs: 'ho-prod')
|
|
385
|
+
def revert_migration(self) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Revert the most recently tagged migration.
|
|
388
|
+
|
|
389
|
+
Acquires a lock on ho-prod (via decorator), finds the ho-migration/*
|
|
390
|
+
tag with the highest version, and runs `git revert --no-edit` on each
|
|
391
|
+
affected branch (active branches first, then ho-prod). The tag is
|
|
392
|
+
deleted (local + remote) after a successful revert.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
MigrationManagerError: if no migration tag exists (never migrated,
|
|
396
|
+
or already locked by a production promotion).
|
|
397
|
+
"""
|
|
398
|
+
git_repo = self._repo.hgit._HGit__git_repo
|
|
399
|
+
|
|
400
|
+
migration_tags = sorted(
|
|
401
|
+
[t for t in git_repo.tags if t.name.startswith('ho-migration/')],
|
|
402
|
+
key=lambda t: version.parse(t.name[len('ho-migration/'):]),
|
|
403
|
+
reverse=True,
|
|
404
|
+
)
|
|
405
|
+
if not migration_tags:
|
|
406
|
+
raise MigrationManagerError(
|
|
407
|
+
"No migration tag found — revert is not possible "
|
|
408
|
+
"(migration was never run, or already locked by a "
|
|
409
|
+
"production promotion)."
|
|
410
|
+
)
|
|
411
|
+
tag = migration_tags[0]
|
|
412
|
+
|
|
413
|
+
# Parse annotation: "Migration from X to Y\nho-prod:<sha>\nbranch:<sha>…"
|
|
414
|
+
shas: Dict = {}
|
|
415
|
+
for line in tag.tag.message.splitlines()[1:]:
|
|
416
|
+
if ':' in line:
|
|
417
|
+
branch, sha = line.split(':', 1)
|
|
418
|
+
shas[branch.strip()] = sha.strip()
|
|
419
|
+
|
|
420
|
+
if 'ho-prod' not in shas:
|
|
421
|
+
raise MigrationManagerError(
|
|
422
|
+
f"Migration tag {tag.name} is malformed (missing ho-prod SHA)."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Revert sync commits on active branches first
|
|
426
|
+
for branch, sha in shas.items():
|
|
427
|
+
if branch == 'ho-prod':
|
|
428
|
+
continue
|
|
429
|
+
git_repo.git.checkout(branch)
|
|
430
|
+
git_repo.git.revert(sha, '--no-edit')
|
|
431
|
+
self._repo.hgit.push_branch(branch)
|
|
432
|
+
|
|
433
|
+
# Revert migration commit on ho-prod last
|
|
434
|
+
git_repo.git.checkout('ho-prod')
|
|
435
|
+
git_repo.git.revert(shas['ho-prod'], '--no-edit')
|
|
436
|
+
self._repo.hgit.push_branch('ho-prod')
|
|
437
|
+
|
|
438
|
+
# Remove tag (local + remote)
|
|
439
|
+
self._repo.hgit.delete_local_tag(tag.name)
|
|
440
|
+
try:
|
|
441
|
+
self._repo.hgit.delete_remote_tag(tag.name)
|
|
442
|
+
except Exception:
|
|
443
|
+
pass # remote tag may already be gone
|
|
444
|
+
|
|
358
445
|
def _create_migration_commit_message(
|
|
359
446
|
self,
|
|
360
447
|
from_version: str,
|
|
@@ -389,6 +476,42 @@ class MigrationManager:
|
|
|
389
476
|
|
|
390
477
|
return '\n'.join(lines)
|
|
391
478
|
|
|
479
|
+
def _create_migration_tag(
|
|
480
|
+
self, from_version: str, to_version: str, sync_result: Dict
|
|
481
|
+
) -> None:
|
|
482
|
+
"""
|
|
483
|
+
Create annotated tag ho-migration/{to_version} encoding commit SHAs.
|
|
484
|
+
|
|
485
|
+
The annotation stores the ho-prod commit SHA and the SHA of each sync
|
|
486
|
+
commit on active branches, enabling revert_migration() to undo the
|
|
487
|
+
migration precisely.
|
|
488
|
+
|
|
489
|
+
If the tag already exists (e.g. a previous failed migration left it),
|
|
490
|
+
it is deleted first.
|
|
491
|
+
"""
|
|
492
|
+
tag_name = f"ho-migration/{to_version}"
|
|
493
|
+
|
|
494
|
+
# Remove stale tag if present
|
|
495
|
+
if self._repo.hgit.tag_exists(tag_name):
|
|
496
|
+
self._repo.hgit.delete_local_tag(tag_name)
|
|
497
|
+
try:
|
|
498
|
+
self._repo.hgit.delete_remote_tag(tag_name)
|
|
499
|
+
except Exception:
|
|
500
|
+
pass # remote tag may not exist
|
|
501
|
+
|
|
502
|
+
ho_prod_sha = self._repo.hgit._HGit__git_repo.head.commit.hexsha
|
|
503
|
+
branch_commits = (
|
|
504
|
+
sync_result.get('sync_result', {}).get('branch_commits', {})
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
lines = [f"Migration from {from_version} to {to_version}"]
|
|
508
|
+
lines.append(f"ho-prod:{ho_prod_sha}")
|
|
509
|
+
for branch, sha in branch_commits.items():
|
|
510
|
+
lines.append(f"{branch}:{sha}")
|
|
511
|
+
|
|
512
|
+
self._repo.hgit.create_tag(tag_name, message='\n'.join(lines))
|
|
513
|
+
self._repo.hgit.push_tag(tag_name)
|
|
514
|
+
|
|
392
515
|
def _update_pyproject_dependency_version(self, target_version: str) -> None:
|
|
393
516
|
"""
|
|
394
517
|
Update half_orm_dev version in pyproject.toml.
|
|
@@ -506,8 +629,14 @@ class MigrationManager:
|
|
|
506
629
|
except Exception:
|
|
507
630
|
return results
|
|
508
631
|
|
|
509
|
-
|
|
510
|
-
|
|
632
|
+
component_dirs = {'hop': self._migrations_root / 'hop'}
|
|
633
|
+
if get_breaking_changes_dir is not None:
|
|
634
|
+
try:
|
|
635
|
+
component_dirs['half_orm'] = get_breaking_changes_dir()
|
|
636
|
+
except Exception:
|
|
637
|
+
pass # older half-orm — ignore silently
|
|
638
|
+
|
|
639
|
+
for component, bc_dir in component_dirs.items():
|
|
511
640
|
if not bc_dir.is_dir():
|
|
512
641
|
continue
|
|
513
642
|
for bc_file in sorted(bc_dir.glob('BREAKING_CHANGES-*.md')):
|
|
@@ -48,6 +48,8 @@ HO_DATACLASSES = [
|
|
|
48
48
|
from half_orm.relation import DC_Relation
|
|
49
49
|
from half_orm.field import Field''']
|
|
50
50
|
HO_DATACLASSES_IMPORTS = set()
|
|
51
|
+
HO_TYPEDICTS: list = []
|
|
52
|
+
HO_TYPEDICTS_IMPORTS: set = set()
|
|
51
53
|
INIT_MODULE_TEMPLATE = read_template('init_module_template')
|
|
52
54
|
MODULE_TEMPLATE_1 = read_template('module_template_1')
|
|
53
55
|
MODULE_TEMPLATE_2 = read_template('module_template_2')
|
|
@@ -194,7 +196,12 @@ def __gen_dataclass(relation, fkeys):
|
|
|
194
196
|
# Invert user-defined aliases: constraint_name → alias
|
|
195
197
|
aliases = {constraint: alias for alias, constraint in fkeys.items() if alias != ''}
|
|
196
198
|
for constraint_name, fkey in rel._ho_fkeys.items():
|
|
197
|
-
|
|
199
|
+
if constraint_name in aliases:
|
|
200
|
+
attr_name = aliases[constraint_name]
|
|
201
|
+
elif constraint_name.startswith('_reverse_fkey_'):
|
|
202
|
+
attr_name = 'rfk_' + constraint_name[len('_reverse_fkey_'):]
|
|
203
|
+
else:
|
|
204
|
+
attr_name = 'fk_' + constraint_name
|
|
198
205
|
try:
|
|
199
206
|
fk_fqrn = list(fkey()._t_fqrn)
|
|
200
207
|
fdc_name = f'DC_{__get_full_class_name(fk_fqrn[1], fk_fqrn[2])}'
|
|
@@ -204,6 +211,144 @@ def __gen_dataclass(relation, fkeys):
|
|
|
204
211
|
return '\n'.join([f'@dataclasses.dataclass\nclass {dc_name}(DC_Relation):'] + fields + post_init)
|
|
205
212
|
|
|
206
213
|
|
|
214
|
+
def __get_type_annotation(field) -> tuple:
|
|
215
|
+
"""Return (type_str, extra_imports) for a TypedDict field annotation.
|
|
216
|
+
|
|
217
|
+
Array types (SQL prefix '_') map to List[T].
|
|
218
|
+
"""
|
|
219
|
+
sql_type = field._metadata['fieldtype']
|
|
220
|
+
is_array = sql_type.startswith('_')
|
|
221
|
+
base_sql_type = sql_type[1:] if is_array else sql_type
|
|
222
|
+
|
|
223
|
+
py_type = SQL_ADAPTER.get(base_sql_type)
|
|
224
|
+
imports: set = set()
|
|
225
|
+
|
|
226
|
+
if py_type is None or py_type is Any:
|
|
227
|
+
type_str = 'Any'
|
|
228
|
+
elif py_type.__module__ != 'builtins':
|
|
229
|
+
imports.add(py_type.__module__)
|
|
230
|
+
name = py_type.__name__ if hasattr(py_type, '__name__') else 'Any'
|
|
231
|
+
type_str = f'{py_type.__module__}.{name}'
|
|
232
|
+
else:
|
|
233
|
+
type_str = py_type.__name__
|
|
234
|
+
|
|
235
|
+
if is_array:
|
|
236
|
+
return f'List[{type_str}]', imports
|
|
237
|
+
return type_str, imports
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def __json_scalar_type(sql_type_name: str) -> str:
|
|
241
|
+
"""Map a JSON schema scalar type name to a Python type string.
|
|
242
|
+
|
|
243
|
+
Updates HO_TYPEDICTS_IMPORTS as needed.
|
|
244
|
+
"""
|
|
245
|
+
py_type = SQL_ADAPTER.get(sql_type_name.lower())
|
|
246
|
+
if py_type is None or py_type is Any:
|
|
247
|
+
return 'Any'
|
|
248
|
+
if py_type.__module__ != 'builtins':
|
|
249
|
+
HO_TYPEDICTS_IMPORTS.add(py_type.__module__)
|
|
250
|
+
name = py_type.__name__ if hasattr(py_type, '__name__') else 'Any'
|
|
251
|
+
return f'{py_type.__module__}.{name}'
|
|
252
|
+
return py_type.__name__
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def __gen_json_typedicts(name_prefix: str, schema) -> tuple:
|
|
256
|
+
"""Recursively generate TypedDict classes from a Field.json_schema structure.
|
|
257
|
+
|
|
258
|
+
Returns (class_strings, top_class_name).
|
|
259
|
+
class_strings are in dependency order (nested classes before the class using them).
|
|
260
|
+
|
|
261
|
+
YAML value conventions:
|
|
262
|
+
scalar string → SQL type name (e.g. 'text', 'integer', 'uuid')
|
|
263
|
+
[scalar] → List[T]
|
|
264
|
+
[dict] → List[NestedDict]
|
|
265
|
+
dict → NestedDict
|
|
266
|
+
"""
|
|
267
|
+
if not isinstance(schema, dict):
|
|
268
|
+
return [], 'Any'
|
|
269
|
+
|
|
270
|
+
classes = []
|
|
271
|
+
fields = []
|
|
272
|
+
|
|
273
|
+
for key, val in schema.items():
|
|
274
|
+
if isinstance(val, str):
|
|
275
|
+
type_str = __json_scalar_type(val)
|
|
276
|
+
fields.append(f" {key}: Optional[{type_str}]")
|
|
277
|
+
elif isinstance(val, list) and len(val) == 1:
|
|
278
|
+
item = val[0]
|
|
279
|
+
if isinstance(item, str):
|
|
280
|
+
inner = __json_scalar_type(item)
|
|
281
|
+
fields.append(f" {key}: Optional[List[{inner}]]")
|
|
282
|
+
elif isinstance(item, dict):
|
|
283
|
+
child_prefix = name_prefix + ''.join(w.capitalize() for w in key.split('_'))
|
|
284
|
+
nested, child_name = __gen_json_typedicts(child_prefix, item)
|
|
285
|
+
classes.extend(nested)
|
|
286
|
+
fields.append(f" {key}: Optional[List['{child_name}']]")
|
|
287
|
+
else:
|
|
288
|
+
fields.append(f" {key}: Optional[Any]")
|
|
289
|
+
elif isinstance(val, dict):
|
|
290
|
+
child_prefix = name_prefix + ''.join(w.capitalize() for w in key.split('_'))
|
|
291
|
+
nested, child_name = __gen_json_typedicts(child_prefix, val)
|
|
292
|
+
classes.extend(nested)
|
|
293
|
+
fields.append(f" {key}: Optional['{child_name}']")
|
|
294
|
+
else:
|
|
295
|
+
fields.append(f" {key}: Optional[Any]")
|
|
296
|
+
|
|
297
|
+
class_name = f'{name_prefix}Dict'
|
|
298
|
+
body = '\n'.join(fields) if fields else ' pass'
|
|
299
|
+
classes.append(f'class {class_name}(TypedDict, total=False):\n{body}')
|
|
300
|
+
return classes, class_name
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def __gen_typedict(relation, fkeys) -> list:
|
|
304
|
+
"""Generate TypedDict class(es) for a relation.
|
|
305
|
+
|
|
306
|
+
Returns a list of class strings: nested JSON TypedDicts first, then the main class.
|
|
307
|
+
Only database columns are included — FK accessor attributes are not part of a row dict.
|
|
308
|
+
json/jsonb fields with a json_schema generate nested TypedDict classes.
|
|
309
|
+
"""
|
|
310
|
+
rel = relation()
|
|
311
|
+
t_qrn = list(rel._t_fqrn)[1:]
|
|
312
|
+
dict_class_name = f'{__get_full_class_name(*t_qrn)}Dict'
|
|
313
|
+
|
|
314
|
+
extra_classes = []
|
|
315
|
+
fields = []
|
|
316
|
+
for field_name, field in rel._ho_fields.items():
|
|
317
|
+
json_schema = getattr(field, 'json_schema', None)
|
|
318
|
+
if json_schema is not None and isinstance(json_schema, dict):
|
|
319
|
+
field_cc = ''.join(w.capitalize() for w in field_name.split('_'))
|
|
320
|
+
json_classes, top_name = __gen_json_typedicts(
|
|
321
|
+
f'{dict_class_name[:-4]}{field_cc}', json_schema
|
|
322
|
+
)
|
|
323
|
+
extra_classes.extend(json_classes)
|
|
324
|
+
type_str = top_name
|
|
325
|
+
else:
|
|
326
|
+
type_str, imports = __get_type_annotation(field)
|
|
327
|
+
HO_TYPEDICTS_IMPORTS.update(imports)
|
|
328
|
+
line = f" {field_name}: Optional[{type_str}]"
|
|
329
|
+
error = utils.check_attribute_name(field_name)
|
|
330
|
+
if error:
|
|
331
|
+
line = f"# {line} # FIX ME! {error}"
|
|
332
|
+
fields.append(line)
|
|
333
|
+
|
|
334
|
+
body = '\n'.join(fields) if fields else ' pass'
|
|
335
|
+
main_class = f'class {dict_class_name}(TypedDict, total=False):\n{body}'
|
|
336
|
+
return extra_classes + [main_class]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def __gen_typedicts(package_dir: str, package_name: str) -> None:
|
|
340
|
+
with open(os.path.join(package_dir, "ho_typeddicts.py"), "w", encoding='utf-8') as file_:
|
|
341
|
+
file_.write(f"# TypedDicts for {package_name}\n\n")
|
|
342
|
+
file_.write("from __future__ import annotations\n")
|
|
343
|
+
file_.write("from typing import TypedDict, Optional, List, Any\n")
|
|
344
|
+
td_imports = sorted(HO_TYPEDICTS_IMPORTS)
|
|
345
|
+
for mod in td_imports:
|
|
346
|
+
file_.write(f"import {mod}\n")
|
|
347
|
+
file_.write("\n")
|
|
348
|
+
for td in HO_TYPEDICTS:
|
|
349
|
+
file_.write(f"\n{td}\n")
|
|
350
|
+
|
|
351
|
+
|
|
207
352
|
def __get_modules_list(dir, files_list, files):
|
|
208
353
|
all_ = []
|
|
209
354
|
for file_ in files:
|
|
@@ -357,6 +502,9 @@ def __update_this_module(
|
|
|
357
502
|
inheritance_import, inherited_classes = __get_inheritance_info(
|
|
358
503
|
rel, package_name)
|
|
359
504
|
|
|
505
|
+
t_qrn = list(rel._t_fqrn)[1:]
|
|
506
|
+
dict_class_name = f'{__get_full_class_name(*t_qrn)}Dict'
|
|
507
|
+
|
|
360
508
|
# Generate Python module
|
|
361
509
|
with open(module_path, 'w', encoding='utf-8') as file_:
|
|
362
510
|
documentation = "\n".join([line and f" {line}" or "" for line in str(rel).split("\n")[1:]])
|
|
@@ -370,6 +518,7 @@ def __update_this_module(
|
|
|
370
518
|
inherited_classes=inherited_classes,
|
|
371
519
|
class_name=class_name,
|
|
372
520
|
dc_name=rel._ho_dataclass_name(),
|
|
521
|
+
dict_class_name=dict_class_name,
|
|
373
522
|
fqtn=fqtn,
|
|
374
523
|
kwargs=kwargs,
|
|
375
524
|
arg_names=arg_names,
|
|
@@ -394,6 +543,7 @@ def __update_this_module(
|
|
|
394
543
|
class_name=class_name))
|
|
395
544
|
|
|
396
545
|
HO_DATACLASSES.append(__gen_dataclass(rel, existing_fkeys))
|
|
546
|
+
HO_TYPEDICTS.extend(__gen_typedict(rel, existing_fkeys))
|
|
397
547
|
|
|
398
548
|
return module_path
|
|
399
549
|
|
|
@@ -469,6 +619,7 @@ def generate(repo):
|
|
|
469
619
|
# Tests are no longer added to files_list (they live in tests/ directory)
|
|
470
620
|
|
|
471
621
|
__gen_dataclasses(str(package_dir), package_name)
|
|
622
|
+
__gen_typedicts(str(package_dir), package_name)
|
|
472
623
|
|
|
473
624
|
if len(NO_APAPTER):
|
|
474
625
|
print("MISSING ADAPTER FOR SQL TYPE")
|
|
@@ -2497,6 +2497,12 @@ class ReleaseManager:
|
|
|
2497
2497
|
|
|
2498
2498
|
# Save original branch for rollback
|
|
2499
2499
|
original_branch = self._repo.hgit.branch
|
|
2500
|
+
# Save schema.sql symlink target so rollback can restore it if
|
|
2501
|
+
# _generate_schema_sql updates it before a failure.
|
|
2502
|
+
_schema_sql = Path(self._repo.model_dir) / 'schema.sql'
|
|
2503
|
+
original_schema_link = (
|
|
2504
|
+
os.readlink(str(_schema_sql)) if _schema_sql.is_symlink() else None
|
|
2505
|
+
)
|
|
2500
2506
|
|
|
2501
2507
|
try:
|
|
2502
2508
|
# 5. Create temporary branch from release branch (validation only)
|
|
@@ -2547,6 +2553,15 @@ class ReleaseManager:
|
|
|
2547
2553
|
staged_patches = release_file.get_patches(status="staged")
|
|
2548
2554
|
self._create_prod_snapshot(version, staged_patches, release_file, model_dir)
|
|
2549
2555
|
|
|
2556
|
+
# 10b. Passe 2: validate all bootstrap scripts against final schema.
|
|
2557
|
+
# schema.sql now points to schema-X.Y.Z.sql (just generated).
|
|
2558
|
+
# restore_database_from_schema() resets the DB and runs ALL pending
|
|
2559
|
+
# bootstrap scripts in order — exactly what a fresh clone does.
|
|
2560
|
+
# Any failure here aborts promotion before the commit.
|
|
2561
|
+
print(f"\n🔍 Validating bootstrap scripts against schema {version}...")
|
|
2562
|
+
self._repo.restore_database_from_schema()
|
|
2563
|
+
print(f"✓ Bootstrap validation passed")
|
|
2564
|
+
|
|
2550
2565
|
# 11. Commit the merge with all promote changes included
|
|
2551
2566
|
self._repo.hgit.add(".")
|
|
2552
2567
|
self._repo.hgit.commit("-m", commit_msg)
|
|
@@ -2600,6 +2615,16 @@ class ReleaseManager:
|
|
|
2600
2615
|
result['deleted_branches'] = deleted_branches
|
|
2601
2616
|
result['migrated_to'] = next_patch_version if migrate_candidates else None
|
|
2602
2617
|
result['migrated_patches'] = candidates if migrate_candidates else []
|
|
2618
|
+
|
|
2619
|
+
# Lock migration revert: delete all ho-migration/* tags so that
|
|
2620
|
+
# revert_migration() is no longer possible after going to production.
|
|
2621
|
+
for tag in list(self._repo.hgit._HGit__git_repo.tags):
|
|
2622
|
+
if tag.name.startswith('ho-migration/'):
|
|
2623
|
+
self._repo.hgit.delete_local_tag(tag.name)
|
|
2624
|
+
try:
|
|
2625
|
+
self._repo.hgit.delete_remote_tag(tag.name)
|
|
2626
|
+
except Exception:
|
|
2627
|
+
pass # remote tag may already be gone
|
|
2603
2628
|
else:
|
|
2604
2629
|
result['branch'] = release_branch
|
|
2605
2630
|
|
|
@@ -2615,6 +2640,15 @@ class ReleaseManager:
|
|
|
2615
2640
|
except GitCommandError:
|
|
2616
2641
|
pass
|
|
2617
2642
|
|
|
2643
|
+
# Restore tracked files deleted/modified outside of git control
|
|
2644
|
+
# during the merge window (by _generate_schema_sql and
|
|
2645
|
+
# _create_prod_snapshot). merge_abort resets the index but
|
|
2646
|
+
# not working-tree files that were deleted directly on disk.
|
|
2647
|
+
try:
|
|
2648
|
+
self._repo.hgit.checkout('HEAD', '--', '.')
|
|
2649
|
+
except GitCommandError:
|
|
2650
|
+
pass
|
|
2651
|
+
|
|
2618
2652
|
# Return to original branch
|
|
2619
2653
|
try:
|
|
2620
2654
|
self._repo.hgit.checkout(original_branch)
|
|
@@ -2631,6 +2665,17 @@ class ReleaseManager:
|
|
|
2631
2665
|
except GitCommandError:
|
|
2632
2666
|
pass # Already deleted (prod validation complete) or never created
|
|
2633
2667
|
|
|
2668
|
+
# Restore schema.sql symlink if _generate_schema_sql changed it
|
|
2669
|
+
# before the failure (merge_abort does not undo symlink changes
|
|
2670
|
+
# made outside of the merge commit itself).
|
|
2671
|
+
if original_schema_link is not None:
|
|
2672
|
+
try:
|
|
2673
|
+
if _schema_sql.is_symlink() and os.readlink(str(_schema_sql)) != original_schema_link:
|
|
2674
|
+
_schema_sql.unlink()
|
|
2675
|
+
_schema_sql.symlink_to(original_schema_link)
|
|
2676
|
+
except OSError:
|
|
2677
|
+
pass
|
|
2678
|
+
|
|
2634
2679
|
except (GitCommandError, TypeError) as cleanup_error:
|
|
2635
2680
|
print(f" Warning: Cleanup failed: {cleanup_error}", file=sys.stderr)
|
|
2636
2681
|
|
|
@@ -621,7 +621,8 @@ class Repo:
|
|
|
621
621
|
result = {
|
|
622
622
|
'synced_branches': [],
|
|
623
623
|
'skipped_branches': [],
|
|
624
|
-
'errors': []
|
|
624
|
+
'errors': [],
|
|
625
|
+
'branch_commits': {}, # branch → commit SHA of the sync commit created
|
|
625
626
|
}
|
|
626
627
|
|
|
627
628
|
if not self.hgit:
|
|
@@ -798,6 +799,11 @@ class Repo:
|
|
|
798
799
|
commit_msg = f"[HOP] Sync .hop/ from {source_branch} ({reason})"
|
|
799
800
|
self.hgit.commit('-m', commit_msg)
|
|
800
801
|
|
|
802
|
+
# Record the SHA of the sync commit for potential revert
|
|
803
|
+
result['branch_commits'][branch] = (
|
|
804
|
+
self.hgit._HGit__git_repo.head.commit.hexsha
|
|
805
|
+
)
|
|
806
|
+
|
|
801
807
|
# Push to remote
|
|
802
808
|
self.hgit.push_branch(branch)
|
|
803
809
|
|
|
@@ -862,12 +868,14 @@ class Repo:
|
|
|
862
868
|
|
|
863
869
|
# Check if working directory is clean
|
|
864
870
|
if git_repo.is_dirty(untracked_files=False):
|
|
871
|
+
status = git_repo.git.status('--short')
|
|
865
872
|
raise RepoError(
|
|
866
873
|
f"Working directory has uncommitted changes.\n"
|
|
867
874
|
f"Please commit or stash your changes before running this command:\n"
|
|
868
875
|
f" git stash\n"
|
|
869
876
|
f" OR\n"
|
|
870
|
-
f" git add . && git commit -m \"your message\""
|
|
877
|
+
f" git add . && git commit -m \"your message\"\n"
|
|
878
|
+
f"Dirty files:\n{status}"
|
|
871
879
|
)
|
|
872
880
|
|
|
873
881
|
# Switch to ho-prod temporarily
|
|
@@ -988,6 +996,11 @@ class Repo:
|
|
|
988
996
|
|
|
989
997
|
return result
|
|
990
998
|
|
|
999
|
+
def revert_migration(self) -> None:
|
|
1000
|
+
"""Revert the most recently tagged migration. Delegates to MigrationManager."""
|
|
1001
|
+
mgr = MigrationManager(self)
|
|
1002
|
+
mgr.revert_migration()
|
|
1003
|
+
|
|
991
1004
|
@property
|
|
992
1005
|
def base_dir(self):
|
|
993
1006
|
"Returns the base dir of the repository"
|
|
@@ -2483,11 +2496,14 @@ Each script is executed only once unless `--force` is used.
|
|
|
2483
2496
|
# up_to_version prevents bootstraps from future versions from running.
|
|
2484
2497
|
from half_orm_dev.bootstrap_manager import BootstrapManager
|
|
2485
2498
|
bootstrap_mgr = BootstrapManager(self)
|
|
2486
|
-
bootstrap_mgr.run_bootstrap(
|
|
2499
|
+
result = bootstrap_mgr.run_bootstrap(
|
|
2487
2500
|
exclude_patch_id=exclude_bootstrap_patch_id,
|
|
2488
2501
|
exclude_version=exclude_bootstrap_version,
|
|
2489
2502
|
up_to_version=exclude_bootstrap_version
|
|
2490
2503
|
)
|
|
2504
|
+
if result['errors']:
|
|
2505
|
+
error_msg = "\n".join([f" • {f}: {e}" for f, e in result['errors']])
|
|
2506
|
+
raise RepoError(f"Bootstrap execution failed:\n{error_msg}")
|
|
2491
2507
|
|
|
2492
2508
|
except RepoError:
|
|
2493
2509
|
# Re-raise RepoError as-is
|
|
@@ -6,7 +6,10 @@ WARNING!
|
|
|
6
6
|
|
|
7
7
|
{warning}
|
|
8
8
|
"""
|
|
9
|
+
from typing import TYPE_CHECKING, Iterator
|
|
9
10
|
from half_orm.model import register
|
|
10
11
|
from {package_name} import MODEL, ho_dataclasses
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from {package_name}.ho_typeddicts import {dict_class_name}
|
|
11
14
|
fields_aliases=None
|
|
12
15
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
#pylint: disable=line-too-long, too-many-arguments, redefined-builtin, too-many-positional-arguments
|
|
2
2
|
def __init__(self, {kwargs}):
|
|
3
|
-
super().__init__({arg_names}, **kwargs)
|
|
3
|
+
super().__init__({arg_names}, **kwargs)
|
|
4
|
+
|
|
5
|
+
def __iter__(self) -> 'Iterator[{dict_class_name}]':
|
|
6
|
+
return super().__iter__()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0-a5
|
|
@@ -22,6 +22,7 @@ half_orm_dev/version.txt
|
|
|
22
22
|
half_orm_dev.egg-info/PKG-INFO
|
|
23
23
|
half_orm_dev.egg-info/SOURCES.txt
|
|
24
24
|
half_orm_dev.egg-info/dependency_links.txt
|
|
25
|
+
half_orm_dev.egg-info/entry_points.txt
|
|
25
26
|
half_orm_dev.egg-info/requires.txt
|
|
26
27
|
half_orm_dev.egg-info/top_level.txt
|
|
27
28
|
half_orm_dev/cli/__init__.py
|
|
@@ -36,6 +37,7 @@ half_orm_dev/cli/commands/migrate.py
|
|
|
36
37
|
half_orm_dev/cli/commands/patch.py
|
|
37
38
|
half_orm_dev/cli/commands/release.py
|
|
38
39
|
half_orm_dev/cli/commands/restore.py
|
|
40
|
+
half_orm_dev/cli/commands/revert_migration.py
|
|
39
41
|
half_orm_dev/cli/commands/set_git_origin.py
|
|
40
42
|
half_orm_dev/cli/commands/sync.py
|
|
41
43
|
half_orm_dev/cli/commands/todo.py
|
|
@@ -50,7 +52,6 @@ half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py
|
|
|
50
52
|
half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py
|
|
51
53
|
half_orm_dev/migrations/0/18/0/00_add_async_support.py
|
|
52
54
|
half_orm_dev/migrations/0/18/0/01_update_default_tests.py
|
|
53
|
-
half_orm_dev/migrations/half_orm/BREAKING_CHANGES-1.0.0.md
|
|
54
55
|
half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md
|
|
55
56
|
half_orm_dev/patches/log
|
|
56
57
|
half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql
|
|
@@ -33,6 +33,9 @@ classifiers = [
|
|
|
33
33
|
]
|
|
34
34
|
requires-python = ">=3.9"
|
|
35
35
|
|
|
36
|
+
[project.scripts]
|
|
37
|
+
half_orm = "half_orm.cli:main"
|
|
38
|
+
|
|
36
39
|
[project.urls]
|
|
37
40
|
Homepage = "https://github.com/half-orm/half-orm-dev"
|
|
38
41
|
|
|
@@ -49,8 +52,7 @@ half_orm_dev = [
|
|
|
49
52
|
"patches/**/*",
|
|
50
53
|
"scripts/*",
|
|
51
54
|
"version.txt",
|
|
52
|
-
"migrations/hop/*.md"
|
|
53
|
-
"migrations/half_orm/*.md"
|
|
55
|
+
"migrations/hop/*.md"
|
|
54
56
|
]
|
|
55
57
|
|
|
56
58
|
[tool.setuptools.dynamic]
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# half-orm 1.0.0 — Breaking Changes
|
|
2
|
-
|
|
3
|
-
## `ho_get()` returns a `dict` and raises on 0 or >1 rows
|
|
4
|
-
|
|
5
|
-
`ho_get()` now returns a plain `dict` directly (no longer a Relation
|
|
6
|
-
object). It raises:
|
|
7
|
-
- `NotFoundError` if no row matches
|
|
8
|
-
- `MultipleRowsError` if more than one row matches
|
|
9
|
-
|
|
10
|
-
**Before:**
|
|
11
|
-
```python
|
|
12
|
-
obj = MyTable(id=1).ho_get() # returned a Relation
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
**After:**
|
|
16
|
-
```python
|
|
17
|
-
row = MyTable(id=1).ho_get() # returns dict, or raises
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
The async counterpart `ho_aget()` has been added with the same semantics.
|
|
21
|
-
|
|
22
|
-
## Deprecated query-builder setters removed
|
|
23
|
-
|
|
24
|
-
`ho_limit`, `ho_offset`, `ho_order_by`, `ho_distinct` no longer exist as
|
|
25
|
-
property setters. Pass them as keyword arguments to `ho_select()`.
|
|
26
|
-
|
|
27
|
-
**Before:**
|
|
28
|
-
```python
|
|
29
|
-
rel.ho_limit = 10
|
|
30
|
-
rel.ho_order_by = "name"
|
|
31
|
-
for row in rel.ho_select():
|
|
32
|
-
...
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**After:**
|
|
36
|
-
```python
|
|
37
|
-
for row in rel.ho_select(limit=10, order_by="name"):
|
|
38
|
-
...
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## `FKEYS_PROPERTIES` / `FKEYS` class attributes removed
|
|
42
|
-
|
|
43
|
-
Use `Fkeys` only. Any subclass that still defines `FKEYS_PROPERTIES` or
|
|
44
|
-
`FKEYS` will raise an error at class definition time.
|
|
45
|
-
|
|
46
|
-
## `ho_cast()` raises `CastError` for invalid inheritance targets
|
|
47
|
-
|
|
48
|
-
`ho_cast(TargetClass)` now raises `half_orm.relation.CastError` if
|
|
49
|
-
`TargetClass` is not in the PostgreSQL inheritance hierarchy of the
|
|
50
|
-
source table.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0-a2
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py
RENAMED
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py
RENAMED
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/0/18/0/00_add_async_support.py
RENAMED
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a5}/half_orm_dev/templates/git-hooks/prepare-commit-msg
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|