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.
- {half_orm_dev-1.0.0a30/half_orm_dev.egg-info → half_orm_dev-1.0.0a32}/PKG-INFO +22 -18
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/README.md +19 -17
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/__init__.py +0 -3
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/check.py +1 -1
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/recover.py +1 -1
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/main.py +1 -1
- half_orm_dev-1.0.0a32/half_orm_dev/file_executor.py +221 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/hgit.py +43 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/migration_manager.py +1 -0
- {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
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patch_manager.py +231 -180
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/release_file.py +3 -4
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/release_manager.py +106 -139
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/repo.py +157 -114
- half_orm_dev-1.0.0a32/half_orm_dev/version.txt +1 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32/half_orm_dev.egg-info}/PKG-INFO +22 -18
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/SOURCES.txt +0 -3
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/requires.txt +2 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/setup.py +2 -0
- half_orm_dev-1.0.0a30/half_orm_dev/bootstrap_manager.py +0 -388
- half_orm_dev-1.0.0a30/half_orm_dev/cli/commands/bootstrap.py +0 -139
- half_orm_dev-1.0.0a30/half_orm_dev/file_executor.py +0 -151
- half_orm_dev-1.0.0a30/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -103
- half_orm_dev-1.0.0a30/half_orm_dev/version.txt +0 -1
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/AUTHORS +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/LICENSE +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/__init__.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/__init__.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/apply.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/clone.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/init.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/migrate.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/patch.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/release.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/restore.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/revert_migration.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/rollback.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/sync.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/todo.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/undo.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli/commands/upgrade.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/cli_extension.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/database.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/decorators.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/modules.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patch_validator.py +0 -0
- {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
- {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
- {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
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/log +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/py.typed +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/scripts/repair-metadata.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/.gitignore +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/MANIFEST.in +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/README +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/conftest_template +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/pre-push +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/init_module_template +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_1 +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_2 +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/module_template_3 +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/pyproject.toml +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/relation_test +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/sql_adapter +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/templates/warning +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev/utils.py +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/dependency_links.txt +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/entry_points.txt +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/half_orm_dev.egg-info/top_level.txt +0 -0
- {half_orm_dev-1.0.0a30 → half_orm_dev-1.0.0a32}/pyproject.toml +0 -0
- {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.
|
|
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
|
|
389
|
+
### Bootstrap - Data Initialization
|
|
388
390
|
|
|
389
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
|
356
|
+
### Bootstrap - Data Initialization
|
|
357
357
|
|
|
358
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
373
|
-
-
|
|
374
|
-
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
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.
|
|
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', '
|
|
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
|
|