half-orm-dev 1.0.0a18__tar.gz → 1.0.0a19__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 (78) hide show
  1. {half_orm_dev-1.0.0a18/half_orm_dev.egg-info → half_orm_dev-1.0.0a19}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/main.py +30 -13
  3. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patch_manager.py +1 -6
  4. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/release_manager.py +37 -249
  5. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/repo.py +18 -11
  6. half_orm_dev-1.0.0a19/half_orm_dev/version.txt +1 -0
  7. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19/half_orm_dev.egg-info}/PKG-INFO +1 -1
  8. half_orm_dev-1.0.0a18/half_orm_dev/version.txt +0 -1
  9. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/AUTHORS +0 -0
  10. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/LICENSE +0 -0
  11. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/README.md +0 -0
  12. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/__init__.py +0 -0
  13. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/bootstrap_manager.py +0 -0
  14. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/__init__.py +0 -0
  15. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/__init__.py +0 -0
  16. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/apply.py +0 -0
  17. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  18. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/check.py +0 -0
  19. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/clone.py +0 -0
  20. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/init.py +0 -0
  21. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/migrate.py +0 -0
  22. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/patch.py +0 -0
  23. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/release.py +0 -0
  24. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/restore.py +0 -0
  25. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  26. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  27. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/sync.py +0 -0
  28. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/todo.py +0 -0
  29. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/undo.py +0 -0
  30. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/update.py +0 -0
  31. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli/commands/upgrade.py +0 -0
  32. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/cli_extension.py +0 -0
  33. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/database.py +0 -0
  34. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/decorators.py +0 -0
  35. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/file_executor.py +0 -0
  36. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/hgit.py +0 -0
  37. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migration_manager.py +0 -0
  38. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  39. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  40. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  41. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  42. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  43. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  44. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  45. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  46. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  47. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/modules.py +0 -0
  48. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patch_validator.py +0 -0
  49. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  50. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  51. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  52. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/log +0 -0
  53. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  54. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/release_file.py +0 -0
  55. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/scripts/repair-metadata.py +0 -0
  56. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/.gitignore +0 -0
  57. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/MANIFEST.in +0 -0
  58. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/README +0 -0
  59. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/conftest_template +0 -0
  60. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  61. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  62. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/init_module_template +0 -0
  63. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_1 +0 -0
  64. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_2 +0 -0
  65. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/module_template_3 +0 -0
  66. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/pyproject.toml +0 -0
  67. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/relation_test +0 -0
  68. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/sql_adapter +0 -0
  69. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/templates/warning +0 -0
  70. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev/utils.py +0 -0
  71. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/SOURCES.txt +0 -0
  72. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  73. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/entry_points.txt +0 -0
  74. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/requires.txt +0 -0
  75. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/half_orm_dev.egg-info/top_level.txt +0 -0
  76. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/pyproject.toml +0 -0
  77. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/setup.cfg +0 -0
  78. {half_orm_dev-1.0.0a18 → half_orm_dev-1.0.0a19}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a18
3
+ Version: 1.0.0a19
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
@@ -4,6 +4,8 @@ Main CLI module - Creates and configures the CLI group
4
4
 
5
5
  import click
6
6
  import functools
7
+ import os
8
+ import subprocess
7
9
  import sys
8
10
  from half_orm_dev.repo import Repo, OutdatedHalfORMDevError
9
11
  from half_orm import utils
@@ -39,15 +41,16 @@ class Hop:
39
41
  but will be blocked by the decorator at execution time.
