half-orm-dev 0.17.2a4__tar.gz → 0.17.2a6__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 (66) hide show
  1. {half_orm_dev-0.17.2a4/half_orm_dev.egg-info → half_orm_dev-0.17.2a6}/PKG-INFO +1 -1
  2. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/__init__.py +3 -0
  3. half_orm_dev-0.17.2a6/half_orm_dev/cli/commands/migrate.py +125 -0
  4. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/main.py +37 -14
  5. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/migration_manager.py +41 -43
  6. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/repo.py +179 -26
  7. half_orm_dev-0.17.2a6/half_orm_dev/version.txt +1 -0
  8. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6/half_orm_dev.egg-info}/PKG-INFO +1 -1
  9. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  10. half_orm_dev-0.17.2a4/half_orm_dev/version.txt +0 -1
  11. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/AUTHORS +0 -0
  12. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/LICENSE +0 -0
  13. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/README.md +0 -0
  14. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/__init__.py +0 -0
  15. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/__init__.py +0 -0
  16. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/apply.py +0 -0
  17. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/check.py +0 -0
  18. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/clone.py +0 -0
  19. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/init.py +0 -0
  20. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/new.py +0 -0
  21. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/patch.py +0 -0
  22. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/release.py +0 -0
  23. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/restore.py +0 -0
  24. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/sync.py +0 -0
  25. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/todo.py +0 -0
  26. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/undo.py +0 -0
  27. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/update.py +0 -0
  28. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli/commands/upgrade.py +0 -0
  29. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/cli_extension.py +0 -0
  30. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/database.py +0 -0
  31. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/decorators.py +0 -0
  32. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/hgit.py +0 -0
  33. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  34. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  35. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/modules.py +0 -0
  36. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patch_manager.py +0 -0
  37. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patch_validator.py +0 -0
  38. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  39. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  40. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  41. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patches/log +0 -0
  42. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  43. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/release_file.py +0 -0
  44. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/release_manager.py +0 -0
  45. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/.gitignore +0 -0
  46. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/MANIFEST.in +0 -0
  47. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/Pipfile +0 -0
  48. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/README +0 -0
  49. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/conftest_template +0 -0
  50. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  51. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  52. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/init_module_template +0 -0
  53. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/module_template_1 +0 -0
  54. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/module_template_2 +0 -0
  55. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/module_template_3 +0 -0
  56. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/pyproject.toml +0 -0
  57. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/relation_test +0 -0
  58. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/sql_adapter +0 -0
  59. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/templates/warning +0 -0
  60. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev/utils.py +0 -0
  61. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  62. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev.egg-info/requires.txt +0 -0
  63. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/half_orm_dev.egg-info/top_level.txt +0 -0
  64. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/pyproject.toml +0 -0
  65. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/setup.cfg +0 -0
  66. {half_orm_dev-0.17.2a4 → half_orm_dev-0.17.2a6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.2a4
3
+ Version: 0.17.2a6
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()
@@ -28,6 +28,9 @@ class Hop:
28
28
  # Outside hop repository - commands for project initialization
29
29
  return ['init', 'clone']
30
30
 
31
+ if self.__repo.needs_migration():
32
+ return ['migrate']
33
+
31
34
  # Inside hop repository
32
35
  if not self.__repo.devel:
33
36
  # Sync-only mode (no metadata)
@@ -73,21 +76,41 @@ def create_cli_group():
73
76
  if ctx.invoked_subcommand is None:
74
77
  # Show repo state when no subcommand is provided
75
78
  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")
79
+ # Check if migration is needed
80
+ if hop._Hop__repo.needs_migration():
81
+ # Display migration warning
82
+ from half_orm_dev.utils import hop_version
83
+ installed_version = hop_version()
84
+ config_version = hop._Hop__repo._Repo__config.hop_version
85
+ current_branch = hop._Hop__repo.hgit.branch if hop._Hop__repo.hgit else 'unknown'
86
+
87
+ click.echo(f"\n{'='*70}")
88
+ click.echo(f"⚠️ {utils.Color.bold(utils.Color.red('REPOSITORY MIGRATION REQUIRED'))} ⚠️")
89
+ click.echo(f"{'='*70}")
90
+ click.echo(f"\n Repository version: {utils.Color.red(config_version)}")
91
+ click.echo(f" Installed version: {utils.Color.green(installed_version)}")
92
+ click.echo(f" Current branch: {current_branch}")
93
+ click.echo(f"\n {utils.Color.bold('All commands are blocked until migration is complete.')}")
94
+ click.echo(f"\n To apply migration, run:")
95
+ click.echo(f" {utils.Color.bold('half_orm dev migrate')}")
96
+ click.echo(f"\n{'='*70}\n")
84
97
  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")
98
+ # Normal display
99
+ click.echo(hop.state)
100
+ click.echo(f"\n{utils.Color.bold('Available commands:')}")
101
+
102
+ # Adapt displayed commands based on environment
103
+ if hop._Hop__repo.database.production:
104
+ # Production commands
105
+ click.echo(f" • {utils.Color.bold('update')} - Fetch and list available releases")
106
+ click.echo(f" • {utils.Color.bold('upgrade [--to-release=X.Y.Z]')} - Apply releases to production")
107
+ else:
108
+ # Development commands
109
+ click.echo(f" • {utils.Color.bold('patch')}")
110
+ click.echo(f" • {utils.Color.bold('prepare-release <level>')} - Prepare next release stage file (patch/minor/major)")
111
+ click.echo(f" • {utils.Color.bold('promote-to <target>')} - Promote stage to rc or prod")
112
+
113
+ click.echo(f"\nTry {utils.Color.bold('half_orm dev <command> --help')} for more information.\n")
91
114
  else:
92
115
  click.echo(hop.state)
93
116
  click.echo("\nNot in a hop repository.")
@@ -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
@@ -250,15 +250,16 @@ class Repo:
250
250
  This method is called when no hop config file is provided.
251
251
  Returns True if we are in a repo, False otherwise.
252
252
  """
253
- base_dir = os.path.abspath(os.path.curdir)
253
+ base_dir = self._find_base_dir()
254
254
  while base_dir:
255
255
  if self.__set_base_dir(base_dir):
256
256
  self.database = Database(self)
257
257
  if self.devel:
258
258
  self.hgit = HGit(self)
259
259
  self.__checked = True
260
- # Perform automatic migration if needed (after hgit is initialized)
261
- self._run_pending_migrations()
260
+ # NOTE: Migration is no longer automatic - user must run `half_orm dev migrate`
261
+ # This prevents implicit changes and gives user control over when migration happens
262
+ return
262
263
  par_dir = os.path.split(base_dir)[0]
263
264
  if par_dir == base_dir:
264
265
  break
@@ -270,64 +271,216 @@ class Repo:
270
271
  self.__base_dir = base_dir
271
272
  self.__config = Config(base_dir)
272
273
  self.__local_config = LocalConfig(base_dir)
274
+ self._validate_version()
273
275
  return True
274
276
  return False
275
277
 
276
- def _run_pending_migrations(self):
278
+ def _validate_version(self):
279
+ """
280
+ Validate that installed half_orm_dev version meets repository requirement.
281
+
282
+ Raises:
283
+ RepoError: If installed version is lower than required hop_version
284
+ """
285
+ required_version = self.__config.hop_version
286
+ if not required_version:
287
+ # No version requirement in .hop/config, skip validation
288
+ return
289
+
290
+ installed_version = hop_version()
291
+
292
+ try:
293
+ # Use centralized comparison method
294
+ if self.compare_versions(installed_version, required_version) < 0:
295
+ raise RepoError(
296
+ f"Repository requires half_orm_dev >= {required_version} "
297
+ f"but {installed_version} is installed.\n"
298
+ f"Please upgrade: pip install --upgrade half_orm_dev"
299
+ )
300
+ except RepoError as e:
301
+ # If it's a version validation error (not comparison error), re-raise it
302
+ if "requires half_orm_dev" in str(e):
303
+ raise
304
+ # If version parsing fails, log warning but don't block
305
+ warnings.warn(
306
+ f"Could not parse version: installed={installed_version}, "
307
+ f"required={required_version}. Error: {e}",
308
+ UserWarning
309
+ )
310
+
311
+ def compare_versions(self, version1: str, version2: str) -> int:
312
+ """
313
+ Compare two version strings using packaging.version.
314
+
315
+ Properly handles pre-release versions (alpha, beta, rc) according to PEP 440.
316
+
317
+ Args:
318
+ version1: First version string (e.g., "0.17.2-a5")
319
+ version2: Second version string (e.g., "0.17.2-a3")
320
+
321
+ Returns:
322
+ -1 if version1 < version2
323
+ 0 if version1 == version2
324
+ 1 if version1 > version2
325
+
326
+ Raises:
327
+ RepoError: If either version string is invalid
328
+
329
+ Examples:
330
+ >>> repo.compare_versions("0.17.2-a5", "0.17.2-a3")
331
+ 1 # 0.17.2a5 > 0.17.2a3
332
+ >>> repo.compare_versions("0.17.2", "0.17.2-a5")
333
+ 1 # 0.17.2 > 0.17.2a5 (release > pre-release)
334
+ >>> repo.compare_versions("0.17.1", "0.17.2")
335
+ -1 # 0.17.1 < 0.17.2
336
+ >>> repo.compare_versions("0.17.2", "0.17.2")
337
+ 0 # Equal
338
+ """
339
+ try:
340
+ v1 = version.parse(version1)
341
+ v2 = version.parse(version2)
342
+
343
+ if v1 < v2:
344
+ return -1
345
+ elif v1 > v2:
346
+ return 1
347
+ else:
348
+ return 0
349
+
350
+ except version.InvalidVersion as e:
351
+ raise RepoError(
352
+ f"Invalid version format: {e}"
353
+ ) from e
354
+
355
+ def needs_migration(self) -> bool:
356
+ """
357
+ Check if repository needs migration.
358
+
359
+ Compares installed half_orm_dev version with repository's hop_version.
360
+
361
+ Returns:
362
+ True if installed version > repository version (migration needed)
363
+ False otherwise
364
+
365
+ Examples:
366
+ >>> repo.needs_migration()
367
+ True # Installed 0.18.0, repo at 0.17.2
368
+ """
369
+ if not hasattr(self, '_Repo__config') or not self.__config:
370
+ return False
371
+
372
+ installed_version = hop_version()
373
+ config_version = self.__config.hop_version
374
+
375
+ if not config_version:
376
+ return False
377
+
378
+ try:
379
+ return self.compare_versions(installed_version, config_version) > 0
380
+ except RepoError:
381
+ # If version comparison fails, assume no migration needed
382
+ return False
383
+
384
+ def run_migrations_if_needed(self, silent: bool = False) -> dict:
277
385
  """
278
386
  Run pending migrations using MigrationManager.
279
387
 
280
- Automatically detects and runs migrations based on current
281
- half_orm_dev version vs hop_version in .hop/config.
388
+ Detects and runs migrations based on current half_orm_dev version
389
+ vs hop_version in .hop/config.
282
390
 
283
391
  Behavior:
284
- - On ho-prod: Runs migration with lock, creates commit, notifies active branches
285
- - On other branches: Warns user to merge ho-prod to get migration
392
+ - On ho-prod: Runs migration with lock, creates commit, syncs to active branches
393
+ - On other branches: Raises RepoError directing user to checkout ho-prod
394
+
395
+ Args:
396
+ silent: If True, suppress informational messages (only show errors)
286
397
 
287
- Silent by default - only logs errors.
398
+ Returns:
399
+ dict with keys:
400
+ - migration_needed: bool - True if migration was needed
401
+ - migration_run: bool - True if migration was executed
402
+ - target_version: str - Target version migrated to
403
+ - errors: list - Any errors encountered
404
+
405
+ Raises:
406
+ RepoError: If not on ho-prod branch and migration is needed
407
+
408
+ Examples:
409
+ # Run migration (raises if not on ho-prod)
410
+ result = repo.run_migrations_if_needed()
411
+
412
+ # Check result
413
+ if result['migration_run']:
414
+ print(f"Migrated to {result['target_version']}")
288
415
  """
416
+ result = {
417
+ 'migration_needed': False,
418
+ 'migration_run': False,
419
+ 'target_version': None,
420
+ 'errors': []
421
+ }
422
+
289
423
  try:
290
424
  # Create migration manager
291
425
  migration_mgr = MigrationManager(self)
292
426
 
293
427
  # Get current half_orm_dev version
294
428
  current_version = hop_version()
429
+ result['target_version'] = current_version
295
430
 
296
431
  # Check if migration is needed
297
432
  if not migration_mgr.check_migration_needed(current_version):
298
- return
433
+ return result
434
+
435
+ result['migration_needed'] = True
299
436
 
300
437
  # Only run migrations on ho-prod branch
301
438
  if not self.hgit or self.hgit.branch != 'ho-prod':
302
- # Warn user to switch to ho-prod to run migration
439
+ # Raise error directing user to checkout ho-prod
303
440
  current_branch = self.hgit.branch if self.hgit else 'unknown'
304
441
  config_version = self.__config.hop_version if hasattr(self, '_Repo__config') else '0.0.0'
305
- print(f"\n{utils.Color.bold('⚠️ Migration needed:')}", file=sys.stderr)
306
- print(f" half_orm_dev {config_version} → {current_version}", file=sys.stderr)
307
- print(f" Current branch: {current_branch}", file=sys.stderr)
308
- print(f"\n To apply migration, checkout to ho-prod branch and rerun:", file=sys.stderr)
309
- print(f" git checkout ho-prod", file=sys.stderr)
310
- print(f" half_orm dev check\n", file=sys.stderr)
311
- return
442
+ raise RepoError(
443
+ f"Repository migration required\n\n"
444
+ f" Repository version: {config_version}\n"
445
+ f" Installed version: {current_version}\n"
446
+ f" Current branch: {current_branch}\n\n"
447
+ f" Please checkout to ho-prod branch and run:\n"
448
+ f" git checkout ho-prod\n"
449
+ f" half_orm dev migrate\n"
450
+ )
312
451
 
313
452
  # Run migrations on ho-prod
314
453
  # Branch sync is handled automatically by the decorator
315
- result = migration_mgr.run_migrations(
454
+ migration_result = migration_mgr.run_migrations(
316
455
  target_version=current_version,
317
456
  create_commit=True
318
457
  )
319
458
 
320
- # Log errors if any
321
- if result.get('errors'):
322
- for error in result['errors']:
323
- print(f"Migration error: {error}", file=sys.stderr)
459
+ result['migration_run'] = True
460
+ result['errors'] = migration_result.get('errors', [])
461
+
462
+ # Log success if not silent
463
+ if not silent:
464
+ if migration_result.get('migrations_applied'):
465
+ print(f"✓ Applied {len(migration_result['migrations_applied'])} migration(s)")
466
+ else:
467
+ print(f"✓ Updated repository version to {current_version}")
324
468
 
469
+ except RepoError:
470
+ # Re-raise RepoError (for branch check)
471
+ raise
325
472
  except MigrationManagerError as e:
326
- # Log migration errors but don't fail repo initialization
327
- print(f"Migration failed: {e}", file=sys.stderr)
473
+ # Log migration errors
474
+ error_msg = f"Migration failed: {e}"
475
+ result['errors'].append(error_msg)
476
+ raise RepoError(error_msg) from e
328
477
  except Exception as e:
329
478
  # Catch any unexpected errors
330
- print(f"Unexpected migration error: {e}", file=sys.stderr)
479
+ error_msg = f"Unexpected migration error: {e}"
480
+ result['errors'].append(error_msg)
481
+ raise RepoError(error_msg) from e
482
+
483
+ return result
331
484
 
332
485
  def sync_hop_to_active_branches(self, reason: str = "update") -> dict:
333
486
  """
@@ -0,0 +1 @@
1
+ 0.17.2-a6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.2a4
3
+ Version: 0.17.2a6
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 +0,0 @@
1
- 0.17.2-a4
File without changes
File without changes