half-orm-dev 1.0.0a2__tar.gz → 1.0.0a4__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.0a4}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/__init__.py +2 -0
- half_orm_dev-1.0.0a4/half_orm_dev/cli/commands/revert_migration.py +49 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/main.py +2 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/hgit.py +13 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/migration_manager.py +137 -8
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/modules.py +167 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/release_manager.py +10 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/repo.py +12 -1
- half_orm_dev-1.0.0a4/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4/half_orm_dev.egg-info}/PKG-INFO +1 -1
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev.egg-info/SOURCES.txt +2 -1
- half_orm_dev-1.0.0a4/half_orm_dev.egg-info/entry_points.txt +2 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/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.0a4}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/LICENSE +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/README.md +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/bootstrap_manager.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/bootstrap.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/check.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/update.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/decorators.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/file_executor.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/patch_manager.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/patch_validator.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/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.0a4}/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.0a4}/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.0a4}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/release_file.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev.egg-info/requires.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/setup.cfg +0 -0
- {half_orm_dev-1.0.0a2 → half_orm_dev-1.0.0a4}/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,163 @@ 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
|
+
FK fields starting with 'rfk_' (reverse) are typed List['TargetDict'];
|
|
308
|
+
all other FK fields are typed 'TargetDict'.
|
|
309
|
+
json/jsonb fields with a json_schema generate nested TypedDict classes.
|
|
310
|
+
"""
|
|
311
|
+
rel = relation()
|
|
312
|
+
t_qrn = list(rel._t_fqrn)[1:]
|
|
313
|
+
dict_class_name = f'{__get_full_class_name(*t_qrn)}Dict'
|
|
314
|
+
|
|
315
|
+
extra_classes = []
|
|
316
|
+
fields = []
|
|
317
|
+
for field_name, field in rel._ho_fields.items():
|
|
318
|
+
json_schema = getattr(field, 'json_schema', None)
|
|
319
|
+
if json_schema is not None and isinstance(json_schema, dict):
|
|
320
|
+
field_cc = ''.join(w.capitalize() for w in field_name.split('_'))
|
|
321
|
+
json_classes, top_name = __gen_json_typedicts(
|
|
322
|
+
f'{dict_class_name[:-4]}{field_cc}', json_schema
|
|
323
|
+
)
|
|
324
|
+
extra_classes.extend(json_classes)
|
|
325
|
+
type_str = top_name
|
|
326
|
+
else:
|
|
327
|
+
type_str, imports = __get_type_annotation(field)
|
|
328
|
+
HO_TYPEDICTS_IMPORTS.update(imports)
|
|
329
|
+
line = f" {field_name}: Optional[{type_str}]"
|
|
330
|
+
error = utils.check_attribute_name(field_name)
|
|
331
|
+
if error:
|
|
332
|
+
line = f"# {line} # FIX ME! {error}"
|
|
333
|
+
fields.append(line)
|
|
334
|
+
|
|
335
|
+
aliases = {constraint: alias for alias, constraint in fkeys.items() if alias != ''}
|
|
336
|
+
for constraint_name, fkey in rel._ho_fkeys.items():
|
|
337
|
+
if constraint_name in aliases:
|
|
338
|
+
attr_name = aliases[constraint_name]
|
|
339
|
+
elif constraint_name.startswith('_reverse_fkey_'):
|
|
340
|
+
attr_name = 'rfk_' + constraint_name[len('_reverse_fkey_'):]
|
|
341
|
+
else:
|
|
342
|
+
attr_name = 'fk_' + constraint_name
|
|
343
|
+
try:
|
|
344
|
+
fk_fqrn = list(fkey()._t_fqrn)
|
|
345
|
+
target_name = f'{__get_full_class_name(fk_fqrn[1], fk_fqrn[2])}Dict'
|
|
346
|
+
except Exception:
|
|
347
|
+
target_name = dict_class_name
|
|
348
|
+
if attr_name.startswith('rfk_'):
|
|
349
|
+
fields.append(f" {attr_name}: Optional[List['{target_name}']]")
|
|
350
|
+
else:
|
|
351
|
+
fields.append(f" {attr_name}: Optional['{target_name}']")
|
|
352
|
+
|
|
353
|
+
body = '\n'.join(fields) if fields else ' pass'
|
|
354
|
+
main_class = f'class {dict_class_name}(TypedDict, total=False):\n{body}'
|
|
355
|
+
return extra_classes + [main_class]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def __gen_typedicts(package_dir: str, package_name: str) -> None:
|
|
359
|
+
with open(os.path.join(package_dir, "ho_typeddicts.py"), "w", encoding='utf-8') as file_:
|
|
360
|
+
file_.write(f"# TypedDicts for {package_name}\n\n")
|
|
361
|
+
file_.write("from __future__ import annotations\n")
|
|
362
|
+
file_.write("from typing import TypedDict, Optional, List, Any\n")
|
|
363
|
+
td_imports = sorted(HO_TYPEDICTS_IMPORTS)
|
|
364
|
+
for mod in td_imports:
|
|
365
|
+
file_.write(f"import {mod}\n")
|
|
366
|
+
file_.write("\n")
|
|
367
|
+
for td in HO_TYPEDICTS:
|
|
368
|
+
file_.write(f"\n{td}\n")
|
|
369
|
+
|
|
370
|
+
|
|
207
371
|
def __get_modules_list(dir, files_list, files):
|
|
208
372
|
all_ = []
|
|
209
373
|
for file_ in files:
|
|
@@ -394,6 +558,7 @@ def __update_this_module(
|
|
|
394
558
|
class_name=class_name))
|
|
395
559
|
|
|
396
560
|
HO_DATACLASSES.append(__gen_dataclass(rel, existing_fkeys))
|
|
561
|
+
HO_TYPEDICTS.extend(__gen_typedict(rel, existing_fkeys))
|
|
397
562
|
|
|
398
563
|
return module_path
|
|
399
564
|
|
|
@@ -469,6 +634,7 @@ def generate(repo):
|
|
|
469
634
|
# Tests are no longer added to files_list (they live in tests/ directory)
|
|
470
635
|
|
|
471
636
|
__gen_dataclasses(str(package_dir), package_name)
|
|
637
|
+
__gen_typedicts(str(package_dir), package_name)
|
|
472
638
|
|
|
473
639
|
if len(NO_APAPTER):
|
|
474
640
|
print("MISSING ADAPTER FOR SQL TYPE")
|
|
@@ -2600,6 +2600,16 @@ class ReleaseManager:
|
|
|
2600
2600
|
result['deleted_branches'] = deleted_branches
|
|
2601
2601
|
result['migrated_to'] = next_patch_version if migrate_candidates else None
|
|
2602
2602
|
result['migrated_patches'] = candidates if migrate_candidates else []
|
|
2603
|
+
|
|
2604
|
+
# Lock migration revert: delete all ho-migration/* tags so that
|
|
2605
|
+
# revert_migration() is no longer possible after going to production.
|
|
2606
|
+
for tag in list(self._repo.hgit._HGit__git_repo.tags):
|
|
2607
|
+
if tag.name.startswith('ho-migration/'):
|
|
2608
|
+
self._repo.hgit.delete_local_tag(tag.name)
|
|
2609
|
+
try:
|
|
2610
|
+
self._repo.hgit.delete_remote_tag(tag.name)
|
|
2611
|
+
except Exception:
|
|
2612
|
+
pass # remote tag may already be gone
|
|
2603
2613
|
else:
|
|
2604
2614
|
result['branch'] = release_branch
|
|
2605
2615
|
|
|
@@ -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
|
|
|
@@ -988,6 +994,11 @@ class Repo:
|
|
|
988
994
|
|
|
989
995
|
return result
|
|
990
996
|
|
|
997
|
+
def revert_migration(self) -> None:
|
|
998
|
+
"""Revert the most recently tagged migration. Delegates to MigrationManager."""
|
|
999
|
+
mgr = MigrationManager(self)
|
|
1000
|
+
mgr.revert_migration()
|
|
1001
|
+
|
|
991
1002
|
@property
|
|
992
1003
|
def base_dir(self):
|
|
993
1004
|
"Returns the base dir of the repository"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0-a4
|
|
@@ -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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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.0a4}/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
|
|
File without changes
|
|
File without changes
|