40
42
  """
41
43
  if self.needs_hop_upgrade:
42
- # Version downgrade detected - return a minimal set of commands
43
- # Commands will be blocked by decorator, but we need them in the list
44
- # so Click doesn't show "No such command" error
45
- return ['check', 'migrate']
44
+ return [] # handled at invocation time in the dev group callback
46
45
 
47
46
  if not self.repo_checked:
48
47
  # Outside hop repository - commands for project initialization
49
48
  return ['init', 'clone']
50
49
 
50
+ # PRODUCTION ENVIRONMENT — read-only, no migrations, no dev commands
51
+ if self.__repo.database.production:
52
+ return ['update', 'upgrade', 'bootstrap']
53
+
51
54
  if self.__repo.needs_migration():
52
55
  return ['migrate']
53
56
 
@@ -56,14 +59,9 @@ class Hop:
56
59
  # Sync-only mode (no metadata)
57
60
  return ['sync-package', 'check']
58
61
 
59
- # Development mode (metadata present)
60
- if self.__repo.database.production:
61
- # PRODUCTION ENVIRONMENT - Release deployment only
62
- return ['update', 'upgrade', 'bootstrap']
63
- else:
64
- # DEVELOPMENT ENVIRONMENT - Patch development
65
- return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
66
- 'revert-migration']
62
+ # DEVELOPMENT ENVIRONMENT - Patch development
63
+ return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
64
+ 'revert-migration']
67
65
 
68
66
  @property
69
67
  def repo_checked(self):
@@ -137,9 +135,28 @@ def create_cli_group():
137
135
 
138
136
  @click.group(cls=VersionCheckGroup, invoke_without_command=True)
139
137
  @click.pass_context
140
- @check_version_before_invoke
141
138
  def dev(ctx):
142
139
  """halfORM development tools - Git-centric patch management and database synchronization"""
140
+ if hop.needs_hop_upgrade:
141
+ error = hop.hop_upgrade_error
142
+ required = error.required_version
143
+ installed = error.installed_version
144
+ click.echo(f"\n Ce repo requiers half-orm-dev {required} "
145
+ f"(installed : {installed}).")
146
+ click.echo(f" Installing the required version...")
147
+ try:
148
+ subprocess.run(
149
+ [sys.executable, '-m', 'pip', 'install', f'half-orm-dev=={required}'],
150
+ check=True,
151
+ )
152
+ except subprocess.CalledProcessError:
153
+ click.echo(f"\n Installation failed.", err=True)
154
+ click.echo(f" Run manually : pip install half-orm-dev=={required}", err=True)
155
+ sys.exit(1)
156
+ click.echo(f" ✓ half-orm-dev {required} installed. Restarting...\n")
157
+ os.execv(sys.argv[0], sys.argv)
158
+ return
159
+
143
160
  if ctx.invoked_subcommand is None:
144
161
  # Show repo state when no subcommand is provided
145
162
  if hop.repo_checked:
@@ -18,6 +18,7 @@ from typing import List, Dict, Optional, Tuple, Any
18
18
  from dataclasses import dataclass
19
19
  import click
20
20
  from git.exc import GitCommandError
21
+ from packaging.version import Version, InvalidVersion
21
22
 
22
23
  from half_orm import utils
23
24
  from half_orm_dev import modules
@@ -2146,9 +2147,6 @@ class PatchManager:
2146
2147
  - Commit the updated schema
2147
2148
  4. Return to original branch
2148
2149
  """
2149
- from packaging.version import Version
2150
- from half_orm_dev.release_file import ReleaseFile
2151
-
2152
2150
  original_branch = self._repo.hgit.branch
2153
2151
  current_ver = Version(version)
2154
2152
 
@@ -2197,9 +2195,6 @@ class PatchManager:
2197
2195
  Args:
2198
2196
  version: Current release version that was just updated
