half-orm-dev 1.0.0a15__tar.gz → 1.0.0a16__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.0a15/half_orm_dev.egg-info → half_orm_dev-1.0.0a16}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/hgit.py +22 -0
  3. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migration_manager.py +34 -2
  4. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/release_manager.py +55 -15
  5. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/repo.py +35 -14
  6. half_orm_dev-1.0.0a16/half_orm_dev/version.txt +1 -0
  7. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16/half_orm_dev.egg-info}/PKG-INFO +1 -1
  8. half_orm_dev-1.0.0a15/half_orm_dev/version.txt +0 -1
  9. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/AUTHORS +0 -0
  10. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/LICENSE +0 -0
  11. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/README.md +0 -0
  12. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/__init__.py +0 -0
  13. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/bootstrap_manager.py +0 -0
  14. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/__init__.py +0 -0
  15. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/__init__.py +0 -0
  16. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/apply.py +0 -0
  17. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  18. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/check.py +0 -0
  19. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/clone.py +0 -0
  20. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/init.py +0 -0
  21. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/migrate.py +0 -0
  22. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/patch.py +0 -0
  23. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/release.py +0 -0
  24. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/restore.py +0 -0
  25. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  26. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  27. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/sync.py +0 -0
  28. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/todo.py +0 -0
  29. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/undo.py +0 -0
  30. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/update.py +0 -0
  31. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/commands/upgrade.py +0 -0
  32. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli/main.py +0 -0
  33. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/cli_extension.py +0 -0
  34. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/database.py +0 -0
  35. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/decorators.py +0 -0
  36. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/file_executor.py +0 -0
  37. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  38. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  39. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  40. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  41. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  42. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  43. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  44. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  45. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  46. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/modules.py +0 -0
  47. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patch_manager.py +0 -0
  48. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patch_validator.py +0 -0
  49. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  50. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  51. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  52. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patches/log +0 -0
  53. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  54. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/release_file.py +0 -0
  55. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/scripts/repair-metadata.py +0 -0
  56. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/.gitignore +0 -0
  57. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/MANIFEST.in +0 -0
  58. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/README +0 -0
  59. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/conftest_template +0 -0
  60. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  61. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  62. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/init_module_template +0 -0
  63. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/module_template_1 +0 -0
  64. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/module_template_2 +0 -0
  65. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/module_template_3 +0 -0
  66. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/pyproject.toml +0 -0
  67. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/relation_test +0 -0
  68. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/sql_adapter +0 -0
  69. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/templates/warning +0 -0
  70. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev/utils.py +0 -0
  71. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev.egg-info/SOURCES.txt +0 -0
  72. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  73. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev.egg-info/entry_points.txt +0 -0
  74. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev.egg-info/requires.txt +0 -0
  75. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/half_orm_dev.egg-info/top_level.txt +0 -0
  76. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/pyproject.toml +0 -0
  77. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/setup.cfg +0 -0
  78. {half_orm_dev-1.0.0a15 → half_orm_dev-1.0.0a16}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a15
3
+ Version: 1.0.0a16
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
@@ -931,6 +931,28 @@ class HGit:
931
931
  print(f"⚠️ Warning: Failed to delete local lock tag {lock_tag}: {e}")
932
932
 
933
933
 
934
+ def is_branch_locked(self, branch_name: str, timeout_minutes: int = 30) -> bool:
935
+ """Return True if a valid (non-stale) lock tag for branch_name exists on origin."""
936
+ self.fetch_tags()
937
+ safe_branch_name = branch_name.replace('/', '-')
938
+ lock_pattern = f"lock-{safe_branch_name}-*"
939
+ existing_locks = self.list_tags(pattern=lock_pattern)
940
+ if not existing_locks:
941
+ return False
942
+ lock_name = existing_locks[0]
943
+ remote_ref = self.__git_repo.git.ls_remote(
944
+ '--tags', 'origin', f'refs/tags/{lock_name}'
945
+ ).strip()
946
+ if not remote_ref:
947
+ return False
948
+ match = re.search(r'-(\d+)$', lock_name)
949
+ if not match:
950
+ return True
951
+ lock_timestamp_ms = int(match.group(1))
952
+ lock_time = datetime.fromtimestamp(lock_timestamp_ms / 1000.0, tz=timezone.utc)
953
+ age_minutes = (datetime.now(timezone.utc) - lock_time).total_seconds() / 60
954
+ return age_minutes <= timeout_minutes
955
+
934
956
  def list_tags(self, pattern: Optional[str] = None) -> List[str]:
