half-orm-dev 1.0.0a30__tar.gz → 1.0.0a31__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 (83) hide show
  1. {half_orm_dev-1.0.0a30/half_orm_dev.egg-info → half_orm_dev-1.0.0a31}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/bootstrap_manager.py +6 -2
  3. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/file_executor.py +68 -0
  4. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/release_manager.py +69 -69
  5. half_orm_dev-1.0.0a31/half_orm_dev/version.txt +1 -0
  6. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31/half_orm_dev.egg-info}/PKG-INFO +1 -1
  7. half_orm_dev-1.0.0a30/half_orm_dev/version.txt +0 -1
  8. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/AUTHORS +0 -0
  9. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/LICENSE +0 -0
  10. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/README.md +0 -0
  11. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/__init__.py +0 -0
  12. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/__init__.py +0 -0
  13. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/__init__.py +0 -0
  14. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/apply.py +0 -0
  15. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  16. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/check.py +0 -0
  17. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/clone.py +0 -0
  18. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/init.py +0 -0
  19. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/migrate.py +0 -0
  20. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/patch.py +0 -0
  21. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/recover.py +0 -0
  22. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/release.py +0 -0
  23. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/restore.py +0 -0
  24. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  25. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/rollback.py +0 -0
  26. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  27. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/sync.py +0 -0
  28. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/todo.py +0 -0
  29. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/undo.py +0 -0
  30. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/commands/upgrade.py +0 -0
  31. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli/main.py +0 -0
  32. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/cli_extension.py +0 -0
  33. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/database.py +0 -0
  34. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/decorators.py +0 -0
  35. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/hgit.py +0 -0
  36. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migration_manager.py +0 -0
  37. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  38. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  39. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  40. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  41. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  42. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  43. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  44. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  45. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +0 -0
  46. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  47. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/modules.py +0 -0
  48. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patch_manager.py +0 -0
  49. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patch_validator.py +0 -0
  50. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  51. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  52. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  53. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patches/log +0 -0
  54. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  55. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/py.typed +0 -0
  56. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/release_file.py +0 -0
  57. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/repo.py +0 -0
  58. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/scripts/repair-metadata.py +0 -0
  59. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/.gitignore +0 -0
  60. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/MANIFEST.in +0 -0
  61. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/README +0 -0
  62. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/conftest_template +0 -0
  63. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  64. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/git-hooks/pre-push +0 -0
  65. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  66. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
  67. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/init_module_template +0 -0
  68. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/module_template_1 +0 -0
  69. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/module_template_2 +0 -0
  70. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/module_template_3 +0 -0
  71. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/pyproject.toml +0 -0
  72. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/relation_test +0 -0
  73. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/sql_adapter +0 -0
  74. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/templates/warning +0 -0
  75. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev/utils.py +0 -0
  76. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev.egg-info/SOURCES.txt +0 -0
  77. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  78. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev.egg-info/entry_points.txt +0 -0
  79. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev.egg-info/requires.txt +0 -0
  80. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/half_orm_dev.egg-info/top_level.txt +0 -0
  81. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/pyproject.toml +0 -0
  82. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/setup.cfg +0 -0
  83. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a31}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a30
3
+ Version: 1.0.0a31
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
@@ -18,7 +18,7 @@ from pathlib import Path
18
18
  from typing import List, Set, Tuple, Optional, TYPE_CHECKING
19
19
 
20
20
  from half_orm_dev.file_executor import (
21
- execute_sql_file, execute_python_file, FileExecutionError
21
+ execute_sql_file, execute_python_bootstrap, FileExecutionError
22
22
  )
23
23
 
24
24
  if TYPE_CHECKING:
@@ -180,7 +180,11 @@ class BootstrapManager:
180
180
  if file_path.suffix == '.sql':
181
181
  execute_sql_file(file_path, self._repo.database.model)
182
182
  elif file_path.suffix == '.py':
183
- output = execute_python_file(file_path, cwd=self._bootstrap_dir)
183
+ output = execute_python_bootstrap(
184
+ file_path,
185
+ model=self._repo.database.model,
186
+ cwd=self._bootstrap_dir
187
+ )
184
188
  if output:
185
189
  click.echo(f" Output: {output}")
186
190
  else:
@@ -5,6 +5,8 @@ This module provides common file execution functionality used by both
5
5
  PatchManager and BootstrapManager.