2199
2197
  """
2200
- from packaging.version import Version
2201
- from half_orm_dev.release_file import ReleaseFile
2202
-
2203
2198
  if not hasattr(self, '_pending_higher_releases') or not self._pending_higher_releases:
2204
2199
  return
2205
2200
 
@@ -13,16 +13,17 @@ import subprocess
13
13
 
14
14
  from pathlib import Path
15
15
  from typing import Optional, Tuple, List, Dict, Literal
16
- from dataclasses import dataclass
17
16
  from datetime import datetime, timezone
18
17
 
19
18
  import click
20
19
 
21
20
  from git.exc import GitCommandError
21
+ from packaging.version import Version, InvalidVersion
22
22
  from half_orm_dev.decorators import with_dynamic_branch_lock
23
23
  from half_orm import utils
24
24
  from half_orm_dev.release_file import ReleaseFile
25
25
 
26
+
26
27
  class ReleaseManagerError(Exception):
27
28
  """Base exception for ReleaseManager operations."""
28
29
  pass
@@ -38,69 +39,6 @@ class ReleaseFileError(ReleaseManagerError):
38
39
  pass
39
40
 
40
41
 
41
- @dataclass
42
- class Version:
43
- """Semantic version with stage information."""
44
- major: int
45
- minor: int
46
- patch: int
47
- stage: Optional[str] = None # None, "stage", "rc1", "rc2", "hotfix1", etc.
48
-
49
- def __str__(self) -> str:
50
- """String representation of version."""
51
- base = f"{self.major}.{self.minor}.{self.patch}"
52
- if self.stage:
53
- return f"{base}-{self.stage}"
54
- return base
55
-
56
- def __lt__(self, other: 'Version') -> bool:
57
- """Compare versions for sorting."""
58
- # Compare base version first
59
- if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
60
- return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
61
-
62
- # If base versions equal, compare stages
63
- # Priority: production (None) > rc > stage > hotfix
64
- stage_priority = {
65
- None: 4, # Production (highest)
66
- 'rc': 3, # Release candidate
67
- 'stage': 2, # Development stage
68
- 'hotfix': 1 # Hotfix (lowest)
69
- }
70
-
71
- # Extract stage type (rc1 → rc, hotfix2 → hotfix)
72
- self_stage_type = self._get_stage_type()
73
- other_stage_type = other._get_stage_type()
74
-
75
- self_priority = stage_priority.get(self_stage_type, 0)
76
- other_priority = stage_priority.get(other_stage_type, 0)
77
-
78
- # If different stage types, compare by priority
79
- if self_priority != other_priority:
80
- return self_priority < other_priority
81
-
82
- # Same stage type - compare stage strings for RC/hotfix numbers
83
- # rc2 > rc1, hotfix2 > hotfix1
84
- if self.stage and other.stage:
85
- return self.stage < other.stage
86
-
87
- return False
88
-
89
- def _get_stage_type(self) -> Optional[str]:
90
- """Extract stage type from stage string."""
91
- if not self.stage:
92
- return None
93
-
94
- if self.stage == 'stage':
95
- return 'stage'
96
- elif self.stage.startswith('rc'):
97
- return 'rc'
98
- elif self.stage.startswith('hotfix'):
99
- return 'hotfix'
100
-
101
- return None
102
-
103
-
104
42
  class ReleaseManager:
105
43
  """
106
44
  Manages release files and version lifecycle.
@@ -474,21 +412,16 @@ class ReleaseManager:
474
412
  if not release_files:
475
413
  return None
476
414
 
477
- # Parse all valid versions
478
415
  versions = []
479
- for release_file in release_files:
416
+ for f in release_files:
480
417
  try:
481
- version = self.parse_version_from_filename(release_file.name)
482
- versions.append(version)
483
- except ReleaseVersionError:
484
- # Ignore files with invalid format
418
+ versions.append(Version(f.stem))
419
+ except InvalidVersion:
485
420
  continue
486
421
 
487
422
  if not versions:
488
423
  return None
489
424
 
490
- # Sort versions and return latest
491
- # Version.__lt__ handles sorting with stage priority
492
425
  return max(versions)
493
426
 
494
427
 
@@ -559,83 +492,11 @@ class ReleaseManager:
559
492
  elif increment_type == 'minor':
560
493
  return f"{current_version.major}.{current_version.minor + 1}.0"
