half-orm-dev 0.17.2a5__tar.gz → 0.17.2a7__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.
Files changed (67) hide show
  1. {half_orm_dev-0.17.2a5/half_orm_dev.egg-info → half_orm_dev-0.17.2a7}/PKG-INFO +1 -1
  2. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/__init__.py +3 -0
  3. half_orm_dev-0.17.2a7/half_orm_dev/cli/commands/migrate.py +125 -0
  4. half_orm_dev-0.17.2a7/half_orm_dev/cli/main.py +190 -0
  5. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/migration_manager.py +41 -43
  6. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/repo.py +172 -32
  7. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/git-hooks/pre-commit +37 -0
  8. half_orm_dev-0.17.2a7/half_orm_dev/version.txt +1 -0
  9. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7/half_orm_dev.egg-info}/PKG-INFO +1 -1
  10. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  11. half_orm_dev-0.17.2a5/half_orm_dev/cli/main.py +0 -103
  12. half_orm_dev-0.17.2a5/half_orm_dev/version.txt +0 -1
  13. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/AUTHORS +0 -0
  14. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/LICENSE +0 -0
  15. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/README.md +0 -0
  16. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/__init__.py +0 -0
  17. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/__init__.py +0 -0
  18. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/apply.py +0 -0
  19. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/check.py +0 -0
  20. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/clone.py +0 -0
  21. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/init.py +0 -0
  22. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/new.py +0 -0
  23. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/patch.py +0 -0
  24. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/release.py +0 -0
  25. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/restore.py +0 -0
  26. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/sync.py +0 -0
  27. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/todo.py +0 -0
  28. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/undo.py +0 -0
  29. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/update.py +0 -0
  30. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli/commands/upgrade.py +0 -0
  31. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/cli_extension.py +0 -0
  32. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/database.py +0 -0
  33. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/decorators.py +0 -0
  34. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/hgit.py +0 -0
  35. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  36. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  37. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/modules.py +0 -0
  38. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patch_manager.py +0 -0
  39. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patch_validator.py +0 -0
  40. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  41. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  42. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  43. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patches/log +0 -0
  44. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  45. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/release_file.py +0 -0
  46. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/release_manager.py +0 -0
  47. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/.gitignore +0 -0
  48. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/MANIFEST.in +0 -0
  49. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/Pipfile +0 -0
  50. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/README +0 -0
  51. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/conftest_template +0 -0
  52. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  53. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/init_module_template +0 -0
  54. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/module_template_1 +0 -0
  55. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/module_template_2 +0 -0
  56. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/module_template_3 +0 -0
  57. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/pyproject.toml +0 -0
  58. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/relation_test +0 -0
  59. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/sql_adapter +0 -0
  60. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/templates/warning +0 -0
  61. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev/utils.py +0 -0
  62. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  63. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev.egg-info/requires.txt +0 -0
  64. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/half_orm_dev.egg-info/top_level.txt +0 -0
  65. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/pyproject.toml +0 -0
  66. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/setup.cfg +0 -0
  67. {half_orm_dev-0.17.2a5 → half_orm_dev-0.17.2a7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.2a5
3
+ Version: 0.17.2a7
4
4
  Summary: half_orm development Framework.
5
5
  Author-email: Joël Maïzi <joel.maizi@collorg.org>
6
6
  License-Expression: GPL-3.0-or-later
@@ -13,6 +13,7 @@ from .release import release
13
13
  from .update import update
14
14
  from .upgrade import upgrade
15
15
  from .check import check
16
+ from .migrate import migrate
16
17
  from .todo import apply_release
17
18
  from .todo import rollback
18
19
 
@@ -30,6 +31,7 @@ ALL_COMMANDS = {
30
31
  'update': update, # Adapted for production
31
32
  'upgrade': upgrade, # Adapted for production
32
33
  'check': check, # Project health check and updates
34
+ 'migrate': migrate, # Repository migration after upgrade
33
35
  # 🚧 (stubs)
34
36
  'apply_release': apply_release,
35
37
 
@@ -49,6 +51,7 @@ __all__ = [
49
51
  'release',
50
52
  'upgrade',
51
53
  'check',
54
+ 'migrate',
52
55
  'rollback',
53
56
  # Adapted commands
54
57
  'sync_package',
@@ -0,0 +1,125 @@
1
+ """
2
+ Migrate command - Apply repository migrations after half_orm_dev upgrade.
3
+
4
+ This command runs pending migrations when the installed half_orm_dev version
5
+ is newer than the repository's hop_version in .hop/config.
6
+ """
7
+
8
+ import click
9
+ from half_orm_dev.repo import Repo, RepoError
10
+ from half_orm import utils
11
+
12
+
13
+ @click.command()
14
+ @click.option(
15
+ '--verbose', '-v',
16
+ is_flag=True,
17
+ help='Show detailed migration information'
18
+ )
19
+ def migrate(verbose: bool) -> None:
20
+ """
21
+ Apply repository migrations after half_orm_dev upgrade.
22
+
23
+ This command updates the repository structure and configuration files
24
+ when you upgrade to a newer version of half_orm_dev.
25
+
26
+ Requirements:
27
+ • Must be on ho-prod branch
28
+ • Repository must be clean (no uncommitted changes)
29
+
30
+ Process:
31
+ 1. Detects version mismatch between installed half_orm_dev and repository
32
+ 2. Applies any migration scripts for intermediate versions
33
+ 3. Updates hop_version in .hop/config
34
+ 4. Creates migration commit on ho-prod
35
+ 5. Syncs .hop/ directory to all active branches
36
+
37
+ Examples:
38
+ # After upgrading half_orm_dev
39
+ $ pip install --upgrade half_orm_dev
40
+ $ half_orm dev migrate
41
+ ⚠️ Migration needed: half_orm_dev 0.17.2 → 0.18.0
42
+ Current branch: ho-prod
43
+
44
+ Running migrations...
45
+ ✓ Applied migration: 0.17.2 → 0.18.0
46
+ ✓ Updated .hop/config: hop_version = 0.18.0
47
+ ✓ Synced .hop/ to active branches
48
+
49
+ # View detailed migration info
50
+ $ half_orm dev migrate --verbose
51
+ """
52
+ try:
53
+ repo = Repo()
54
+
55
+ # Check if we're in a repository
56
+ if not repo.checked:
57
+ click.echo(utils.Color.red("❌ Not in a hop repository"), err=True)
58
+ raise click.Abort()
59
+
60
+ # Get current versions
61
+ from half_orm_dev.utils import hop_version
62
+ installed_version = hop_version()
63
+ config_version = repo._Repo__config.hop_version if hasattr(repo, '_Repo__config') else '0.0.0'
64
+
65
+ # Check if migration is needed
66
+ comparison = repo.compare_versions(installed_version, config_version)
67
+
68
+ if comparison == 0:
69
+ # Versions are equal - no migration needed
70
+ click.echo(f"✓ {utils.Color.green('Repository is up to date')}")
71
+ click.echo(f" Repository version: {config_version}")
72
+ click.echo(f" Installed version: {installed_version}")
73
+ return
74
+
75
+ elif comparison < 0:
76
+ # Installed version is OLDER than repository version
77
+ click.echo(f"⚠️ {utils.Color.red('Installed half_orm_dev is older than repository version')}", err=True)
78
+ click.echo(f"\n Repository version: {config_version}", err=True)
79
+ click.echo(f" Installed version: {installed_version}", err=True)
80
+ click.echo(f"\n Please upgrade half_orm_dev:", err=True)
81
+ click.echo(f" pip install --upgrade half_orm_dev\n", err=True)
82
+ raise click.Abort()
83
+
84
+ # Migration needed (comparison > 0)
85
+ click.echo(f"⚠️ {utils.Color.bold('Migration needed:')}")
86
+ click.echo(f" half_orm_dev {config_version} → {installed_version}")
87
+
88
+ # Check current branch
89
+ current_branch = repo.hgit.branch if repo.hgit else 'unknown'
90
+ click.echo(f" Current branch: {current_branch}")
91
+ click.echo()
92
+
93
+ # Run migrations
94
+ click.echo(f" Running migrations...")
95
+
96
+ try:
97
+ result = repo.run_migrations_if_needed(silent=False)
98
+
99
+ if result['migration_run']:
100
+ click.echo(f"\n✓ {utils.Color.green('Migration completed successfully')}")
101
+ click.echo(f" Updated .hop/config: hop_version = {installed_version}")
102
+
103
+ if verbose and result.get('errors'):
104
+ click.echo(f"\n⚠️ Warnings during migration:")
105
+ for error in result['errors']:
106
+ click.echo(f" • {error}")
107
+
108
+ click.echo(f"\n✓ Synced .hop/ to active branches")
109
+ else:
110
+ click.echo(f"✓ {utils.Color.green('Repository is up to date')}")
111
+
112
+ except RepoError as e:
113
+ # Migration failed or branch check failed
114
+ click.echo(utils.Color.red(f"\n❌ {e}"), err=True)
115
+ raise click.Abort()
116
+
117
+ except RepoError as e:
118
+ click.echo(utils.Color.red(f"❌ Error: {e}"), err=True)
119
+ raise click.Abort()
120
+ except Exception as e:
121
+ click.echo(utils.Color.red(f"❌ Unexpected error: {e}"), err=True)
122
+ if verbose:
123
+ import traceback
124
+ traceback.print_exc()
125
+ raise click.Abort()
@@ -0,0 +1,190 @@
1
+ """
2
+ Main CLI module - Creates and configures the CLI group
3
+ """
4
+
5
+ import click
6
+ import functools
7
+ from half_orm_dev.repo import Repo, OutdatedHalfORMDevError
8
+ from half_orm import utils
9
+ from .commands import ALL_COMMANDS
10
+
11
+
12
+ class Hop:
13
+ """Sets the options available to the hop command"""
14
+
15
+ def __init__(self):
16
+ self.__repo: Repo = None
17
+ self.__hop_upgrade_error: OutdatedHalfORMDevError = None
18
+
19
+ # Try to initialize Repo, catch version errors
20
+ try:
21
+ self.__repo = Repo() # Utilise le singleton
22
+ except OutdatedHalfORMDevError as e:
23
+ # Capture the error but don't raise it yet
24
+ self.__hop_upgrade_error = e
25
+
26
+ self.__available_cmds = self._determine_available_commands()
27
+
28
+ def _determine_available_commands(self):
29
+ """
30
+ Determine which commands are available based on context.
31
+
32
+ Returns different command sets based on:
33
+ - Repository status (checked/unchecked)
34
+ - Development mode (devel flag - metadata presence)
35
+ - Environment (production flag)
36
+
37
+ Note: When needs_hop_upgrade is true, commands will still be added
38
+ but will be blocked by the decorator at execution time.
39
+ """
40
+ if self.needs_hop_upgrade:
41
+ # Version downgrade detected - return a minimal set of commands
42
+ # Commands will be blocked by decorator, but we need them in the list
43
+ # so Click doesn't show "No such command" error
44
+ return ['check', 'migrate']
45
+
46
+ if not self.repo_checked:
47
+ # Outside hop repository - commands for project initialization
48
+ return ['init', 'clone']
49
+
50
+ if self.__repo.needs_migration():
51
+ return ['migrate']
52
+
53
+ # Inside hop repository
54
+ if not self.__repo.devel:
55
+ # Sync-only mode (no metadata)
56
+ return ['sync-package', 'check']
57
+
58
+ # Development mode (metadata present)
59
+ if self.__repo.database.production:
60
+ # PRODUCTION ENVIRONMENT - Release deployment only
61
+ return ['update', 'upgrade', 'check']
62
+ else:
63
+ # DEVELOPMENT ENVIRONMENT - Patch development
64
+ return ['patch', 'release', 'check']
65
+
66
+ @property
67
+ def repo_checked(self):
68
+ """Returns whether we are in a repo or not."""
69
+ return self.__repo and self.__repo.checked
70
+
71
+ @property
72
+ def needs_hop_upgrade(self):
73
+ """Returns whether half_orm_dev needs to be upgraded."""
74
+ return self.__hop_upgrade_error is not None
75
+
76
+ @property
77
+ def hop_upgrade_error(self):
78
+ """Returns the upgrade error if any."""
79
+ return self.__hop_upgrade_error
80
+
81
+ @property
82
+ def state(self):
83
+ """Returns the state of the repo."""
84
+ return self.__repo.state if self.__repo else "Not in a repository"
85
+
86
+ @property
87
+ def available_commands(self):
88
+ """Returns the list of available commands."""
89
+ return self.__available_cmds
90
+
91
+
92
+ def create_cli_group():
93
+ """
94
+ Creates and returns the CLI group with appropriate commands.
95
+
96
+ Returns:
97
+ click.Group: Configured CLI group
98
+ """
99
+ hop = Hop()
100
+
101
+ def check_version_before_invoke(f, allow_on_downgrade=False):
102
+ """Decorator to check version before invoking any command.
103
+
104
+ Args:
105
+ f: Function to wrap
106
+ allow_on_downgrade: If True, allow this command even when version is outdated
107
+ """
108
+ @functools.wraps(f)
109
+ def wrapper(*args, **kwargs):
110
+ if hop.needs_hop_upgrade and not allow_on_downgrade:
111
+ # Display formatted error message for version downgrade
112
+ error = hop.hop_upgrade_error
113
+ click.echo("=" * 70, err=True)
114
+ click.echo(f"{utils.Color.red('❌ OUTDATED half_orm_dev VERSION')}", err=True)
115
+ click.echo("=" * 70, err=True)
116
+ click.echo(f"\n Repository requires: {utils.Color.bold(error.required_version)}", err=True)
117
+ click.echo(f" Installed version: {utils.Color.bold(error.installed_version)}", err=True)
118
+ click.echo(f"\n Your installed version is OLDER than the repository requirement.", err=True)
119
+ click.echo(f" All commands are blocked for safety.", err=True)
120
+ click.echo(f"\n Please upgrade half_orm_dev:", err=True)
121
+ click.echo(f" {utils.Color.bold('pip install --upgrade half_orm_dev')}", err=True)
122
+ click.echo("\n" + "=" * 70 + "\n", err=True)
123
+ raise click.Abort()
124
+ return f(*args, **kwargs)
125
+ return wrapper
126
+
127
+ # Create custom Group that auto-decorates commands
128
+ class VersionCheckGroup(click.Group):
129
+ def add_command(self, cmd, name=None):
130
+ """Override to decorate all commands with version check."""
131
+ if isinstance(cmd, click.Command) and cmd.callback:
132
+ cmd.callback = check_version_before_invoke(cmd.callback)
133
+ super().add_command(cmd, name)
134
+
135
+ @click.group(cls=VersionCheckGroup, invoke_without_command=True)
136
+ @click.pass_context
137
+ @check_version_before_invoke
138
+ def dev(ctx):
139
+ """halfORM development tools - Git-centric patch management and database synchronization"""
140
+ if ctx.invoked_subcommand is None:
141
+ # Show repo state when no subcommand is provided
142
+ if hop.repo_checked:
143
+ # Check if migration is needed
144
+ if hop._Hop__repo.needs_migration():
145
+ # Display migration warning
146
+ from half_orm_dev.utils import hop_version
147
+ installed_version = hop_version()
148
+ config_version = hop._Hop__repo._Repo__config.hop_version
149
+ current_branch = hop._Hop__repo.hgit.branch if hop._Hop__repo.hgit else 'unknown'
150
+
151
+ click.echo(f"\n{'='*70}")
152
+ click.echo(f"⚠️ {utils.Color.bold(utils.Color.red('REPOSITORY MIGRATION REQUIRED'))} ⚠️")
153
+ click.echo(f"{'='*70}")
154
+ click.echo(f"\n Repository version: {utils.Color.red(config_version)}")
155
+ click.echo(f" Installed version: {utils.Color.green(installed_version)}")
156
+ click.echo(f" Current branch: {current_branch}")
157
+ click.echo(f"\n {utils.Color.bold('All commands are blocked until migration is complete.')}")
158
+ click.echo(f"\n To apply migration, run:")
159
+ click.echo(f" {utils.Color.bold('half_orm dev migrate')}")
160
+ click.echo(f"\n{'='*70}\n")
161
+ else:
162
+ # Normal display
163
+ click.echo(hop.state)
164
+ click.echo(f"\n{utils.Color.bold('Available commands:')}")
165
+
166
+ # Adapt displayed commands based on environment
167
+ if hop._Hop__repo.database.production:
168
+ # Production commands
169
+ click.echo(f" • {utils.Color.bold('update')} - Fetch and list available releases")
170
+ click.echo(f" • {utils.Color.bold('upgrade [--to-release=X.Y.Z]')} - Apply releases to production")
171
+ else:
172
+ # Development commands
173
+ click.echo(f" • {utils.Color.bold('patch')}")
174
+ click.echo(f" • {utils.Color.bold('prepare-release <level>')} - Prepare next release stage file (patch/minor/major)")
175
+ click.echo(f" • {utils.Color.bold('promote-to <target>')} - Promote stage to rc or prod")
176
+
177
+ click.echo(f"\nTry {utils.Color.bold('half_orm dev <command> --help')} for more information.\n")
178
+ else:
179
+ click.echo(hop.state)
180
+ click.echo("\nNot in a hop repository.")
181
+ click.echo(f"\n{utils.Color.bold('Available commands:')}")
182
+ click.echo(f"\n • {utils.Color.bold('init <package_name>')} - Create new halfORM project.")
183
+ click.echo(f"\n • {utils.Color.bold('clone <git origin>')} - Clone an existing halfORM project.\n")
184
+
185
+ # Add only available commands to the group
186
+ for cmd_name in hop.available_commands:
187
+ if cmd_name in ALL_COMMANDS:
188
+ dev.add_command(ALL_COMMANDS[cmd_name])
189
+
190
+ return dev
@@ -25,6 +25,7 @@ import re
25
25
  import subprocess
26
26
  import sys
27
27
  import importlib.util
28
+ from packaging import version
28
29
  from pathlib import Path
29
30
  from typing import List, Dict, Optional, Tuple
30
31
  from half_orm import utils
@@ -58,38 +59,6 @@ class MigrationManager:
58
59
  # Path to migrations directory (in half_orm_dev package)
59
60
  self._migrations_root = Path(__file__).parent / 'migrations'
60
61
 
61
- def _parse_version(self, version_str: str) -> Tuple[int, int, int]:
62
- """
63
- Parse version string to tuple.
64
-
65
- Supports version formats:
66
- - "0.17.1"
67
- - "0.1.0a1" (ignores suffix)
68
- - "0.17.1-a1" (ignores suffix)
69
- - "0.17.1-rc2" (ignores suffix)
70
-
71
- Args:
72
- version_str: Version string like "0.17.1" or "0.17.1-a1"
73
-
74
- Returns:
75
- Tuple of (major, minor, patch)
76
- """
77
- # Strip any pre-release suffix (e.g., "-a1", "-rc2")
78
- base_version = version_str.split('-')[0]
79
- if not re.match(r"^\d+\.\d+\.\d+$", base_version):
80
- match = re.match(r"^(\d+\.\d+\.\d+)", base_version)
81
- if match:
82
- base_version = match.group(1)
83
-
84
- parts = base_version.split('.')
85
- if len(parts) != 3:
86
- raise MigrationManagerError(f"Invalid version format: {version_str}")
87
-
88
- try:
89
- return (int(parts[0]), int(parts[1]), int(parts[2]))
90
- except ValueError as e:
91
- raise MigrationManagerError(f"Invalid version format: {version_str}") from e
92
-
93
62
  def _version_to_path(self, version: Tuple[int, int, int]) -> Path:
94
63
  """
95
64
  Convert version tuple to migration directory path.
@@ -117,8 +86,8 @@ class MigrationManager:
117
86
  Returns:
118
87
  List of (version_str, migration_dir_path) tuples in order
119
88
  """
120
- current = self._parse_version(current_version)
121
- target = self._parse_version(target_version)
89
+ current = version.parse(current_version).release
90
+ target = version.parse(target_version).release
122
91
 
123
92
  pending = []
124
93
 
@@ -298,8 +267,19 @@ class MigrationManager:
298
267
  ) else "0.0.0"
299
268
 
300
269
  # If already at target version, nothing to do
301
- if current_version == target_version:
302
- return result
270
+ try:
271
+ comparison = self._repo.compare_versions(current_version, target_version)
272
+
273
+ if comparison >= 0:
274
+ # Already at or past target version (0 = equal, 1 = higher)
275
+ return result
276
+ except Exception as e:
277
+ # If version comparison fails (invalid format), log and continue
278
+ # This allows migration to proceed even if version format is unexpected
279
+ result['errors'].append(
280
+ f"Could not compare versions {current_version} and {target_version}: {e}. "
281
+ f"Continuing with migration attempt."
282
+ )
303
283
 
304
284
  # Get pending migrations
305
285
  pending = self.get_pending_migrations(current_version, target_version)
@@ -392,21 +372,39 @@ class MigrationManager:
392
372
  Check if migration is needed.
393
373
 
394
374
  Compares current tool version with hop_version in .hop/config.
375
+ Properly handles pre-release versions (alpha, beta, rc).
395
376
 
396
377
  Args:
397
- current_tool_version: Current half_orm_dev version
378
+ current_tool_version: Current half_orm_dev version (e.g., "0.17.2-a5")
398
379
 
399
380
  Returns:
400
- True if migration is needed
381
+ True if migration/update is needed
401
382
  """
402
383
  if not hasattr(self._repo, '_Repo__config'):
403
384
  return False
404
385
 
405
386
  config_version = self._repo._Repo__config.hop_version
406
387
 
407
- # Parse versions
408
- current = self._parse_version(current_tool_version)
409
- config = self._parse_version(config_version)
388
+ # If no hop_version is configured, no migration needed
389
+ if not config_version:
390
+ return False
410
391
 
411
- # Migration needed if current version is higher
412
- return current > config
392
+ try:
393
+ # Use Repo's centralized comparison method
394
+ # Returns: 1 if current > config, 0 if equal, -1 if current < config
395
+ comparison = self._repo.compare_versions(current_tool_version, config_version)
396
+
397
+ # Migration needed if current version is higher
398
+ # This now properly compares: 0.17.2a5 > 0.17.2a3 → returns 1 ✓
399
+ return comparison > 0
400
+
401
+ except Exception as e:
402
+ # If version parsing fails, log warning and don't block
403
+ import warnings
404
+ warnings.warn(
405
+ f"Could not parse versions for migration check: "
406
+ f"current={current_tool_version}, config={config_version}. "
407
+ f"Error: {e}",
408
+ UserWarning
409
+ )
410
+ return False
@@ -37,6 +37,17 @@ from .utils import TEMPLATE_DIRS, hop_version, resolve_database_config_name
37
37
  class RepoError(Exception):
38
38
  pass
39
39
 
40
+ class OutdatedHalfORMDevError(RepoError):
41
+ """Raised when installed half_orm_dev version is older than repository requirement."""
42
+ def __init__(self, required_version: str, installed_version: str):
43
+ self.required_version = required_version
44
+ self.installed_version = installed_version
45
+ super().__init__(
46
+ f"Repository requires half_orm_dev >= {required_version} "
47
+ f"but {installed_version} is installed.\n"
48
+ f"Please upgrade: pip install --upgrade half_orm_dev"
49
+ )
50
+
40
51
  class Config:
41
52
  """
42
53
  """
@@ -257,8 +268,12 @@ class Repo:
257
268
  if self.devel:
258
269
  self.hgit = HGit(self)
259
270
  self.__checked = True
260
- # Perform automatic migration if needed (after hgit is initialized)
261
- self._run_pending_migrations()
271
+ # NOTE: Migration is no longer automatic - user must run `half_orm dev migrate`
272
+ # This prevents implicit changes and gives user control over when migration happens
273
+
274
+ # Automatically check and update hooks/config (silent, uses cache)
275
+ # This ensures Git hooks are always up-to-date for all commands
276
+ self.check_and_update(silent=True)
262
277
  return
263
278
  par_dir = os.path.split(base_dir)[0]
264
279
  if par_dir == base_dir:
@@ -290,13 +305,13 @@ class Repo:
290
305
  installed_version = hop_version()
291
306
 
292
307
  try:
293
- if version.parse(installed_version) < version.parse(required_version):
294
- raise RepoError(
295
- f"Repository requires half_orm_dev >= {required_version} "
296
- f"but {installed_version} is installed.\n"
297
- f"Please upgrade: pip install --upgrade half_orm_dev"
298
- )
299
- except version.InvalidVersion as e:
308
+ # Use centralized comparison method
309
+ if self.compare_versions(installed_version, required_version) < 0:
310
+ raise OutdatedHalfORMDevError(required_version, installed_version)
311
+ except OutdatedHalfORMDevError:
312
+ # Re-raise downgrade errors immediately
313
+ raise
314
+ except RepoError as e:
300
315
  # If version parsing fails, log warning but don't block
301
316
  warnings.warn(
302
317
  f"Could not parse version: installed={installed_version}, "
@@ -304,61 +319,179 @@ class Repo:
304
319
  UserWarning
305
320
  )
306
321
 
307
- def _run_pending_migrations(self):
322
+ def compare_versions(self, version1: str, version2: str) -> int:
323
+ """
324
+ Compare two version strings using packaging.version.
325
+
326
+ Properly handles pre-release versions (alpha, beta, rc) according to PEP 440.
327
+
328
+ Args:
329
+ version1: First version string (e.g., "0.17.2-a5")
330
+ version2: Second version string (e.g., "0.17.2-a3")
331
+
332
+ Returns:
333
+ -1 if version1 < version2
334
+ 0 if version1 == version2
335
+ 1 if version1 > version2
336
+
337
+ Raises:
338
+ RepoError: If either version string is invalid
339
+
340
+ Examples:
341
+ >>> repo.compare_versions("0.17.2-a5", "0.17.2-a3")
342
+ 1 # 0.17.2a5 > 0.17.2a3
343
+ >>> repo.compare_versions("0.17.2", "0.17.2-a5")
344
+ 1 # 0.17.2 > 0.17.2a5 (release > pre-release)
345
+ >>> repo.compare_versions("0.17.1", "0.17.2")
346
+ -1 # 0.17.1 < 0.17.2
347
+ >>> repo.compare_versions("0.17.2", "0.17.2")
348
+ 0 # Equal
349
+ """
350
+ try:
351
+ v1 = version.parse(version1)
352
+ v2 = version.parse(version2)
353
+
354
+ if v1 < v2:
355
+ return -1
356
+ elif v1 > v2:
357
+ return 1
358
+ else:
359
+ return 0
360
+
361
+ except version.InvalidVersion as e:
362
+ raise RepoError(
363
+ f"Invalid version format: {e}"
364
+ ) from e
365
+
366
+ def needs_migration(self) -> bool:
367
+ """
368
+ Check if repository needs migration.
369
+
370
+ Compares installed half_orm_dev version with repository's hop_version.
371
+
372
+ Returns:
373
+ True if installed version > repository version (migration needed)
374
+ False otherwise
375
+
376
+ Examples:
377
+ >>> repo.needs_migration()
378
+ True # Installed 0.18.0, repo at 0.17.2
379
+ """
380
+ if not hasattr(self, '_Repo__config') or not self.__config:
381
+ return False
382
+
383
+ installed_version = hop_version()
384
+ config_version = self.__config.hop_version
385
+
386
+ if not config_version:
387
+ return False
388
+
389
+ try:
390
+ return self.compare_versions(installed_version, config_version) > 0
391
+ except RepoError:
392
+ # If version comparison fails, assume no migration needed
393
+ return False
394
+
395
+ def run_migrations_if_needed(self, silent: bool = False) -> dict:
308
396
  """
309
397
  Run pending migrations using MigrationManager.
310
398
 
311
- Automatically detects and runs migrations based on current
312
- half_orm_dev version vs hop_version in .hop/config.
399
+ Detects and runs migrations based on current half_orm_dev version
400
+ vs hop_version in .hop/config.
313
401
 
314
402
  Behavior:
315
- - On ho-prod: Runs migration with lock, creates commit, notifies active branches
316
- - On other branches: Warns user to merge ho-prod to get migration
403
+ - On ho-prod: Runs migration with lock, creates commit, syncs to active branches
404
+ - On other branches: Raises RepoError directing user to checkout ho-prod
405
+
406
+ Args:
407
+ silent: If True, suppress informational messages (only show errors)
408
+
409
+ Returns:
410
+ dict with keys:
411
+ - migration_needed: bool - True if migration was needed
412
+ - migration_run: bool - True if migration was executed
413
+ - target_version: str - Target version migrated to
414
+ - errors: list - Any errors encountered
415
+
416
+ Raises:
417
+ RepoError: If not on ho-prod branch and migration is needed
418
+
419
+ Examples:
420
+ # Run migration (raises if not on ho-prod)
421
+ result = repo.run_migrations_if_needed()
317
422
 
318
- Silent by default - only logs errors.
423
+ # Check result
424
+ if result['migration_run']:
425
+ print(f"Migrated to {result['target_version']}")
319
426
  """
427
+ result = {
428
+ 'migration_needed': False,
429
+ 'migration_run': False,
430
+ 'target_version': None,
431
+ 'errors': []
432
+ }
433
+
320
434
  try:
321
435
  # Create migration manager
322
436
  migration_mgr = MigrationManager(self)
323
437
 
324
438
  # Get current half_orm_dev version
325
439
  current_version = hop_version()
440
+ result['target_version'] = current_version
326
441
 
327
442
  # Check if migration is needed
328
443
  if not migration_mgr.check_migration_needed(current_version):
329
- return
444
+ return result
445
+
446
+ result['migration_needed'] = True
330
447
 
331
448
  # Only run migrations on ho-prod branch
332
449
  if not self.hgit or self.hgit.branch != 'ho-prod':
333
- # Warn user to switch to ho-prod to run migration
450
+ # Raise error directing user to checkout ho-prod
334
451
  current_branch = self.hgit.branch if self.hgit else 'unknown'
335
452
  config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
336
- print(f"\n{utils.Color.bold('⚠️ Migration needed:')}", file=sys.stderr)
337
- print(f" half_orm_dev {config_version} → {current_version}", file=sys.stderr)
338
- print(f" Current branch: {current_branch}", file=sys.stderr)
339
- print(f"\n To apply migration, checkout to ho-prod branch and rerun:", file=sys.stderr)
340
- print(f" git checkout ho-prod", file=sys.stderr)
341
- print(f" half_orm dev check\n", file=sys.stderr)
342
- return
453
+ raise RepoError(
454
+ f"Repository migration required\n\n"
455
+ f" Repository version: {config_version}\n"
456
+ f" Installed version: {current_version}\n"
457
+ f" Current branch: {current_branch}\n\n"
458
+ f" Please checkout to ho-prod branch and run:\n"
459
+ f" git checkout ho-prod\n"
460
+ f" half_orm dev migrate\n"
461
+ )
343
462
 
344
463
  # Run migrations on ho-prod
345
464
  # Branch sync is handled automatically by the decorator
346
- result = migration_mgr.run_migrations(
465
+ migration_result = migration_mgr.run_migrations(
347
466
  target_version=current_version,
348
467
  create_commit=True
349
468
  )
350
469
 
351
- # Log errors if any
352
- if result.get('errors'):
353
- for error in result['errors']:
354
- print(f"Migration error: {error}", file=sys.stderr)
470
+ result['migration_run'] = True
471
+ result['errors'] = migration_result.get('errors', [])
472
+
473
+ # Log success if not silent
474
+ if not silent:
475
+ if migration_result.get('migrations_applied'):
476
+ print(f"✓ Applied {len(migration_result['migrations_applied'])} migration(s)")
477
+ else:
478
+ print(f"✓ Updated repository version to {current_version}")
355
479
 
480
+ except RepoError:
481
+ # Re-raise RepoError (for branch check)
482
+ raise
356
483
  except MigrationManagerError as e:
357
- # Log migration errors but don't fail repo initialization
358
- print(f"Migration failed: {e}", file=sys.stderr)
484
+ # Log migration errors
485
+ error_msg = f"Migration failed: {e}"
486
+ result['errors'].append(error_msg)
487
+ raise RepoError(error_msg) from e
359
488
  except Exception as e:
360
489
  # Catch any unexpected errors
361
- print(f"Unexpected migration error: {e}", file=sys.stderr)
490
+ error_msg = f"Unexpected migration error: {e}"
491
+ result['errors'].append(error_msg)
492
+ raise RepoError(error_msg) from e
493
+
494
+ return result
362
495
 
363
496
  def sync_hop_to_active_branches(self, reason: str = "update") -> dict:
364
497
  """
@@ -998,6 +1131,9 @@ class Repo:
998
1131
  hooks_source_dir = os.path.join(TEMPLATE_DIRS, 'git-hooks')
999
1132
  hooks_dest_dir = os.path.join(self.__base_dir, '.git', 'hooks')
1000
1133
 
1134
+ # Create .git/hooks directory if it doesn't exist
1135
+ os.makedirs(hooks_dest_dir, exist_ok=True)
1136
+
1001
1137
  any_installed = False
1002
1138
  overall_action = 'skipped'
1003
1139
 
@@ -2093,6 +2229,7 @@ See docs/half_orm_dev.md for complete documentation.
2093
2229
  5. Create .hop/alt_config if custom database_name provided
2094
2230
  6. Setup database (create + metadata if create_db=True)
2095
2231
  7. Restore database from model/schema.sql to production version
2232
+ 8. Install Git hooks (pre-commit, prepare-commit-msg)
2096
2233
 
2097
2234
  Examples:
2098
2235
  # Interactive with prompts for connection params
@@ -2216,3 +2353,6 @@ See docs/half_orm_dev.md for complete documentation.
2216
2353
  raise RepoError(
2217
2354
  f"Failed to restore database from schema: {e}"
2218
2355
  ) from e
2356
+
2357
+ # Step 9: Install Git hooks
2358
+ repo.install_git_hooks()
@@ -3,11 +3,48 @@
3
3
  # Half-ORM pre-commit hook
4
4
  # 1. Checks if current ho-* branch exists on remote origin
5
5
  # 2. Protects ho-prod branch from direct commits
6
+ # 3. Optionally calls pre-commit-custom if it exists
6
7
  # Generated by half_orm_dev
7
8
 
8
9
  # Get current branch
9
10
  CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
10
11
 
12
+ # =============================================================================
13
+ # CUSTOM PRE-COMMIT HOOK SUPPORT
14
+ # =============================================================================
15
+ # If a pre-commit-custom script exists in .git/hooks/, execute it first.
16
+ # This allows users to add their own custom checks (linting, formatting, etc.)
17
+ #
18
+ # ⚠️ WARNING: pre-commit-custom is NOT tracked by Git and NOT cloned.
19
+ # It must be added manually to each local repository clone.
20
+ #
21
+ # To create a custom hook:
22
+ # 1. Create .git/hooks/pre-commit-custom (executable)
23
+ # 2. Add your custom checks
24
+ # 3. Exit with 0 (success) or non-zero (failure) to block the commit
25
+ #
26
+ # Example .git/hooks/pre-commit-custom:
27
+ # #!/usr/bin/env bash
28
+ # # Run Python linter
29
+ # python -m pylint $(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
30
+ # exit $?
31
+ # =============================================================================
32
+
33
+ CUSTOM_HOOK=".git/hooks/pre-commit-custom"
34
+ if [ -x "$CUSTOM_HOOK" ]; then
35
+ "$CUSTOM_HOOK"
36
+ CUSTOM_EXIT_CODE=$?
37
+ if [ $CUSTOM_EXIT_CODE -ne 0 ]; then
38
+ echo ""
39
+ echo "❌ Custom pre-commit hook failed (exit code: $CUSTOM_EXIT_CODE)"
40
+ exit $CUSTOM_EXIT_CODE
41
+ fi
42
+ fi
43
+
44
+ # =============================================================================
45
+ # HALF-ORM STANDARD CHECKS
46
+ # =============================================================================
47
+
11
48
  # Check if current ho-* branch (except ho-prod) still exists on remote origin
12
49
  # This prevents committing to ho-* branches that were deleted remotely
13
50
  if [[ "$CURRENT_BRANCH" == ho-* ]] && [ "$CURRENT_BRANCH" != "ho-prod" ]; then
@@ -0,0 +1 @@
1
+ 0.17.2-a7
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.2a5
3
+ Version: 0.17.2a7
4
4
  Summary: half_orm development Framework.
5
5
  Author-email: Joël Maïzi <joel.maizi@collorg.org>
6
6
  License-Expression: GPL-3.0-or-later
@@ -29,6 +29,7 @@ half_orm_dev/cli/commands/apply.py
29
29
  half_orm_dev/cli/commands/check.py
30
30
  half_orm_dev/cli/commands/clone.py
31
31
  half_orm_dev/cli/commands/init.py
32
+ half_orm_dev/cli/commands/migrate.py
32
33
  half_orm_dev/cli/commands/new.py
33
34
  half_orm_dev/cli/commands/patch.py
34
35
  half_orm_dev/cli/commands/release.py
@@ -1,103 +0,0 @@
1
- """
2
- Main CLI module - Creates and configures the CLI group
3
- """
4
-
5
- import click
6
- from half_orm_dev.repo import Repo
7
- from half_orm import utils
8
- from .commands import ALL_COMMANDS
9
-
10
-
11
- class Hop:
12
- """Sets the options available to the hop command"""
13
-
14
- def __init__(self):
15
- self.__repo: Repo = Repo() # Utilise le singleton
16
- self.__available_cmds = self._determine_available_commands()
17
-
18
- def _determine_available_commands(self):
19
- """
20
- Determine which commands are available based on context.
21
-
22
- Returns different command sets based on:
23
- - Repository status (checked/unchecked)
24
- - Development mode (devel flag - metadata presence)
25
- - Environment (production flag)
26
- """
27
- if not self.repo_checked:
28
- # Outside hop repository - commands for project initialization
29
- return ['init', 'clone']
30
-
31
- # Inside hop repository
32
- if not self.__repo.devel:
33
- # Sync-only mode (no metadata)
34
- return ['sync-package', 'check']
35
-
36
- # Development mode (metadata present)
37
- if self.__repo.database.production:
38
- # PRODUCTION ENVIRONMENT - Release deployment only
39
- return ['update', 'upgrade', 'check']
40
- else:
41
- # DEVELOPMENT ENVIRONMENT - Patch development
42
- return ['patch', 'release', 'check']
43
-
44
- @property
45
- def repo_checked(self):
46
- """Returns whether we are in a repo or not."""
47
- return self.__repo.checked
48
-
49
- @property
50
- def state(self):
51
- """Returns the state of the repo."""
52
- return self.__repo.state
53
-
54
- @property
55
- def available_commands(self):
56
- """Returns the list of available commands."""
57
- return self.__available_cmds
58
-
59
-
60
- def create_cli_group():
61
- """
62
- Creates and returns the CLI group with appropriate commands.
63
-
64
- Returns:
65
- click.Group: Configured CLI group
66
- """
67
- hop = Hop()
68
-
69
- @click.group(invoke_without_command=True)
70
- @click.pass_context
71
- def dev(ctx):
72
- """halfORM development tools - Git-centric patch management and database synchronization"""
73
- if ctx.invoked_subcommand is None:
74
- # Show repo state when no subcommand is provided
75
- if hop.repo_checked:
76
- click.echo(hop.state)
77
- click.echo(f"\n{utils.Color.bold('Available commands:')}")
78
-
79
- # Adapt displayed commands based on environment
80
- if hop.__repo.database.production:
81
- # Production commands
82
- click.echo(f" • {utils.Color.bold('update')} - Fetch and list available releases")
83
- click.echo(f" • {utils.Color.bold('upgrade [--to-release=X.Y.Z]')} - Apply releases to production")
84
- else:
85
- # Development commands
86
- click.echo(f" • {utils.Color.bold('patch')}")
87
- click.echo(f" • {utils.Color.bold('prepare-release <level>')} - Prepare next release stage file (patch/minor/major)")
88
- click.echo(f" • {utils.Color.bold('promote-to <target>')} - Promote stage to rc or prod")
89
-
90
- click.echo(f"\nTry {utils.Color.bold('half_orm dev <command> --help')} for more information.\n")
91
- else:
92
- click.echo(hop.state)
93
- click.echo("\nNot in a hop repository.")
94
- click.echo(f"\n{utils.Color.bold('Available commands:')}")
95
- click.echo(f"\n • {utils.Color.bold('init <package_name>')} - Create new halfORM project.")
96
- click.echo(f"\n • {utils.Color.bold('clone <git origin>')} - Clone an existing halfORM project.\n")
97
-
98
- # Add only available commands to the group
99
- for cmd_name in hop.available_commands:
100
- if cmd_name in ALL_COMMANDS:
101
- dev.add_command(ALL_COMMANDS[cmd_name])
102
-
103
- return dev
@@ -1 +0,0 @@
1
- 0.17.2-a5
File without changes
File without changes