half-orm-dev 1.0.0a30__tar.gz → 1.0.0a32__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 (84) hide show
  1. {half_orm_dev-1.0.0a30/half_orm_dev.egg-info → half_orm_dev-1.0.0a32}/PKG-INFO +22 -18
  2. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/README.md +19 -17
  3. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/__init__.py +0 -3
  4. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/check.py +1 -1
  5. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/recover.py +1 -1
  6. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/main.py +1 -1
  7. half_orm_dev-1.0.0a32/half_orm_dev/file_executor.py +221 -0
  8. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/hgit.py +43 -0
  9. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migration_manager.py +1 -0
  10. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +3 -3
  11. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patch_manager.py +231 -180
  12. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/release_file.py +3 -4
  13. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/release_manager.py +106 -139
  14. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/repo.py +157 -114
  15. half_orm_dev-1.0.0a32/half_orm_dev/version.txt +1 -0
  16. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32/half_orm_dev.egg-info}/PKG-INFO +22 -18
  17. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/SOURCES.txt +0 -3
  18. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/requires.txt +2 -0
  19. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/setup.py +2 -0
  20. half_orm_dev-1.0.0a30/half_orm_dev/bootstrap_manager.py +0 -388
  21. half_orm_dev-1.0.0a30/half_orm_dev/cli/commands/bootstrap.py +0 -139
  22. half_orm_dev-1.0.0a30/half_orm_dev/file_executor.py +0 -151
  23. half_orm_dev-1.0.0a30/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -103
  24. half_orm_dev-1.0.0a30/half_orm_dev/version.txt +0 -1
  25. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/AUTHORS +0 -0
  26. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/LICENSE +0 -0
  27. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/__init__.py +0 -0
  28. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/__init__.py +0 -0
  29. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/apply.py +0 -0
  30. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/clone.py +0 -0
  31. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/init.py +0 -0
  32. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/migrate.py +0 -0
  33. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/patch.py +0 -0
  34. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/release.py +0 -0
  35. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/restore.py +0 -0
  36. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  37. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/rollback.py +0 -0
  38. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  39. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/sync.py +0 -0
  40. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/todo.py +0 -0
  41. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/undo.py +0 -0
  42. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/upgrade.py +0 -0
  43. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli_extension.py +0 -0
  44. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/database.py +0 -0
  45. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/decorators.py +0 -0
  46. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  47. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  48. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  49. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  50. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  51. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  52. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +0 -0
  53. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  54. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/modules.py +0 -0
  55. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patch_validator.py +0 -0
  56. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  57. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  58. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  59. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/log +0 -0
  60. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  61. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/py.typed +0 -0
  62. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/scripts/repair-metadata.py +0 -0
  63. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/.gitignore +0 -0
  64. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/MANIFEST.in +0 -0
  65. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/README +0 -0
  66. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/conftest_template +0 -0
  67. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  68. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/pre-push +0 -0
  69. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  70. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
  71. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/init_module_template +0 -0
  72. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_1 +0 -0
  73. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_2 +0 -0
  74. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_3 +0 -0
  75. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/pyproject.toml +0 -0
  76. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/relation_test +0 -0
  77. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/sql_adapter +0 -0
  78. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/warning +0 -0
  79. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/utils.py +0 -0
  80. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  81. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/entry_points.txt +0 -0
  82. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/top_level.txt +0 -0
  83. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/pyproject.toml +0 -0
  84. {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/setup.cfg +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.0a32
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
@@ -21,8 +21,10 @@ License-File: LICENSE
21
21
  License-File: AUTHORS
22
22
  Requires-Dist: GitPython
23
23
  Requires-Dist: click
24
+ Requires-Dist: packaging
24
25
  Requires-Dist: pydash
25
26
  Requires-Dist: pytest
27
+ Requires-Dist: pytest-asyncio
26
28
  Requires-Dist: half_orm<1.1.0,>=1.0.0a1
27
29
  Requires-Dist: tomli>=2.0.0; python_version < "3.11"
28
30
  Requires-Dist: tomli_w>=1.0.0
@@ -384,29 +386,31 @@ half_orm dev upgrade [--to-release X.Y.Z]
384
386
  half_orm dev upgrade --dry-run
385
387
  ```
386
388
 
387
- ### Data Bootstrap
389
+ ### Bootstrap - Data Initialization
388
390
 
389
- Mark patch files with `-- @HOP:bootstrap` (SQL) or `# @HOP:bootstrap` (Python) to declare reference data.
390
- The marker **must be on the first line** of the file:
391
+ Bootstrap scripts initialize application data on empty databases. Place SQL and Python files in the `bootstrap/` directory:
391
392
 
392
- ```sql
393
- -- @HOP:bootstrap
394
- INSERT INTO roles (name) VALUES ('admin'), ('user') ON CONFLICT DO NOTHING;
395
393
  ```
396
-
397
- ```python
398
- # @HOP:bootstrap
399
- # (no shebang — the file is executed directly by half-orm-dev)
400
- MyModel(field='value').ho_insert()
394
+ bootstrap/
395
+ ├── 01-init-roles.sql
396
+ ├── 02-seed-config.py
397
+ └── 03-reference-data.sql
401
398
  ```
402
399
 
403
- These files are automatically:
404
- - Copied to `bootstrap/` during `patch merge`
405
- - Executed during production `upgrade`
406
- - Tracked in database (each script runs once)
400
+ Files are executed **alphabetically** during:
401
+ - **Development**: Each `patch apply` (allows iteration on bootstrap scripts)
402
+ - **Production**: Initial `clone` only (one-time initialization)
403
+
404
+ For production data changes, use **patches** (not bootstrap).
407
405
 
408
- > **Note:** If the marker is not on the first line it is silently ignored.
409
- > A warning is displayed during `patch apply` to help catch this mistake.
406
+ **Python files** can define a `run(model)` function to share the database connection:
407
+
408
+ ```python
409
+ def run(model):
410
+ # model is the halfORM Model instance with active connection
411
+ MyModel = model.get_relation_class('schema.table')
412
+ MyModel(field='value').ho_insert()
413
+ ```
410
414
 
411
415
  **Note:** Use `half_orm dev <command> --help` for detailed help on each command.
412
416
 
@@ -353,29 +353,31 @@ half_orm dev upgrade [--to-release X.Y.Z]
353
353
  half_orm dev upgrade --dry-run
354
354
  ```
355
355
 
356
- ### Data Bootstrap
356
+ ### Bootstrap - Data Initialization
357
357
 
358
- Mark patch files with `-- @HOP:bootstrap` (SQL) or `# @HOP:bootstrap` (Python) to declare reference data.
359
- The marker **must be on the first line** of the file:
358
+ Bootstrap scripts initialize application data on empty databases. Place SQL and Python files in the `bootstrap/` directory:
360
359
 
361
- ```sql
362
- -- @HOP:bootstrap
363
- INSERT INTO roles (name) VALUES ('admin'), ('user') ON CONFLICT DO NOTHING;
364
360
  ```
365
-
366
- ```python
367
- # @HOP:bootstrap
368
- # (no shebang — the file is executed directly by half-orm-dev)
369
- MyModel(field='value').ho_insert()
361
+ bootstrap/
362
+ ├── 01-init-roles.sql
363
+ ├── 02-seed-config.py
364
+ └── 03-reference-data.sql
370
365
  ```
371
366
 
372
- These files are automatically:
373
- - Copied to `bootstrap/` during `patch merge`
374
- - Executed during production `upgrade`
375
- - Tracked in database (each script runs once)
367
+ Files are executed **alphabetically** during:
368
+ - **Development**: Each `patch apply` (allows iteration on bootstrap scripts)
369
+ - **Production**: Initial `clone` only (one-time initialization)
370
+
371
+ For production data changes, use **patches** (not bootstrap).
376
372
 
377
- > **Note:** If the marker is not on the first line it is silently ignored.
378
- > A warning is displayed during `patch apply` to help catch this mistake.
373
+ **Python files** can define a `run(model)` function to share the database connection:
374
+
375
+ ```python
376
+ def run(model):
377
+ # model is the halfORM Model instance with active connection
378
+ MyModel = model.get_relation_class('schema.table')
379
+ MyModel(field='value').ho_insert()
380
+ ```
379
381
 
380
382
  **Note:** Use `half_orm dev <command> --help` for detailed help on each command.
381
383
 
@@ -15,7 +15,6 @@ from .check import check
15
15
  from .set_git_origin import set_git_origin
16
16
  from .migrate import migrate
17
17
  from .revert_migration import revert_migration
18
- from .bootstrap import bootstrap
19
18
  from .rollback import rollback
20
19
  from .recover import recover
21
20
  from .todo import apply_release
@@ -36,7 +35,6 @@ ALL_COMMANDS = {
36
35
  'set-git-origin': set_git_origin, # Update git remote origin URL
37
36
  'migrate': migrate, # Repository migration after upgrade
38
37
  'revert-migration': revert_migration, # Revert last migration
39
- 'bootstrap': bootstrap, # Execute data initialization scripts
40
38
  # 🚧 (stubs)
41
39
  'apply_release': apply_release,
42
40
 
@@ -58,7 +56,6 @@ __all__ = [
58
56
  'upgrade',
59
57
  'check',
60
58
  'migrate',
61
- 'bootstrap',
62
59
  'rollback',
63
60
  'recover',
64
61
  # Adapted commands
@@ -162,7 +162,7 @@ def _display_check_results(repo, result: dict, dry_run: bool, verbose: bool):
162
162
  click.echo(f"\n🔧 {utils.Color.bold('Orphaned patches')} ({len(orphaned_patches)}):")
163
163
  for patch_id in sorted(orphaned_patches):
164
164
  click.echo(f" • {patch_id}")
165
- click.echo(f" {utils.Color.blue('(Use \"half_orm dev release attach-patch <id>\" to reattach)')}")
165
+ click.echo(f""" {utils.Color.blue('(Use "half_orm dev release attach-patch <id>" to reattach)')}""")
166
166
 
167
167
  # Show standalone patch branches (not in candidates/stage)
168
168
  standalone_patches = [b for b in patch_branches
@@ -41,6 +41,6 @@ def recover() -> None:
41
41
 
42
42
  if result['errors']:
43
43
  for error in result['errors']:
44
- click.echo(utils.Color.yellow(f"Warning: {error}"), err=True)
44
+ click.echo(utils.Color.bold(f"Warning: {error}"), err=True)
45
45
 
46
46
  click.echo(utils.Color.green("Recovery complete."))
@@ -60,7 +60,7 @@ class Hop:
60
60
  return ['sync-package', 'check']
61
61
 
62
62
  # DEVELOPMENT ENVIRONMENT - Patch development
63
- return ['patch', 'release', 'check', 'bootstrap', 'set-git-origin',
63
+ return ['patch', 'release', 'check', 'set-git-origin',
64
64
  'revert-migration', 'recover']
65
65
 
66
66
  @property
@@ -0,0 +1,221 @@
1
+ """
2
+ Shared utilities for executing SQL and Python files.
3
+
4
+ This module provides common file execution functionality for patch application
5
+ and bootstrap initialization.
6
+ """
7
+
8
+ import ast
9
+ import importlib.util
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ class FileExecutionError(Exception):
17
+ """Raised when file execution fails."""
18
+ pass
19
+
20
+
21
+ def execute_sql_file(file_path: Path, database_model) -> None:
22
+ """
23
+ Execute SQL file against database using halfORM Model.
24
+
25
+ Args:
26
+ file_path: Path to SQL file
27
+ database_model: halfORM Model instance
28
+
29
+ Raises:
30
+ FileExecutionError: If SQL execution fails
31
+ """
32
+ try:
33
+ sql_content = file_path.read_text(encoding='utf-8')
34
+
35
+ # Skip empty files
36
+ if not sql_content.strip():
37
+ return
38
+
39
+ database_model.execute_query(sql_content)
40
+
41
+ except Exception as e:
42
+ raise FileExecutionError(f"SQL execution failed in {file_path.name}: {e}") from e
43
+
44
+
45
+ def execute_sql_file_psql(file_path: Path, database, database_name: str) -> None:
46
+ """
47
+ Execute SQL file using psql command.
48
+
49
+ Uses psql directly instead of halfORM Model, useful for files that
50
+ may contain transaction control or other psql-specific features.
51
+
52
+ Args:
53
+ file_path: Path to SQL file
54
+ database: Database instance with execute_pg_command method
55
+ database_name: Name of the database to connect to
56
+
57
+ Raises:
58
+ FileExecutionError: If psql execution fails
59
+ """
60
+ try:
61
+ database.execute_pg_command('psql', '-d', database_name, '-f', str(file_path))
62
+ except Exception as e:
63
+ raise FileExecutionError(f"psql execution failed for {file_path.name}: {e}") from e
64
+
65
+
66
+ def execute_python_file(file_path: Path, cwd: Optional[Path] = None) -> str:
67
+ """
68
+ Execute Python script as subprocess.
69
+
70
+ Args:
71
+ file_path: Path to Python file
72
+ cwd: Working directory for execution (default: file's parent directory)
73
+
74
+ Returns:
75
+ stdout from the script execution
76
+
77
+ Raises:
78
+ FileExecutionError: If Python execution fails
79
+ """
80
+ if cwd is None:
81
+ cwd = file_path.parent
82
+
83
+ try:
84
+ result = subprocess.run(
85
+ [sys.executable, str(file_path)],
86
+ cwd=cwd,
87
+ capture_output=True,
88
+ text=True,
89
+ check=True
90
+ )
91
+ return result.stdout.strip()
92
+
93
+ except subprocess.CalledProcessError as e:
94
+ error_msg = f"Python execution failed in {file_path.name}"
95
+ if e.stderr:
96
+ error_msg += f": {e.stderr.strip()}"
97
+ raise FileExecutionError(error_msg) from e
98
+ except Exception as e:
99
+ raise FileExecutionError(f"Failed to execute Python file {file_path.name}: {e}") from e
100
+
101
+
102
+ def _has_run_entrypoint(file_path: Path) -> bool:
103
+ """Return True if the file defines a top-level run() function."""
104
+ try:
105
+ tree = ast.parse(file_path.read_text(encoding='utf-8'))
106
+ except (OSError, SyntaxError):
107
+ return False
108
+ return any(
109
+ isinstance(node, ast.FunctionDef) and node.name == 'run'
110
+ for node in tree.body
111
+ )
112
+
113
+
114
+ def execute_python_bootstrap(file_path: Path, model, cwd: Optional[Path] = None) -> str:
115
+ """
116
+ Execute a Python bootstrap script.
117
+
118
+ Fast path — if the script defines a top-level run(model) function it is
119
+ loaded in-process via importlib and called with the live database model,
120
+ sharing the existing connection.
121
+
122
+ Slow path — scripts without run(model) are executed as a subprocess
123
+ (backwards-compatible with pre-API scripts).
124
+
125
+ Args:
126
+ file_path: Path to Python bootstrap script
127
+ model: halfORM Model instance (shared database connection)
128
+ cwd: Working directory for execution (default: file's parent)
129
+
130
+ Returns:
131
+ Return value of run() converted to str, or subprocess stdout.
132
+ Empty string if run() returns None.
133
+
134
+ Raises:
135
+ FileExecutionError: If execution fails
136
+ """
137
+ if cwd is None:
138
+ cwd = file_path.parent
139
+
140
+ if not _has_run_entrypoint(file_path):
141
+ return execute_python_file(file_path, cwd)
142
+
143
+ module_name = f"_hop_bootstrap_{file_path.stem.replace('-', '_').replace('.', '_')}"
144
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
145
+ module = importlib.util.module_from_spec(spec)
146
+
147
+ cwd_str = str(cwd)
148
+ inserted = cwd_str not in sys.path
149
+ if inserted:
150
+ sys.path.insert(0, cwd_str)
151
+
152
+ try:
153
+ spec.loader.exec_module(module)
154
+ result = module.run(model)
155
+ return str(result) if result is not None else ''
156
+ except FileExecutionError:
157
+ raise
158
+ except Exception as e:
159
+ raise FileExecutionError(
160
+ f"Python execution failed in {file_path.name}: {e}"
161
+ ) from e
162
+ finally:
163
+ if inserted and cwd_str in sys.path:
164
+ sys.path.remove(cwd_str)
165
+ sys.modules.pop(module_name, None)
166
+
167
+
168
+ def execute_bootstrap_files(bootstrap_dir: Path, model) -> None:
169
+ """
170
+ Execute all bootstrap files in alphabetic order.
171
+
172
+ Bootstrap files are SQL and Python files in the bootstrap/ directory that
173
+ initialize application data on empty databases. They are executed in
174
+ alphabetic order (no numeric parsing needed).
175
+
176
+ Args:
177
+ bootstrap_dir: Path to bootstrap directory
178
+ model: halfORM Model instance (shared database connection)
179
+
180
+ Raises:
181
+ FileExecutionError: If any file execution fails
182
+
183
+ Example:
184
+ bootstrap_dir = Path('/path/to/project/bootstrap')
185
+ execute_bootstrap_files(bootstrap_dir, model)
186
+
187
+ # Executes files in order:
188
+ # - 01-init-users.sql
189
+ # - 02-seed-config.py
190
+ # - 03-reference-data.sql
191
+ """
192
+ if not bootstrap_dir.exists():
193
+ return
194
+
195
+ # Collect all SQL and Python files
196
+ files = []
197
+ for file_path in bootstrap_dir.iterdir():
198
+ if file_path.is_file() and file_path.suffix in ('.sql', '.py'):
199
+ files.append(file_path)
200
+
201
+ if not files:
202
+ return
203
+
204
+ # Sort alphabetically by filename
205
+ files.sort(key=lambda f: f.name)
206
+
207
+ # Execute each file
208
+ for file_path in files:
209
+ try:
210
+ if file_path.suffix == '.sql':
211
+ execute_sql_file(file_path, model)
212
+ elif file_path.suffix == '.py':
213
+ execute_python_bootstrap(file_path, model, cwd=bootstrap_dir)
214
+ except FileExecutionError:
215
+ # Re-raise FileExecutionError as-is (already has good error message)
216
+ raise
217
+ except Exception as e:
218
+ # Wrap unexpected errors
219
+ raise FileExecutionError(
220
+ f"Failed to execute bootstrap file {file_path.name}: {e}"
221
+ ) from e
@@ -560,6 +560,49 @@ class HGit:
560
560
  finally:
561
561
  marker.unlink(missing_ok=True)
562
562
 
563
+ def setup_production_branches(self) -> None:
564
+ """
565
+ Create local tracking branches for all remote ho-prod-* branches.
566
+
567
+ This ensures production servers have local access to all versioned
568
+ production branches (ho-prod, ho-prod-X.Y.Z) for rollback support.
569
+
570
+ Workflow:
571
+ 1. List all remote branches matching origin/ho-prod*
572
+ 2. For each remote branch, create local tracking branch if missing
573
+ 3. Skip if local branch already exists
574
+
575
+ Used in:
576
+ - Production clone: after initial checkout
577
+ - Production upgrade: after fetch to get new releases
578
+
579
+ Examples:
580
+ # After clone or fetch
581
+ hgit.setup_production_branches()
582
+ # → Creates ho-prod-0.1.0, ho-prod-0.1.1, etc. from origin
583
+ """
584
+ # Get all remote branches matching ho-prod*
585
+ remote_branches = []
586
+ for ref in self.__git_repo.remote('origin').refs:
587
+ branch_name = ref.name.replace('origin/', '')
588
+ if branch_name.startswith('ho-prod'):
589
+ remote_branches.append(branch_name)
590
+
591
+ # Create local tracking branches for each remote ho-prod* branch
592
+ for branch_name in remote_branches:
593
+ try:
594
+ # Check if local branch already exists
595
+ if branch_name in [b.name for b in self.__git_repo.branches]:
596
+ continue
597
+
598
+ # Create local tracking branch
599
+ remote_ref = self.__git_repo.remote('origin').refs[branch_name]
600
+ self.__git_repo.create_head(branch_name, remote_ref)
601
+
602
+ except Exception:
603
+ # Skip branches that can't be created (shouldn't happen)
604
+ pass
605
+
563
606
  def delete_local_branch(self, branch_name: str) -> None:
564
607
  """
565
608
  Delete local branch.
@@ -543,6 +543,7 @@ class MigrationManager:
543
543
  else:
544
544
  repo.restore_database_from_schema(skip_bootstrap=True)
545
545
  else:
546
+ #XXX EST-CE QUE POUR ho-patch/* on ne devrait pas utiliser from_release_schema ?
546
547
  # ho-prod and ho-patch/*: use production schema
547
548
  repo.restore_database_from_schema(skip_bootstrap=True)
548
549
 
@@ -18,10 +18,10 @@ from pathlib import Path
18
18
  import subprocess
19
19
  import sys
20
20
 
21
- try:
21
+ if sys.version_info >= (3, 11):
22
+ import tomllib as tomli
23
+ else:
22
24
  import tomli
23
- except ImportError:
24
- import tomllib as tomli
25
25
 
26
26
  try:
27
27
  import tomli_w