561
494
  elif increment_type == 'patch':
562
- return f"{current_version.major}.{current_version.minor}.{current_version.patch + 1}"
495
+ return f"{current_version.major}.{current_version.minor}.{current_version.micro + 1}"
563
496
 
564
497
  # Should never reach here due to validation above
565
498
  raise ReleaseVersionError(f"Unexpected increment type: {increment_type}")
566
499
 
567
- @classmethod
568
- def parse_version_from_filename(cls, filename: str) -> Version:
569
- """
570
- Parse version from release filename.
571
-
572
- Extracts semantic version and stage from release filename.
573
-
574
- Supported formats:
575
- - X.Y.Z.txt → Version(X, Y, Z, stage=None)
576
- - X.Y.Z-stage.txt → Version(X, Y, Z, stage="stage")
577
- - X.Y.Z-rc1.txt → Version(X, Y, Z, stage="rc1")
578
- - X.Y.Z-hotfix1.txt → Version(X, Y, Z, stage="hotfix1")
579
-
580
- Args:
581
- filename: Release filename (e.g., "1.3.5-rc2.txt")
582
-
583
- Returns:
584
- Version: Parsed version object
585
-
586
- Raises:
587
- ReleaseVersionError: If filename format invalid
588
-
589
- Examples:
590
- ver = release_mgr.parse_version_from_filename("1.3.5.txt")
591
- # Version(1, 3, 5, stage=None)
592
-
593
- ver = release_mgr.parse_version_from_filename("1.4.0-stage.txt")
594
- # Version(1, 4, 0, stage="stage")
595
-
596
- ver = release_mgr.parse_version_from_filename("1.3.5-rc2.txt")
597
- # Version(1, 3, 5, stage="rc2")
598
- """
599
- # Extract just filename if path provided
600
- filename = Path(filename).name
601
-
602
- # Validate not empty
603
- if not filename:
604
- raise ReleaseVersionError("Invalid format: empty filename")
605
-
606
- # Must end with .txt
607
- if not filename.endswith('.txt'):
608
- raise ReleaseVersionError(f"Invalid format: missing .txt extension in '{filename}'")
609
-
610
- # Remove .txt extension
611
- version_str = filename[:-4]
612
-
613
- # Pattern: X.Y.Z or X.Y.Z-stage or X.Y.Z-rc1 or X.Y.Z-hotfix1
614
- pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(stage|rc\d+|hotfix\d+))?$'
615
-
616
- match = re.match(pattern, version_str)
617
-
618
- if not match:
619
- raise ReleaseVersionError(
620
- f"Invalid format: '{filename}' does not match X.Y.Z[-stage].txt pattern"
621
- )
622
-
623
- major, minor, patch, stage = match.groups()
624
-
625
- # Convert to integers
626
- try:
627
- major = int(major)
628
- minor = int(minor)
629
- patch = int(patch)
630
- except ValueError:
631
- raise ReleaseVersionError(f"Invalid format: non-numeric version components in '{filename}'")
632
-
633
- # Validate non-negative
634
- if major < 0 or minor < 0 or patch < 0:
635
- raise ReleaseVersionError(f"Invalid format: negative version numbers in '{filename}'")
636
-
637
- return Version(major, minor, patch, stage)
638
-
639
500
  def get_next_release_version(self) -> Optional[str]:
640
501
  """
641
502
  Détermine LA prochaine release à déployer.
@@ -646,8 +507,7 @@ class ReleaseManager:
646
507
  production_str = self._get_production_version()
647
508
 
648
509
  for level in ['patch', 'minor', 'major']:
649
- next_version = self.calculate_next_version(
650
- self.parse_version_from_filename(f"{production_str}.txt"), level)
510
+ next_version = self.calculate_next_version(Version(production_str), level)
651
511
 
652
512
  # Cherche RC ou patches TOML pour cette version
653
513
  rc_pattern = f"{next_version}-rc*.txt"
@@ -662,12 +522,12 @@ class ReleaseManager:
662
522
  """