935
957
  """
936
958
  List tags matching glob pattern.
@@ -25,7 +25,9 @@ import re
25
25
  import subprocess
26
26
  import sys
27
27
  import importlib.util
28
+ from configparser import ConfigParser
28
29
  from packaging import version
30
+ import click
29
31
  from pathlib import Path
30
32
  from typing import List, Dict, Optional, Tuple
31
33
  from half_orm import utils
@@ -283,14 +285,44 @@ class MigrationManager:
283
285
  )
284
286
  ahead, behind = map(int, result_check.stdout.strip().split())
285
287
  if behind > 0:
288
+ # Check whether the migration was already applied on origin/ho-prod.
289
+ already_done = False
290
+ try:
291
+ remote_config_text = self._repo.hgit._HGit__git_repo.git.show(
292
+ 'origin/ho-prod:.hop/config'
293
+ )
294
+ _cp = ConfigParser()
295
+ _cp.read_string(remote_config_text)
296
+ remote_version = _cp['halfORM'].get('hop_version', '')
297
+ if remote_version == target_version:
298
+ already_done = True
299
+ except Exception:
300
+ pass
301
+
302
+ if already_done:
303
+ # Another developer already ran the migration.
304
+ # Pull and sync all active branches, then return.
305
+ click.echo(
306
+ f" ℹ Migration to {target_version} already applied by another developer.\n"
307
+ f" Pulling and syncing all active branches..."
308
+ )
309
+ self._repo.hgit._HGit__git_repo.remotes.origin.pull('ho-prod')
310
+ self._repo.hgit.sync_active_branches(pattern="ho-*")
311
+ # Reload config so the rest of the tool sees the new version
312
+ self._repo._Repo__config.read()
313
+ result['already_synced'] = True
314
+ return result
315
+
286
316
  raise MigrationManagerError(
287
317
  f"ho-prod is {behind} commits behind origin/ho-prod. "
288
318
  f"Please pull changes first: git pull origin ho-prod"
289
319
  )
290
320
  if ahead > 0:
291
321
  result['errors'].append(f"Warning: ho-prod is {ahead} commits ahead of origin/ho-prod")
292
- except subprocess.CalledProcessError as e:
293
- # Could not compare - maybe origin/ho-prod doesn't exist yet
322
+ except (MigrationManagerError, subprocess.CalledProcessError):
323
+ raise
324
+ except Exception:
325
+ # Could not compare — maybe origin/ho-prod doesn't exist yet
294
326
  pass
295
327
 
296
328
  # Get current version from .hop/config
@@ -1430,6 +1430,31 @@ class ReleaseManager:
1430
1430
  f"Cannot read current production version from database: {e}"
1431
1431
  )
1432
1432
 
1433
+ # Migration: production servers that still track ho-prod instead of
1434
+ # ho-current. Create ho-current from the current version's immutable tag
1435
+ # and switch to it — local-only, no push to origin.
1436
+ if (
1437
+ self._repo.production
1438
+ and self._repo.hgit.branch == 'ho-prod'
1439
+ and not self._repo.hgit.branch_exists('ho-current')
1440
+ ):
1441
+ current_tag = f'v{current_version}'
1442
+ tag_exists = any(
1443
+ t.name == current_tag
1444
+ for t in self._repo.hgit._HGit__git_repo.tags
1445
+ )
1446
+ if not tag_exists:
1447
+ raise ReleaseManagerError(
1448
+ f"Cannot migrate to ho-current: tag {current_tag} not found locally.\n"
1449
+ f"Run 'hop update' again after ensuring tags are fetched."
1450
+ )
1451
+ self._repo.hgit.create_branch_from_tag('ho-current', current_tag)
1452
+ self._repo.hgit._HGit__git_repo.heads['ho-current'].checkout()
1453
+ click.echo(
1454
+ f" ℹ Migrated to ho-current (created from {current_tag}, local only).\n"
1455
+ f" Production servers should now use ho-current instead of ho-prod."
1456
+ )
1457
+
1433
1458
  # 3. Build list of available releases with details
1434
1459
  available_releases = []
1435
1460
 
@@ -1855,25 +1880,31 @@ class ReleaseManager:
1855
1880
  'final_version': upgrade_path[-1] if upgrade_path else current_version
1856
1881
  }
1857
1882
 
1858
- # === 5. Acquire lock, pull ho-prod, apply releases ===
1859
- # The lock ensures the pull does not fetch a partial state left by a
1860
- # push in progress on ho-prod (e.g. patch merge or release promote
1861
- # pushing files in several steps).
1883
+ # === 5. Get release files and apply releases ===
1884
+ # ho-prod: acquire lock + pull (rolling branch, needs lock against partial pushes)
1885
+ # ho-current: fetch + reset --hard vX.Y.Z per version (immutable tags, no lock needed)
1862
1886
  lock_tag = None
1863
1887
  patches_applied = {}
1888
+ on_ho_current = self._repo.hgit.branch == "ho-current"
1864
1889
  try:
1865
- lock_tag = self._repo.hgit.acquire_branch_lock('ho-prod')
1866
-
1867
- # Pull ho-prod to get release files
1868
- # update_production only fetches tags, we need the actual release files
1869
- try:
1870
- self._repo.hgit.pull('origin', 'ho-prod')
1871
- except Exception as e:
1872
- raise ReleaseManagerError(f"Failed to pull ho-prod branch: {e}")
1890
+ if on_ho_current:
1891
+ try:
1892
+ self._repo.hgit.fetch_tags()
1893
+ self._repo.hgit._HGit__git_repo.remotes.origin.fetch()
1894
+ except Exception as e:
1895
+ raise ReleaseManagerError(f"Failed to fetch from origin: {e}")
1896
+ else:
1897
+ lock_tag = self._repo.hgit.acquire_branch_lock('ho-prod')
1898
+ try:
1899
+ self._repo.hgit.pull('origin', 'ho-prod')
1900
+ except Exception as e:
1901
+ raise ReleaseManagerError(f"Failed to pull ho-prod branch: {e}")
1873
1902
 
1874
1903
  # Apply releases sequentially
1875
1904
  try:
1876
1905
  for version in upgrade_path:
1906
+ if on_ho_current:
1907
+ self._repo.hgit._HGit__git_repo.git.reset('--hard', f'v{version}')
1877
1908
  applied_patches = self._apply_release_to_production(version)
1878
1909
  patches_applied[version] = applied_patches
1879
1910
  except Exception as e:
@@ -2033,12 +2064,21 @@ class ReleaseManager:
2033
2064
  # → Raises: "Repository has uncommitted changes"
2034
2065
  """