6
6
  """
7
7
 
8
+ import ast
9
+ import importlib.util
8
10
  import re
9
11
  import subprocess
10
12
  import sys
@@ -98,6 +100,72 @@ def execute_python_file(file_path: Path, cwd: Optional[Path] = None) -> str:
98
100
  raise FileExecutionError(f"Failed to execute Python file {file_path.name}: {e}") from e
99
101
 
100
102
 
103
+ def _has_run_entrypoint(file_path: Path) -> bool:
104
+ """Return True if the file defines a top-level run() function."""
105
+ try:
106
+ tree = ast.parse(file_path.read_text(encoding='utf-8'))
107
+ except (OSError, SyntaxError):
108
+ return False
109
+ return any(
110
+ isinstance(node, ast.FunctionDef) and node.name == 'run'
111
+ for node in tree.body
112
+ )
113
+
114
+
115
+ def execute_python_bootstrap(file_path: Path, model, cwd: Optional[Path] = None) -> str:
116
+ """
117
+ Execute a Python bootstrap script.
118
+
119
+ Fast path — if the script defines a top-level run(model) function it is
120
+ loaded in-process via importlib and called with the live database model,
121
+ sharing the existing connection.
122
+
123
+ Slow path — scripts without run(model) are executed as a subprocess
124
+ (backwards-compatible with pre-API scripts).
125
+
126
+ Args:
127
+ file_path: Path to Python bootstrap script
128
+ model: halfORM Model instance (shared database connection)
129
+ cwd: Working directory for execution (default: file's parent)
130
+
131
+ Returns:
132
+ Return value of run() converted to str, or subprocess stdout.
133
+ Empty string if run() returns None.
134
+
135
+ Raises:
136
+ FileExecutionError: If execution fails
137
+ """
138
+ if cwd is None:
139
+ cwd = file_path.parent
140
+
141
+ if not _has_run_entrypoint(file_path):
142
+ return execute_python_file(file_path, cwd)
143
+
144
+ module_name = f"_hop_bootstrap_{file_path.stem.replace('-', '_').replace('.', '_')}"
145
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
146
+ module = importlib.util.module_from_spec(spec)
147
+
148
+ cwd_str = str(cwd)
149
+ inserted = cwd_str not in sys.path
150
+ if inserted:
151
+ sys.path.insert(0, cwd_str)
152
+
153
+ try:
154
+ spec.loader.exec_module(module)
155
+ result = module.run(model)
156
+ return str(result) if result is not None else ''
157
+ except FileExecutionError:
158
+ raise
159
+ except Exception as e:
160
+ raise FileExecutionError(
161
+ f"Python execution failed in {file_path.name}: {e}"
162
+ ) from e
163
+ finally:
164
+ if inserted and cwd_str in sys.path:
165
+ sys.path.remove(cwd_str)
166
+ sys.modules.pop(module_name, None)
167
+
168
+
101
169
  _HOP_MARKER = re.compile(r"(--|#)\s*@hop:(bootstrap|data)")
102
170
 
103
171
 
@@ -101,15 +101,7 @@ class ReleaseManager:
101
101
  """
102
102
  schema_path = Path(self._base_dir) / ".hop" / "model" / "schema.sql"
103
103
 
104
- # Parse version from symlink
105
- version_from_file = self._parse_version_from_symlink(schema_path)
106
-
107
- # Optional validation against database
108
- version_from_db = self._repo.database.last_release_s
109
- if version_from_file != version_from_db:
110
- self._repo.restore_database_from_schema()
111
-
112
- return version_from_file
104
+ return self._parse_version_from_symlink(schema_path)
113
105
 
114
106
  def _parse_version_from_symlink(self, schema_path: Path) -> str:
115
107
  """
@@ -1138,11 +1130,8 @@ class ReleaseManager:
1138
1130
  """
1139
1131
  Fetch tags and list available releases for production upgrade (read-only).
1140
1132
 
1141
- Equivalent to 'apt update' - synchronizes with origin and shows available
1142
- releases but makes NO modifications to database or repository.
1143
-
1144
1133
  Workflow:
1145
- 1. Fetch tags from origin (git fetch --tags)
1134
+ 1. Fetch all refs from origin (git fetch --prune)
1146
1135
  2. Read current production version from database (hop_last_release)
1147
1136
  3. List available release tags (v1.3.6, v1.3.6-rc1, v1.4.0)
1148
1137
  4. Calculate sequential upgrade path
@@ -1181,7 +1170,11 @@ class ReleaseManager:
1181
1170
  """
1182
1171
  allow_rc = self._repo.allow_rc
1183
1172
 
1184
- # 1. Get available release tags from origin
1173
+ # 1. Sync with remote then read available release tags
1174
+ try:
1175
+ self._repo.hgit.fetch_from_origin()
1176
+ except Exception as e:
1177
+ raise ReleaseManagerError(f"Failed to fetch from origin: {e}")
1185
1178
  available_tags = self._get_available_release_tags(allow_rc=allow_rc)
1186
1179
 
1187
1180
  # 2. Read current production version from database
@@ -1248,7 +1241,9 @@ class ReleaseManager:
1248
1241
  if production_versions:
1249
1242
  # Use last production version as target
1250
1243
  target_version = production_versions[-1]
1251
- upgrade_path = self._calculate_upgrade_path(current_version, target_version)
1244
+ upgrade_path = self._calculate_upgrade_path(
1245
+ current_version, target_version, available_tags=available_tags
1246
+ )
1252
1247
 
1253
1248
  # 5. Return results
1254
1249
  return {
@@ -1260,10 +1255,11 @@ class ReleaseManager:
1260
1255
 
1261
1256
  def _get_available_release_tags(self, allow_rc: bool = False) -> List[str]:
1262
1257
  """
1263
- Get available release tags from Git repository.
1258
+ Get available release tags from local Git repository.
1264
1259
 
1265
- Fetches tags from origin and filters for release tags (v*.*.*).
1260
+ Filters local tags for release tags (v*.*.*).
1266
1261
  Excludes RC tags unless allow_rc=True.
1262
+ Caller is responsible for fetching from origin before calling this.
1267
1263
 
1268
1264
  Args:
1269
1265
  allow_rc: If True, include RC tags (v1.3.6-rc1)
@@ -1283,12 +1279,6 @@ class ReleaseManager:
1283
1279
  tags = mgr._get_available_release_tags(allow_rc=True)
1284
1280
  # → ["v1.3.6-rc1", "v1.3.6", "v1.4.0"]
1285
1281
  """
1286
- try:
1287
- # Fetch tags from origin
1288
- self._repo.hgit.fetch_tags()
1289
- except Exception as e:
1290
- raise ReleaseManagerError(f"Failed to fetch tags from origin: {e}")
1291
-
1292
1282
  # Get all tags from repository
1293
1283
  try:
1294
1284
  all_tags = self._repo.hgit._HGit__git_repo.tags
@@ -1314,7 +1304,8 @@ class ReleaseManager:
1314
1304
  def _calculate_upgrade_path(
1315
1305
  self,
1316
1306
  current: str,
1317
- target: str
1307
+ target: str,
1308
+ available_tags: Optional[List[str]] = None,
1318
1309
  ) -> List[str]:
1319
1310
  """
1320
1311
  Calculate sequential upgrade path between two versions.
@@ -1348,7 +1339,8 @@ class ReleaseManager:
1348
1339
  if current == target:
1349
1340
  return []
1350
1341
 
1351
- available_tags = self._get_available_release_tags(allow_rc=False)
1342
+ if available_tags is None:
1343
+ available_tags = self._get_available_release_tags(allow_rc=False)
1352
1344
 
1353
1345
  available_versions = []
1354
1346
  for tag in available_tags:
@@ -1386,10 +1378,10 @@ class ReleaseManager:
1386
1378
  It does NOT use restore_database_from_schema() which would destroy data.
1387
1379
 
1388
1380
  Workflow:
1389
- 1. CREATE BACKUP (first action, before any validation)
1381
+ 1. Fetch available releases via update_production()
1390
1382
  2. Validate production environment (ho-prod branch, clean repo)
1391
- 3. Fetch available releases via update_production()
1392
- 4. Calculate upgrade path (all or to specific version)
1383
+ 3. Calculate upgrade path (all or to specific version)
1384
+ 4. CREATE BACKUP (last action before any destructive operation)
1393
1385
  5. Apply each release sequentially on existing database
1394
1386
  6. Update database version after each release
1395
1387
 
@@ -1488,36 +1480,10 @@ class ReleaseManager:
1488
1480
  'message': 'Production already at latest version'
1489
1481
  }