663
523
  Liste tous les fichiers <label> pour une version, triés par numéro.
664
524
 
665
- Returns:
666
- Liste triée (ex: ["1.3.6-rc1.txt", "1.3.6-rc2.txt"])
525
+ RC uses hyphen separator (1.3.6-rc1.txt).
526
+ Post-releases use dot separator (1.3.6.post1.txt).
667
527
  """
668
- pattern = f"{version}-{label}*.txt"
669
- reg_ex = rf'-{label}(\d+)\.txt$'
670
- label_pattern = re.compile(reg_ex)
528
+ sep = '.' if label == 'post' else '-'
529
+ pattern = f"{version}{sep}{label}*.txt"
530
+ label_pattern = re.compile(rf'[.-]{label}(\d+)\.txt$')
671
531
  files = list(self._releases_dir.glob(pattern))
672
532
 
673
533
  return sorted(files, key=lambda f: int(re.search(label_pattern, f.name).group(1)))
@@ -806,7 +666,7 @@ class ReleaseManager:
806
666
  else:
807
667
  # Production: read from hotfix snapshot if it exists
808
668
  # This handles the case where we're applying a hotfix release
809
- hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
669
+ hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
810
670
  if hotfix_files:
811
671
  # Apply the latest hotfix
812
672
  stage_patches = self.read_release_patches(hotfix_files[-1].name)
@@ -892,7 +752,7 @@ class ReleaseManager:
892
752
  all_patches.extend(self.read_release_patches(base_file))
893
753
 
894
754
  # 2. Hotfix patches in order
895
- hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
755
+ hotfix_files = sorted(self._releases_dir.glob(f"{version}.post*.txt"))
896
756
  for hotfix_file in hotfix_files:
897
757
  all_patches.extend(self.read_release_patches(hotfix_file.name))
898
758
 
@@ -1483,7 +1343,7 @@ class ReleaseManager:
1483
1343
  patches = [line.strip() for line in content.split('\n') if line.strip()]
1484
1344
 
1485
1345
  # Only include releases newer than current version
1486
- if self._version_is_newer(version, current_version):
1346
+ if Version(version) > Version(current_version):
1487
1347
  available_releases.append({
1488
1348
  'tag': tag,
1489
1349
  'version': version,
@@ -1550,43 +1410,19 @@ class ReleaseManager:
1550
1410
  except Exception as e:
1551
1411
  raise ReleaseManagerError(f"Failed to read tags from repository: {e}")
1552
1412
 
1553
- # Filter for release tags (v*.*.*) with optional -rc or -hotfix suffix
1554
- release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d+|-hotfix\d+)?$')
1413
+ # Filter for release tags: v1.3.5, v1.3.5-rc1, v1.3.5.post1
1414
+ release_pattern = re.compile(r'^v\d+\.\d+\.\d+(-rc\d+|\.post\d+)?$')
1555
1415
  release_tags = []
1556
1416
 
1557
1417
  for tag in all_tags:
1558
1418
  tag_name = tag.name
1559
1419
  if release_pattern.match(tag_name):
1560
- # Filter RC tags unless explicitly allowed
1561
1420
  if '-rc' in tag_name and not allow_rc:
1562
1421
  continue
1563
1422
  release_tags.append(tag_name)
1564
1423
 
1565
- # Sort tags by version (semantic versioning)
1566
- def version_key(tag_name):
1567
- """Extract sortable version tuple from tag name."""
1568
- # Remove 'v' prefix
1569
- version_str = tag_name[1:]
1570
-
1571
- # Split version and suffix
1572
- if '-rc' in version_str:
1573
- base_ver, rc_suffix = version_str.split('-rc')
1574
- rc_num = int(rc_suffix)
1575
- suffix_weight = (1, rc_num) # RC comes before production
1576
- elif '-hotfix' in version_str:
1577
- base_ver, hotfix_suffix = version_str.split('-hotfix')
1578
- hotfix_num = int(hotfix_suffix)
1579
- suffix_weight = (2, hotfix_num) # Hotfix comes after production
1580
- else:
1581
- base_ver = version_str
1582
- suffix_weight = (1.5, 0) # Production between RC and hotfix
1583
-
1584
- # Parse base version
1585
- major, minor, patch = map(int, base_ver.split('.'))
1586
-
1587
- return (major, minor, patch, suffix_weight)
1588
-
1589
- release_tags.sort(key=version_key)
1424
+ # Sort using packaging.version (PEP 440 ordering)
1425
+ release_tags.sort(key=lambda t: Version(t[1:]))
1590
1426
 
1591
1427
  return release_tags
1592
1428
 
@@ -1621,77 +1457,31 @@ class ReleaseManager:
1621
1457
  path = mgr._calculate_upgrade_path("1.4.0", "1.4.0")
1622
1458
  # → []
1623
1459
  """