2035
2066
  # Check branch
2036
- if self._repo.hgit.branch != "ho-prod":
2067
+ current_branch = self._repo.hgit.branch
2068
+ if current_branch not in ("ho-prod", "ho-current"):
2037
2069
  raise ReleaseManagerError(
2038
- f"Must be on ho-prod branch for production upgrade. "
2039
- f"Current branch: {self._repo.hgit.branch}"
2070
+ f"Must be on ho-prod or ho-current branch for production upgrade. "
2071
+ f"Current branch: {current_branch}"
2040
2072
  )
2041
2073
 
2074
+ # For ho-current: reject if a lock is held on ho-prod (dev operation in progress)
2075
+ if current_branch == "ho-current":
2076
+ if self._repo.hgit.is_branch_locked("ho-prod"):
2077
+ raise ReleaseManagerError(
2078
+ "Cannot upgrade: ho-prod is currently locked by a development operation.\n"
2079
+ "Wait for the operation to complete and retry."
2080
+ )
2081
+
2042
2082
  # Check repo is clean
2043
2083
  if not self._repo.hgit.repos_is_clean():
2044
2084
  raise ReleaseManagerError(
@@ -541,23 +541,31 @@ class Repo:
541
541
  create_commit=True
542
542
  )
543
543
 
544
- result['migration_run'] = True
545
544
  result['errors'] = migration_result.get('errors', [])
546
545
 
547
- # Clean up orphaned ho-staged/* branches (patch IDs in .txt = already in production)
548
- if hasattr(self, 'release_manager'):
549
- try:
550
- deleted = self.release_manager.cleanup_orphaned_staged_branches()
551
- result['orphaned_staged_deleted'] = deleted
552
- except Exception:
553
- result['orphaned_staged_deleted'] = []
546
+ if migration_result.get('already_synced'):
547
+ # Migration was already done by another developer; branches are now synced.
548
+ result['migration_run'] = False
549
+ result['already_synced'] = True
550
+ if not silent:
551
+ print(f"✓ Branches synced to migration {current_version} (applied by another developer)")
552
+ else:
553
+ result['migration_run'] = True
554
554
 
555
- # Log success if not silent
556
- if not silent:
557
- if migration_result.get('migrations_applied'):
558
- print(f"✓ Applied {len(migration_result['migrations_applied'])} migration(s)")
559
- else:
560
- print(f"✓ Updated repository version to {current_version}")
555
+ # Clean up orphaned ho-staged/* branches (patch IDs in .txt = already in production)
556
+ if hasattr(self, 'release_manager'):
557
+ try:
558
+ deleted = self.release_manager.cleanup_orphaned_staged_branches()
559
+ result['orphaned_staged_deleted'] = deleted
560
+ except Exception:
561
+ result['orphaned_staged_deleted'] = []
562
+
563
+ # Log success if not silent
564
+ if not silent:
565
+ if migration_result.get('migrations_applied'):
566
+ print(f"✓ Applied {len(migration_result['migrations_applied'])} migration(s)")
567
+ else:
568
+ print(f"✓ Updated repository version to {current_version}")
561
569
 
562
570
  finally:
563
571
  # Always try to return to original branch if we switched
@@ -1600,6 +1608,19 @@ class Repo:
1600
1608
 
1601
1609
  result['branch_sync'] = sync_result
1602
1610
 
1611
+ # 0c. Reload config and validate version after pull/sync.
1612
+ # Another developer may have run the migration and pushed a new hop_version.
1613
+ # The pull + sync above may have updated .hop/config on the current branch;
1614
+ # we must detect that before any further operation.
1615
+ if self.hgit:
1616
+ try:
1617
+ self.__config = Config(self.__base_dir)
1618
+ self._validate_version()
1619
+ except OutdatedHalfORMDevError:
1620
+ raise
1621
+ except Exception:
1622
+ pass # offline or version parsing error — don't block
1623
+
1603
1624
  # 1. Check and update Git hooks
1604
1625
  if not dry_run:
1605
1626
  result['hooks'] = self.install_git_hooks()
@@ -0,0 +1 @@
1
+ 1.0.0-a16
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a15
3
+ Version: 1.0.0a16
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-a15
File without changes
File without changes