1490
1482
 
1491
- # === 2. SNAPSHOT OR BACKUP (unless dry_run or skip_backup) ===
1492
- # Preferred: instant snapshot via CREATE DATABASE ... TEMPLATE (requires CREATEDB).
1493
- # Fallback: full pg_dump.
1494
- # Connections are terminated before the snapshot — this is intentional:
1495
- # we want no application traffic during the schema migration anyway.
1496
- snapshot_name = None
1497
- backup_path = None
1498
- if not dry_run and not skip_backup:
1499
- db = self._repo.database
1500
- version_slug = current_version.replace('.', '_').replace('-', '_')
1501
- snap_name = f"{db.name}_hop_snap_{version_slug}"
1502
-
1503
- if db.has_createdb_privilege():
1504
- if force_backup:
1505
- db.drop_snapshot(snap_name)
1506
- db.terminate_active_connections()
1507
- db.create_snapshot(snap_name)
1508
- snapshot_name = snap_name
1509
- # Our psycopg connection was terminated above — reconnect.
1510
- db._Database__model.reconnect(reload=True)
1511
- else:
1512
- backup_path = self._create_production_backup(
1513
- current_version,
1514
- force=force_backup
1515
- )
1516
-
1517
- # === 3. Validate environment ===
1483
+ # === 2. Validate environment ===
1518
1484
  self._validate_production_upgrade()