1624
- # Parse versions
1625
- current_version = self.parse_version_from_filename(f"{current}.txt")
1626
- target_version = self.parse_version_from_filename(f"{target}.txt")
1460
+ current_version = Version(current)
1461
+ target_version = Version(target)
1627
1462
 
1628
- # If same version, no upgrade needed
1629
1463
  if current == target:
1630
1464
  return []
1631
1465
 
1632
- # Get all available release tags (production only)
1633
1466
  available_tags = self._get_available_release_tags(allow_rc=False)
1634
1467
 
1635
- # Extract versions from tags and parse them
1636
1468
  available_versions = []
1637
1469
  for tag in available_tags:
1638
- # Remove 'v' prefix: v1.3.6 → 1.3.6
1639
1470
  version_str = tag[1:] if tag.startswith('v') else tag
1640
-
1641
- # Skip if not a valid production version format
1642
- if not re.match(r'^\d+\.\d+\.\d+$', version_str):
1643
- continue
1644
-
1645
1471
  try:
1646
- version = self.parse_version_from_filename(f"{version_str}.txt")
1647
- available_versions.append((version_str, version))
1648
- except (ReleaseManagerError, ValueError):
1472
+ available_versions.append((version_str, Version(version_str)))
1473
+ except InvalidVersion:
1649
1474
  continue
1650
1475
 
1651
- # Sort versions
1652
- available_versions.sort(key=lambda x: (x[1].major, x[1].minor, x[1].patch))
1476
+ available_versions.sort(key=lambda x: x[1])
1653
1477
 
1654
- # Build sequential path from current to target
1655
1478
  path = []
1656
1479
  for version_str, version in available_versions:
1657
- # Skip versions <= current
1658
- if (version.major, version.minor, version.patch) <= \
1659
- (current_version.major, current_version.minor, current_version.patch):
1660
- continue
1661
-
1662
- # Add versions <= target
1663
- if (version.major, version.minor, version.patch) <= \
1664
- (target_version.major, target_version.minor, target_version.patch):
1480
+ if current_version < version <= target_version:
1665
1481
  path.append(version_str)
1666
1482
 
1667
1483
  return path
1668
1484
 