1519
1485
 
1520
- # === 4. Calculate upgrade path ===
1486
+ # === 3. Calculate upgrade path ===
1521
1487
  if to_version:
1522
1488
  # Upgrade to specific version
1523
1489
  full_path = update_info['upgrade_path']
@@ -1558,19 +1524,48 @@ class ReleaseManager:
1558
1524
  'final_version': upgrade_path[-1] if upgrade_path else current_version
1559
1525
  }
1560
1526
 
1561
- # === 5. Fetch from remote (read-only) and apply releases ===
1562
- git_repo = self._repo.hgit._HGit__git_repo
1563
- try:
1564
- self._repo.hgit.fetch_tags()
1565
- git_repo.remotes.origin.fetch(prune=True)
1566
- except Exception as e:
1567
- raise ReleaseManagerError(f"Failed to fetch from origin: {e}")
1527
+ # === 4. SNAPSHOT OR BACKUP (last step before any destructive operation) ===
1528
+ # Preferred: instant snapshot via CREATE DATABASE ... TEMPLATE (requires CREATEDB).
1529
+ # Fallback: full pg_dump.
1530
+ # Connections are terminated before the snapshot — this is intentional:
1531
+ # we want no application traffic during the schema migration anyway.
1532
+ snapshot_name = None
1533
+ backup_path = None
1534
+ if not skip_backup:
1535
+ db = self._repo.database
1536
+ version_slug = current_version.replace('.', '_').replace('-', '_')
1537
+ snap_name = f"{db.name}_hop_snap_{version_slug}"
1538
+
1539
+ if db.has_createdb_privilege():
1540
+ if force_backup:
1541
+ db.drop_snapshot(snap_name)
1542
+ db.terminate_active_connections()
1543
+ db.create_snapshot(snap_name)
1544
+ snapshot_name = snap_name
1545
+ # Our psycopg connection was terminated above — reconnect.
1546
+ db._Database__model.reconnect(reload=True)
1547
+ else:
1548
+ backup_path = self._create_production_backup(
1549
+ current_version,
1550
+ force=force_backup
1551
+ )
1568
1552
 
1553
+ # === 5. Apply releases ===
1554
+ git_repo = self._repo.hgit._HGit__git_repo
1569
1555
  patches_applied = {}
1570
1556
  try:
1571
1557
  for version in upgrade_path:
1572
- # Checkout immutable ho-prod-X.Y.Z branch (created at promote time)
1558
+ # Checkout immutable ho-prod-X.Y.Z branch (created at promote time).
1559
+ # Fetch explicitly: production servers cloned with --single-branch only
1560
+ # track ho-prod, so fetch_from_origin() won't fetch ho-prod-X.Y.Z.
1573
1561
  prod_branch = f"ho-prod-{version}"
1562
+ try:
1563
+ git_repo.git.fetch(
1564
+ 'origin',
1565
+ f'{prod_branch}:refs/remotes/origin/{prod_branch}'
1566
+ )
1567
+ except Exception:
1568
+ pass # already present locally or will fail at checkout below
1574
1569
  local_heads = {h.name: h for h in git_repo.heads}
1575
1570
  if prod_branch in local_heads:
1576
1571
  local_heads[prod_branch].checkout()
@@ -1736,13 +1731,18 @@ class ReleaseManager:
1736
1731
  mgr._validate_production_upgrade()
1737
1732
  # → Raises: "Repository has uncommitted changes"
1738
1733
  """
1739
- # Check branch: must be on a ho-prod-X.Y.Z branch
1734
+ # Check branch: must be on a production branch (ho-prod, ho-prod-X.Y.Z, or ho-current)
1740
1735
  current_branch = self._repo.hgit.branch
1741
- if not current_branch.startswith("ho-prod-"):
1736
+ valid = (
1737
+ current_branch == "ho-prod"
1738
+ or current_branch == "ho-current"
1739
+ or current_branch.startswith("ho-prod-")
1740
+ )
1741
+ if not valid:
1742
1742
  raise ReleaseManagerError(
1743
- f"Must be on a ho-prod-X.Y.Z branch for production upgrade.\n"
1744
- f"Current branch: {current_branch}\n"
1745
- f"Run 'hop upgrade' from a ho-prod-X.Y.Z branch."
1743
+ f"Must be on a production branch (ho-prod, ho-prod-X.Y.Z, or ho-current) "
1744
+ f"for production upgrade.\n"
1745
+ f"Current branch: {current_branch}"
1746
1746
  )
1747
1747
 
1748
1748
  # Check repo is clean
@@ -0,0 +1 @@
1
+ 1.0.0-a31
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a30
3
+ Version: 1.0.0a31
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-a30
File without changes
File without changes