1669
- def _version_is_newer(self, version1: str, version2: str) -> bool:
1670
- """
1671
- Compare two version strings to check if version1 is newer than version2.
1672
-
1673
- Args:
1674
- version1: First version (e.g., "1.3.6", "1.3.6-rc1")
1675
- version2: Second version (e.g., "1.3.5")
1676
-
1677
- Returns:
1678
- bool: True if version1 > version2
1679
-
1680
- Examples:
1681
- _version_is_newer("1.3.6", "1.3.5") # → True
1682
- _version_is_newer("1.3.5", "1.3.6") # → False
1683
- _version_is_newer("1.3.6-rc1", "1.3.5") # → True
1684
- """
1685
- # Extract base versions (remove suffix)
1686
- base1 = version1.split('-')[0]
1687
- base2 = version2.split('-')[0]
1688
-
1689
- # Parse versions
1690
- parts1 = tuple(map(int, base1.split('.')))
1691
- parts2 = tuple(map(int, base2.split('.')))
1692
-
1693
- return parts1 > parts2
1694
-
1695
1485
  def upgrade_production(
1696
1486
  self,
1697
1487
  to_version: Optional[str] = None,
@@ -2431,8 +2221,6 @@ class ReleaseManager:
2431
2221
  Returns:
2432
2222
  Version string of base release, or None if should use prod schema
2433
2223
  """
2434
- from packaging.version import Version
2435
-
2436
2224
  new_ver = Version(new_version)
2437
2225
  model_dir = Path(self._repo.model_dir)
2438
2226
 
@@ -3114,7 +2902,7 @@ class ReleaseManager:
3114
2902
  return 1
3115
2903
 
3116
2904
  last_file = files[-1].name
3117
- reg_ex = rf'-{label}(\d+)\.txt'
2905
+ reg_ex = rf'[.-]{label}(\d+)\.txt'
3118
2906
  match = re.search(reg_ex, last_file)
3119
2907
  if match:
3120
2908
  last_num = int(match.group(1))
@@ -3303,26 +3091,26 @@ class ReleaseManager:
3303
3091
  f" 3. OR move to another release (edit patches file manually)"
3304
3092
  )
3305
3093
 
3306
- # 3. Determine next hotfix number
3307
- hotfix_num = self._get_latest_label_number(version, 'hotfix')
3308
- hotfix_tag = f"v{version}-hotfix{hotfix_num}"
3094
+ # 3. Determine next post-release number
3095
+ post_num = self._get_latest_label_number(version, 'post')
3096
+ hotfix_tag = f"v{version}.post{post_num}"
3309
3097
 
3310
3098
  # 4. Switch to ho-prod and merge
3311
3099
  self._repo.hgit.checkout("ho-prod")
3312
3100
 
3313
3101
  # Merge ho-release/X.Y.Z into ho-prod
3314
- merge_msg = f"[release] Merge hotfix %{version}-hotfix{hotfix_num}"
3102
+ merge_msg = f"[release] Merge hotfix %{version}.post{post_num}"
3315
3103
  self._repo.hgit.merge(current_branch, message=merge_msg)
3316
3104
 
3317
- # 5. Create hotfix snapshot file from staged patches
3105
+ # 5. Create post-release snapshot file from staged patches
3318
3106
  toml_file = self._releases_dir / f"{version}-patches.toml"
3319
- hotfix_file = self._releases_dir / f"{version}-hotfix{hotfix_num}.txt"
3107
+ hotfix_file = self._releases_dir / f"{version}.post{post_num}.txt"
3320
3108
 
3321
3109
  if release_file.exists():
3322
3110
  # Get staged patches from TOML file
3323
3111
  staged_patches = release_file.get_patches(status="staged")
3324
3112
 
3325
- # Write snapshot to hotfix TXT file (production format)
3113
+ # Write snapshot to post-release TXT file (production format)
3326
3114
  hotfix_file.write_text("\n".join(staged_patches) + "\n" if staged_patches else "", encoding='utf-8')
3327
3115
  # Delete TOML patches file (no longer needed)
3328
3116
  if toml_file.exists():
@@ -3333,12 +3121,12 @@ class ReleaseManager:
3333
3121
 
3334
3122
  # 7. Commit release file changes and sync to active branches
3335
3123
  sync_result = self._repo.commit_and_sync_to_active_branches(
3336
- message=f"[HOP] Finalize hotfix %{version}-hotfix{hotfix_num} release files",
3337
- reason=f"hotfix {version}-hotfix{hotfix_num}"
3124
+ message=f"[HOP] Finalize hotfix %{version}.post{post_num} release files",
3125
+ reason=f"hotfix {version}.post{post_num}"
3338
3126
  )
3339
3127
 
3340
- # 8. Create hotfix tag on ho-prod
3341
- self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}-hotfix{hotfix_num}")
3128
+ # 8. Create post-release tag on ho-prod
3129
+ self._repo.hgit.create_tag(hotfix_tag, f"Hotfix release %{version}.post{post_num}")
3342
3130
  self._repo.hgit.push_tag(hotfix_tag)
3343
3131
 
3344
3132
  deleted_branches = []
@@ -484,6 +484,14 @@ class Repo:
484
484
  'errors': []
485
485
  }
486
486
 
487
+ if self.production:
488
+ raise RepoError(
489
+ "Repository migration is not available on a production server.\n\n"
490
+ " Production servers are read-only: no commits or pushes to origin.\n"
491
+ " Run 'hop migrate' on a development machine first, then deploy."
492
+ )
493
+
494
+ self._migration_running = True
487
495
  try:
488
496
  # Create migration manager
489
497
  migration_mgr = MigrationManager(self)
@@ -584,18 +592,17 @@ class Repo:
584
592
  print(f" You are now on ho-prod", file=sys.stderr)
585
593
 
586
594
  except RepoError:
587
- # Re-raise RepoError (for branch check)
588
595
  raise
589
596
  except MigrationManagerError as e:
590
- # Log migration errors
591
597
  error_msg = f"Migration failed: {e}"
592
598
  result['errors'].append(error_msg)
593
599
  raise RepoError(error_msg) from e
594
600
  except Exception as e:
595
- # Catch any unexpected errors
596
601
  error_msg = f"Unexpected migration error: {e}"
597
602
  result['errors'].append(error_msg)
598
603
  raise RepoError(error_msg) from e
604
+ finally:
605
+ self._migration_running = False
599
606
 
600
607
  return result
601
608
 
@@ -918,9 +925,11 @@ class Repo:
918
925
  installed_version = hop_version()
919
926
  required_version = self.__config.hop_version
920
927
 
921
- # Validate version compatibility
922
- if not _is_version_compatible(installed_version, required_version):
923
- raise OutdatedHalfORMDevError(required_version, installed_version)
928
+ # Validate version compatibility — strict: installed must equal required.
929
+ # Skip during migration: the config is being updated, versions are transiently mismatched.
930
+ if not getattr(self, '_migration_running', False):
931
+ if self.compare_versions(installed_version, required_version) != 0:
932
+ raise OutdatedHalfORMDevError(required_version, installed_version)
924
933
 
925
934
  except RepoError:
926
935
  # Re-raise RepoError (dirty working directory)
@@ -928,16 +937,14 @@ class Repo:
928
937
  except OutdatedHalfORMDevError:
929
938
  # Re-raise version error (repository was updated to newer version)
930
939
  raise
931
- except Exception as e:
932
- # If we're in detached HEAD or any error, try to return to original state
933
- # and continue (offline mode, no remote, etc.)
940
+ except (GitCommandError, IndexError, KeyError, ValueError) as e:
941
+ # Git/network errors (offline mode, missing branch, no remote, etc.)
942
+ # Restore branch if needed and continue these are non-fatal.
934
943
  try:
935
944
  if current_branch and git_repo:
936
945
  git_repo.heads[current_branch].checkout()
937
946
  except (GitCommandError, IndexError, TypeError):
938
947
  pass
939
- # Log but don't fail (offline mode, no remote, etc.)
940
- # Only critical errors (dirty repo, version mismatch) should block
941
948
 
942
949
  def commit_and_sync_to_active_branches(
943
950
  self,
@@ -0,0 +1 @@
1
+ 1.0.0-a19
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a18
3
+ Version: 1.0.0a19
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
@@ -1 +0,0 @@
1
- 1.0.0-a18
File without changes
File without changes