half-orm-dev 1.0.0a23__tar.gz → 1.0.0a25__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.0a23/half_orm_dev.egg-info → half_orm_dev-1.0.0a25}/PKG-INFO +1 -1
  2. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/release.py +4 -0
  3. half_orm_dev-1.0.0a25/half_orm_dev/cli/commands/upgrade.py +198 -0
  4. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/database.py +1 -1
  5. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migration_manager.py +6 -0
  6. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/modules.py +32 -22
  7. half_orm_dev-1.0.0a25/half_orm_dev/py.typed +0 -0
  8. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/release_manager.py +24 -21
  9. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/module_template_1 +2 -0
  10. half_orm_dev-1.0.0a25/half_orm_dev/version.txt +1 -0
  11. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25/half_orm_dev.egg-info}/PKG-INFO +1 -1
  12. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev.egg-info/SOURCES.txt +1 -0
  13. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/pyproject.toml +1 -0
  14. half_orm_dev-1.0.0a23/half_orm_dev/cli/commands/upgrade.py +0 -191
  15. half_orm_dev-1.0.0a23/half_orm_dev/version.txt +0 -1
  16. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/AUTHORS +0 -0
  17. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/LICENSE +0 -0
  18. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/README.md +0 -0
  19. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/__init__.py +0 -0
  20. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/bootstrap_manager.py +0 -0
  21. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/__init__.py +0 -0
  22. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/__init__.py +0 -0
  23. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/apply.py +0 -0
  24. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/bootstrap.py +0 -0
  25. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/check.py +0 -0
  26. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/clone.py +0 -0
  27. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/init.py +0 -0
  28. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/migrate.py +0 -0
  29. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/patch.py +0 -0
  30. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/restore.py +0 -0
  31. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/revert_migration.py +0 -0
  32. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/set_git_origin.py +0 -0
  33. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/sync.py +0 -0
  34. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/todo.py +0 -0
  35. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/undo.py +0 -0
  36. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/commands/update.py +0 -0
  37. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli/main.py +0 -0
  38. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/cli_extension.py +0 -0
  39. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/decorators.py +0 -0
  40. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/file_executor.py +0 -0
  41. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/hgit.py +0 -0
  42. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/1/00_move_to_hop.py +0 -0
  43. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/1/01_txt_to_toml.py +0 -0
  44. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/4/00_toml_dict_format.py +0 -0
  45. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/4/01_add_bootstrap_table.py +0 -0
  46. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/4/02_move_patches_to_subdirs.py +0 -0
  47. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/17/5/01_update_pyproject_dependency.py +0 -0
  48. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/18/0/00_add_async_support.py +0 -0
  49. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/0/18/0/01_update_default_tests.py +0 -0
  50. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/1/0/0/a20/01_update_gitignore.py +0 -0
  51. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/migrations/hop/BREAKING_CHANGES-1.0.0.md +0 -0
  52. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patch_manager.py +0 -0
  53. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patch_validator.py +0 -0
  54. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +0 -0
  55. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +0 -0
  56. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +0 -0
  57. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patches/log +0 -0
  58. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/patches/sql/half_orm_meta.sql +0 -0
  59. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/release_file.py +0 -0
  60. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/repo.py +0 -0
  61. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/scripts/repair-metadata.py +0 -0
  62. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/.gitignore +0 -0
  63. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/MANIFEST.in +0 -0
  64. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/README +0 -0
  65. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/conftest_template +0 -0
  66. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/git-hooks/pre-commit +0 -0
  67. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/git-hooks/pre-push +0 -0
  68. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/git-hooks/prepare-commit-msg +0 -0
  69. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/git-hooks/reference-transaction +0 -0
  70. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/init_module_template +0 -0
  71. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/module_template_2 +0 -0
  72. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/module_template_3 +0 -0
  73. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/pyproject.toml +0 -0
  74. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/relation_test +0 -0
  75. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/sql_adapter +0 -0
  76. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/templates/warning +0 -0
  77. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev/utils.py +0 -0
  78. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev.egg-info/dependency_links.txt +0 -0
  79. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev.egg-info/entry_points.txt +0 -0
  80. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev.egg-info/requires.txt +0 -0
  81. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/half_orm_dev.egg-info/top_level.txt +0 -0
  82. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/setup.cfg +0 -0
  83. {half_orm_dev-1.0.0a23 → half_orm_dev-1.0.0a25}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a23
3
+ Version: 1.0.0a25
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
@@ -526,6 +526,10 @@ def release_attach_patch(patch_id: str, force: bool) -> None:
526
526
 
527
527
  version = current_branch.replace('ho-release/', '')
528
528
 
529
+ # Accept full branch name or short patch ID
530
+ if patch_id.startswith('ho-patch/'):
531
+ patch_id = patch_id[len('ho-patch/'):]
532
+
529
533
  # Confirmation
530
534
  if not force:
531
535
  click.echo(f"Attaching patch '{patch_id}' to release {version}")
@@ -0,0 +1,198 @@
1
+ """
2
+ Upgrade command - Apply releases sequentially to production database.
3
+
4
+ Equivalent to 'apt upgrade' - applies available releases incrementally
5
+ to existing production database without data destruction.
6
+ """
7
+
8
+ import click
9
+ from half_orm_dev.repo import Repo
10
+ from half_orm_dev.release_manager import ReleaseManagerError
11
+ from half_orm import utils
12
+
13
+
14
+ @click.command()
15
+ @click.option(
16
+ '--to-release', '-t',
17
+ type=str,
18
+ default=None,
19
+ help='Stop at specific version (e.g., 1.3.7). Default: choose interactively'
20
+ )
21
+ @click.option(
22
+ '--dry-run', '-d',
23
+ is_flag=True,
24
+ help='Simulate upgrade without making changes'
25
+ )
26
+ @click.option(
27
+ '--force',
28
+ is_flag=True,
29
+ help='Overwrite existing backup without confirmation'
30
+ )
31
+ @click.option(
32
+ '--skip-backup',
33
+ is_flag=True,
34
+ help='Skip backup creation (DANGEROUS - for testing only)'
35
+ )
36
+ @click.option(
37
+ '--yes', '-y',
38
+ is_flag=True,
39
+ help='Skip confirmation prompt'
40
+ )
41
+ def upgrade(to_release, dry_run, force, skip_backup, yes):
42
+ """
43
+ Apply releases sequentially to production database.
44
+
45
+ Fetches available releases, lets you choose a target version interactively,
46
+ then upgrades the production database incrementally without data destruction.
47
+ Creates automatic backup before any changes.
48
+
49
+ Examples:
50
+ # Interactive: choose target from list
51
+ half_orm dev upgrade
52
+
53
+ # Upgrade to specific version (no prompt)
54
+ half_orm dev upgrade --to-release=1.3.7
55
+
56
+ # Simulate upgrade (no changes, no prompt)
57
+ half_orm dev upgrade --dry-run
58
+
59
+ # Apply all without confirmation
60
+ half_orm dev upgrade --yes
61
+ """
62
+ try:
63
+ repo = Repo()
64
+
65
+ # === Fetch and display available releases ===
66
+ click.echo("🔄 Fetching available releases...\n")
67
+ update_info = repo.release_manager.update_production()
68
+
69
+ current = update_info['current_version']
70
+ click.echo(f"Current version: {utils.Color.bold(current)}")
71
+
72
+ if not update_info['has_updates']:
73
+ click.echo(f"\n✓ {utils.Color.green('Production is already at latest version.')}")
74
+ return
75
+
76
+ available = update_info['available_releases']
77
+ upgrade_path = update_info['upgrade_path']
78
+ latest = upgrade_path[-1]
79
+
80
+ click.echo(f"\nAvailable releases:")
81
+ for rel in available:
82
+ patch_count = len(rel['patches'])
83
+ patches_label = f"{patch_count} patch{'es' if patch_count != 1 else ''}"
84
+ click.echo(f" • {utils.Color.bold(rel['version'])} ({patches_label})")
85
+
86
+ # === Determine target version ===
87
+ if to_release is None and not dry_run and not yes:
88
+ path_str = " → ".join([current] + upgrade_path)
89
+ click.echo(f"\nUpgrade path: {path_str}\n")
90
+
91
+ raw = click.prompt(
92
+ "Target version",
93
+ default=latest,
94
+ ).strip()
95
+
96
+ if raw not in upgrade_path:
97
+ click.echo(
98
+ f"\n❌ '{raw}' is not in the upgrade path.\n"
99
+ f" Available: {', '.join(upgrade_path)}",
100
+ err=True,
101
+ )
102
+ raise click.Abort()
103
+ to_release = raw if raw != latest else None # None means "all"
104
+
105
+ # === Confirmation (unless --dry-run or --yes) ===
106
+ if not dry_run and not yes:
107
+ apply_path = upgrade_path
108
+ if to_release:
109
+ apply_path = upgrade_path[:upgrade_path.index(to_release) + 1]
110
+
111
+ click.echo(f"\nWill apply: {utils.Color.bold(' → '.join(apply_path))}")
112
+ if not skip_backup:
113
+ click.echo("Will create backup before starting.")
114
+
115
+ if not click.confirm("\nProceed?", default=True):
116
+ click.echo("\nUpgrade cancelled.")
117
+ return
118
+
119
+ click.echo()
120
+
121
+ # === Run upgrade (pass pre-fetched update_info to avoid double git fetch) ===
122
+ result = repo.release_manager.upgrade_production(
123
+ to_version=to_release,
124
+ dry_run=dry_run,
125
+ force_backup=force,
126
+ skip_backup=skip_backup,
127
+ update_info=update_info,
128
+ )
129
+
130
+ _display_upgrade_results(result)
131
+
132
+ except ReleaseManagerError as e:
133
+ click.echo(f"\n❌ {utils.Color.red('Upgrade failed:')}")
134
+ click.echo(f" {str(e)}\n")
135
+ raise click.Abort()
136
+
137
+
138
+ def _display_upgrade_results(result):
139
+ """Format and display upgrade results."""
140
+ if result.get('dry_run'):
141
+ click.echo(f"{utils.Color.bold('DRY RUN')} - Simulation only, no changes made\n")
142
+
143
+ current = result['current_version']
144
+ click.echo(f"Current version: {utils.Color.bold(current)}")
145
+
146
+ if not result.get('releases_would_apply'):
147
+ click.echo(f"\n✓ {utils.Color.green('Already at latest version')}")
148
+ return
149
+
150
+ click.echo(f"\nWould create backup: {utils.Color.bold(result.get('backup_would_be_created', ''))}")
151
+ click.echo(f"\nWould apply releases:")
152
+ for version in result['releases_would_apply']:
153
+ patches = result['patches_would_apply'][version]
154
+ click.echo(f" → {utils.Color.bold(version)} - {len(patches)} patches")
155
+ for patch_id in patches:
156
+ click.echo(f" • {patch_id}")
157
+
158
+ final = result['final_version']
159
+ click.echo(f"\nWould upgrade: {current} → {utils.Color.green(final)}")
160
+ click.echo(f"\n{utils.Color.bold('To apply this upgrade, run without --dry-run')}")
161
+ return
162
+
163
+ current = result['current_version']
164
+
165
+ if result.get('backup_created'):
166
+ click.echo(f"✓ Backup created: {utils.Color.bold(result['backup_created'])}")
167
+ elif result.get('snapshot_used'):
168
+ click.echo(f"✓ Snapshot created: {utils.Color.bold(result['snapshot_used'])}")
169
+ elif result.get('releases_applied'):
170
+ click.echo(f"⚠️ {utils.Color.bold('No backup created (--skip-backup used)')}")
171
+
172
+ if not result['releases_applied']:
173
+ click.echo(f"\n✓ {utils.Color.green('Production already at latest version')}")
174
+ return
175
+
176
+ click.echo(f"\n{utils.Color.green('Applied releases:')}")
177
+ for version in result['releases_applied']:
178
+ patches = result['patches_applied'][version]
179
+ if patches:
180
+ click.echo(f" ✓ {utils.Color.bold(version)} - {len(patches)} patches")
181
+ for patch_id in patches:
182
+ click.echo(f" • {patch_id}")
183
+ else:
184
+ click.echo(f" ✓ {utils.Color.bold(version)} - (empty release)")
185
+
186
+ final = result['final_version']
187
+ click.echo(f"\n{utils.Color.green('✓ Upgrade complete!')}")
188
+ click.echo(f" {current} → {utils.Color.bold(utils.Color.green(final))}")
189
+
190
+ if result.get('target_version'):
191
+ click.echo(f"\n📝 Partial upgrade to {result['target_version']} complete.")
192
+ click.echo(f" To upgrade further, run: half_orm dev upgrade")
193
+ else:
194
+ click.echo(f"\n📝 Production is now at latest version.")
195
+
196
+ if result.get('backup_created'):
197
+ click.echo(f"\n💡 To rollback if needed:")
198
+ click.echo(f" psql -d {result.get('db_name', 'DATABASE')} -f {result['backup_created']}")
@@ -149,7 +149,7 @@ class Database:
149
149
 
150
150
  def __init_db(self):
151
151
  """Tries to connect to the database. If unsuccessful, creates the
152
- database end initializes it with half_orm_meta.
152
+ database and initializes it with half_orm_meta.
153
153
  """
154
154
  try:
155
155
  self.__model = Model(self.__name)
@@ -590,6 +590,12 @@ class MigrationManager:
590
590
  Stale local branches (no longer on remote) are skipped to avoid pre-commit
591
591
  hook failures.
592
592
  """
593
+ if self._repo.production:
594
+ raise MigrationManagerError(
595
+ "PRODUCTION SAFETY: _regenerate_modules_after_migration() is forbidden "
596
+ "on a production server.\nModule regeneration (which includes database "
597
+ "restoration) must never run in production."
598
+ )
593
599
  import re as _re
594
600
  from half_orm_dev import modules as _modules
595
601
 
@@ -173,9 +173,11 @@ def __get_field_desc(field_name, field):
173
173
  field_desc = f'{field_desc.__module__}.{ext}'
174
174
  else:
175
175
  field_desc = field_desc.__name__
176
- value = 'dataclasses.field(default=None)'
177
176
  if field._metadata['fieldtype'][0] == '_':
178
177
  value = 'dataclasses.field(default_factory=list)'
178
+ else:
179
+ value = 'dataclasses.field(default=None)'
180
+ field_desc = f'Optional[{field_desc}]'
179
181
  field_desc = f'{field_desc} = {value}'
180
182
  field_desc = f" {field_name}: {field_desc}"
181
183
  error = utils.check_attribute_name(field_name)
@@ -188,10 +190,10 @@ def __gen_dataclass(relation, fkeys):
188
190
  rel = relation()
189
191
  dc_name = relation._ho_dataclass_name()
190
192
  fields = []
191
- post_init = [' def __post_init__(self):']
193
+ post_init = [' def __post_init__(self) -> None:']
192
194
  for field_name, field in rel._ho_fields.items():
193
195
  fields.append(__get_field_desc(field_name, field))
194
- post_init.append(f' self.{field_name}: Field = None')
196
+ post_init.append(f' self.{field_name}: Optional[Field] = None')
195
197
 
196
198
  # Invert user-defined aliases: constraint_name → alias
197
199
  aliases = {constraint: alias for alias, constraint in fkeys.items() if alias != ''}
@@ -518,15 +520,22 @@ def __update_this_module(
518
520
  fields = []
519
521
  kwargs = []
520
522
  arg_names = []
523
+ type_import_modules: set = set()
521
524
  for key, value in rel._ho_fields.items():
522
525
  error = utils.check_attribute_name(key)
523
526
  if not error:
524
527
  fields.append(f"self.{key}: Field = None")
525
528
  kwarg_type = 'typing.Any'
526
529
  if hasattr(value.py_type, '__name__'):
527
- kwarg_type = str(value.py_type.__name__)
528
- kwargs.append(f"{key}: '{kwarg_type}'=None")
530
+ mod = getattr(value.py_type, '__module__', 'builtins')
531
+ if mod and mod != 'builtins':
532
+ type_import_modules.add(mod)
533
+ kwarg_type = f'{mod}.{value.py_type.__name__}'
534
+ else:
535
+ kwarg_type = str(value.py_type.__name__)
536
+ kwargs.append(f"{key}: 'typing.Optional[{kwarg_type}]'=None")
529
537
  arg_names.append(f'{key}={key}')
538
+ type_imports = '\n'.join(f'import {m}' for m in sorted(type_import_modules))
530
539
  fields = "\n ".join(fields)
531
540
  kwargs.append('**kwargs')
532
541
  kwargs = ", ".join(kwargs)
@@ -575,6 +584,7 @@ def __update_this_module(
575
584
  fqtn=fqtn,
576
585
  kwargs=kwargs,
577
586
  arg_names=arg_names,
587
+ type_imports=type_imports,
578
588
  warning=WARNING_TEMPLATE.format(package_name=package_name)))
579
589
 
580
590
  # Generate test file in tests/ directory structure
@@ -641,7 +651,7 @@ def __gen_dc_relation() -> tuple:
641
651
  if is_classmethod:
642
652
  block.append(' @classmethod')
643
653
  prefix = ' async def' if is_async else ' def'
644
- block.append(f'{prefix} {name}{sig_str}:')
654
+ block.append(f'{prefix} {name}{sig_str}: # type: ignore[empty-body]')
645
655
  if doc:
646
656
  block.append(_fmt_doc(doc))
647
657
  block.append(' ...')
@@ -670,29 +680,29 @@ def __gen_baseclass(relation, fkeys) -> str:
670
680
 
671
681
  lines = [
672
682
  f"class {bc_name}(",
673
- f" MODEL.get_relation_class('{fqtn}', fields_aliases=None),",
683
+ f" MODEL.get_relation_class('{fqtn}', fields_aliases=None), # type: ignore[misc]",
674
684
  f" {dc_name}",
675
685
  f"):",
676
686
  f" def __iter__(self) -> Iterator[{d}]:",
677
- f" return super().__iter__()",
687
+ f" return super().__iter__() # type: ignore[return-value]",
678
688
  f"",
679
- f" def ho_select(self, *args, distinct: bool = False, order_by: str = None, limit: int = None, offset: int = None, json_agg=None) -> Iterator[{d}]:",
680
- f" return super().ho_select(*args, distinct=distinct, order_by=order_by, limit=limit, offset=offset, json_agg=json_agg)",
689
+ f" def ho_select(self, *args, distinct: bool = False, order_by: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, json_agg=None) -> Iterator[{d}]:",
690
+ f" return super().ho_select(*args, distinct=distinct, order_by=order_by, limit=limit, offset=offset, json_agg=json_agg) # type: ignore[return-value]",
681
691
  f"",
682
- f" def ho_get(self, *args) -> {d}:",
683
- f" return super().ho_get(*args)",
692
+ f" def ho_get(self, *args) -> {d}: # type: ignore[override]",
693
+ f" return super().ho_get(*args) # type: ignore[return-value]",
684
694
  f"",
685
- f" def ho_insert(self, *args, upsert: Optional[bool] = False) -> {d}:",
686
- f" return super().ho_insert(*args, upsert=upsert)",
695
+ f" def ho_insert(self, *args, upsert: Optional[bool] = False) -> {d}: # type: ignore[override]",
696
+ f" return super().ho_insert(*args, upsert=upsert) # type: ignore[return-value]",
687
697
  f"",
688
- f" async def ho_aselect(self, *args, distinct: bool = False, order_by: str = None, limit: int = None, offset: int = None) -> List[{d}]:",
689
- f" return await super().ho_aselect(*args, distinct=distinct, order_by=order_by, limit=limit, offset=offset)",
698
+ f" async def ho_aselect(self, *args, distinct: bool = False, order_by: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None) -> List[{d}]:",
699
+ f" return await super().ho_aselect(*args, distinct=distinct, order_by=order_by, limit=limit, offset=offset) # type: ignore[return-value]",
690
700
  f"",
691
- f" async def ho_aget(self, *args) -> {d}:",
692
- f" return await super().ho_aget(*args)",
701
+ f" async def ho_aget(self, *args) -> {d}: # type: ignore[override]",
702
+ f" return await super().ho_aget(*args) # type: ignore[return-value]",
693
703
  f"",
694
- f" async def ho_ainsert(self, *args, upsert: bool = False) -> {d}:",
695
- f" return await super().ho_ainsert(*args, upsert=upsert)",
704
+ f" async def ho_ainsert(self, *args, upsert: bool = False) -> {d}: # type: ignore[override]",
705
+ f" return await super().ho_ainsert(*args, upsert=upsert) # type: ignore[return-value]",
696
706
  ]
697
707
  return '\n'.join(lines)
698
708
 
@@ -716,10 +726,10 @@ def __gen_baseclasses(package_dir, package_name):
716
726
  typing_names = sorted(dc_typing | {'Iterator', 'List', 'Optional', 'TYPE_CHECKING'})
717
727
  file_.write(f"from typing import {', '.join(typing_names)}\n")
718
728
  file_.write("import dataclasses\n")
719
- file_.write("from half_orm.field import Field\n")
729
+ file_.write("from half_orm.field import Field # type: ignore[import-not-found]\n")
720
730
  for mod in sorted(HO_DATACLASSES_IMPORTS):
721
731
  file_.write(f"import {mod}\n")
722
- file_.write(f"from {package_name} import MODEL\n")
732
+ file_.write(f"from {package_name} import MODEL # type: ignore[import-not-found]\n")
723
733
  if HO_BASECLASSES_DICT_NAMES:
724
734
  file_.write("if TYPE_CHECKING:\n")
725
735
  file_.write(f" from {package_name}.ho_typeddicts import (\n")
File without changes
@@ -1518,7 +1518,8 @@ class ReleaseManager:
1518
1518
  to_version: Optional[str] = None,
1519
1519
  dry_run: bool = False,
1520
1520
  force_backup: bool = False,
1521
- skip_backup: bool = False
1521
+ skip_backup: bool = False,
1522
+ update_info: Optional[dict] = None,
1522
1523
  ) -> dict:
1523
1524
  """
1524
1525
  Upgrade production database to target version.
@@ -1610,10 +1611,30 @@ class ReleaseManager:
1610
1611
  # 'message': 'Production already at latest version'
1611
1612
  # }
1612
1613
  """
1614
+ assert self._repo.production
1613
1615
  # Get current version
1614
1616
  current_version = self._repo.database.last_release_s
1615
1617
 
1616
- # === 1. SNAPSHOT OR BACKUP (unless dry_run or skip_backup) ===
1618
+ # === 1. Get available releases (before any destructive operation) ===
1619
+ if update_info is None:
1620
+ update_info = self.update_production()
1621
+
1622
+ # Check if already up to date — exit before creating any snapshot/backup
1623
+ if not update_info['has_updates']:
1624
+ return {
1625
+ 'status': 'success',
1626
+ 'dry_run': False,
1627
+ 'backup_created': None,
1628
+ 'snapshot_used': None,
1629
+ 'current_version': current_version,
1630
+ 'target_version': to_version,
1631
+ 'releases_applied': [],
1632
+ 'patches_applied': {},
1633
+ 'final_version': current_version,
1634
+ 'message': 'Production already at latest version'
1635
+ }
1636
+
1637
+ # === 2. SNAPSHOT OR BACKUP (unless dry_run or skip_backup) ===
1617
1638
  # Preferred: instant snapshot via CREATE DATABASE ... TEMPLATE (requires CREATEDB).
1618
1639
  # Fallback: full pg_dump.
1619
1640
  # Connections are terminated before the snapshot — this is intentional:
@@ -1639,27 +1660,9 @@ class ReleaseManager:
1639
1660
  force=force_backup
1640
1661
  )
1641
1662
 
1642
- # === 2. Validate environment ===
1663
+ # === 3. Validate environment ===
1643
1664
  self._validate_production_upgrade()
1644
1665
 
1645
- # === 3. Get available releases ===
1646
- update_info = self.update_production()
1647
-
1648
- # Check if already up to date
1649
- if not update_info['has_updates']:
1650
- return {
1651
- 'status': 'success',
1652
- 'dry_run': False,
1653
- 'backup_created': backup_path,
1654
- 'snapshot_used': snapshot_name,
1655
- 'current_version': current_version,
1656
- 'target_version': to_version,
1657
- 'releases_applied': [],
1658
- 'patches_applied': {},
1659
- 'final_version': current_version,
1660
- 'message': 'Production already at latest version'
1661
- }
1662
-
1663
1666
  # === 4. Calculate upgrade path ===
1664
1667
  if to_version:
1665
1668
  # Upgrade to specific version
@@ -6,6 +6,8 @@ WARNING!
6
6
 
7
7
  {warning}
8
8
  """
9
+ import typing
10
+ {type_imports}
9
11
  from half_orm.model import register
10
12
  from {package_name} import MODEL, ho_baseclasses
11
13
  {inheritance_import}
@@ -0,0 +1 @@
1
+ 1.0.0-a25
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 1.0.0a23
3
+ Version: 1.0.0a25
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
@@ -14,6 +14,7 @@ half_orm_dev/migration_manager.py
14
14
  half_orm_dev/modules.py
15
15
  half_orm_dev/patch_manager.py
16
16
  half_orm_dev/patch_validator.py
17
+ half_orm_dev/py.typed
17
18
  half_orm_dev/release_file.py
18
19
  half_orm_dev/release_manager.py
19
20
  half_orm_dev/repo.py
@@ -45,6 +45,7 @@ include = ["half_orm_dev*"]
45
45
 
46
46
  [tool.setuptools.package-data]
47
47
  half_orm_dev = [
48
+ "py.typed",
48
49
  "templates/*",
49
50
  "templates/git-hooks/*",
50
51
  "templates/.gitignore",
@@ -1,191 +0,0 @@
1
- """
2
- Upgrade command - Apply releases sequentially to production database.
3
-
4
- Equivalent to 'apt upgrade' - applies available releases incrementally
5
- to existing production database without data destruction.
6
- """
7
-
8
- import click
9
- from pathlib import Path
10
- from half_orm_dev.repo import Repo
11
- from half_orm_dev.release_manager import ReleaseManagerError
12
- from half_orm import utils
13
-
14
-
15
- @click.command()
16
- @click.option(
17
- '--to-release', '-t',
18
- type=str,
19
- default=None,
20
- help='Stop at specific version (e.g., 1.3.7). Default: upgrade to latest'
21
- )
22
- @click.option(
23
- '--dry-run', '-d',
24
- is_flag=True,
25
- help='Simulate upgrade without making changes'
26
- )
27
- @click.option(
28
- '--force',
29
- is_flag=True,
30
- help='Overwrite existing backup without confirmation'
31
- )
32
- @click.option(
33
- '--skip-backup',
34
- is_flag=True,
35
- help='Skip backup creation (DANGEROUS - for testing only)'
36
- )
37
- def upgrade(to_release, dry_run, force, skip_backup):
38
- """
39
- Apply releases sequentially to production database.
40
-
41
- Upgrades production database by applying releases incrementally
42
- to existing data. NEVER destroys or recreates the database.
43
- Creates automatic backup before any changes.
44
-
45
- CRITICAL: This command works on EXISTING production database.
46
- It does NOT use restore operations that would destroy data.
47
-
48
- Must be run from ho-prod branch.
49
-
50
- Workflow:
51
- 1. CREATE BACKUP (backups/{version}.sql) - FIRST ACTION
52
- 2. Validate environment (ho-prod branch, clean repo)
53
- 3. Apply releases sequentially on existing database
54
- 4. Update database version after each release
55
-
56
- Examples:
57
- # Upgrade to latest (all available releases)
58
- half_orm dev upgrade
59
-
60
- # Upgrade to specific version
61
- half_orm dev upgrade --to-release=1.3.7
62
-
63
- # Simulate upgrade (no changes)
64
- half_orm dev upgrade --dry-run
65
-
66
- # Force overwrite existing backup
67
- half_orm dev upgrade --force
68
-
69
- Options:
70
- --to-release=VERSION Stop at specific version
71
- --dry-run Simulate without changes
72
- --force Overwrite existing backup
73
- --skip-backup Skip backup (DANGEROUS)
74
-
75
- Requires:
76
- - Current branch: ho-prod
77
- - Repository: clean (no uncommitted changes)
78
- - Permissions: Database write access
79
- """
80
- try:
81
- # Get repository instance
82
- repo = Repo()
83
-
84
- # Delegate to ReleaseManager
85
- click.echo("🔄 Starting production upgrade...\n")
86
-
87
- result = repo.release_manager.upgrade_production(
88
- to_version=to_release,
89
- dry_run=dry_run,
90
- force_backup=force,
91
- skip_backup=skip_backup
92
- )
93
-
94
- # Display results
95
- _display_upgrade_results(result)
96
-
97
- except ReleaseManagerError as e:
98
- click.echo(f"\n❌ {utils.Color.red('Upgrade failed:')}")
99
- click.echo(f" {str(e)}\n")
100
- raise click.Abort()
101
-
102
-
103
- def _display_upgrade_results(result):
104
- """
105
- Format and display upgrade results to user.
106
-
107
- Args:
108
- result: Dict from ReleaseManager.upgrade_production()
109
- """
110
- # === DRY RUN MODE ===
111
- if result.get('dry_run'):
112
- click.echo(f"{utils.Color.bold('DRY RUN')} - Simulation only, no changes made\n")
113
-
114
- current = result['current_version']
115
- click.echo(f"Current version: {utils.Color.bold(current)}")
116
-
117
- if not result['releases_would_apply']:
118
- click.echo(f"\n✓ {utils.Color.green('Already at latest version')}")
119
- return
120
-
121
- # Show what would happen
122
- click.echo(f"\nWould create backup: {utils.Color.bold(result['backup_would_be_created'])}")
123
-
124
- click.echo(f"\nWould apply releases:")
125
- for version in result['releases_would_apply']:
126
- patches = result['patches_would_apply'][version]
127
- patch_count = len(patches)
128
- click.echo(f" → {utils.Color.bold(version)} - {patch_count} patches")
129
- for patch_id in patches:
130
- click.echo(f" • {patch_id}")
131
-
132
- final = result['final_version']
133
- click.echo(f"\nWould upgrade: {current} → {utils.Color.green(final)}")
134
-
135
- click.echo(f"\n{utils.Color.bold('To apply this upgrade, run without --dry-run')}")
136
- return
137
-
138
- # === ACTUAL UPGRADE ===
139
-
140
- current = result['current_version']
141
-
142
- # Backup confirmation
143
- if result['backup_created']:
144
- backup_path = result['backup_created']
145
- click.echo(f"✓ Backup created: {utils.Color.bold(backup_path)}")
146
- elif result.get('message') and 'already at latest' in result['message'].lower():
147
- # Up to date scenario
148
- pass
149
- else:
150
- click.echo(f"⚠️ {utils.Color.bold('No backup created (--skip-backup used)')}")
151
-
152
- click.echo(f"\nCurrent version: {utils.Color.bold(current)}")
153
-
154
- # Check if already up to date
155
- if not result['releases_applied']:
156
- click.echo(f"\n✓ {utils.Color.green('Production already at latest version')}")
157
- return
158
-
159
- # Show applied releases
160
- click.echo(f"\n{utils.Color.green('Applied releases:')}")
161
-
162
- for version in result['releases_applied']:
163
- patches = result['patches_applied'][version]
164
- patch_count = len(patches)
165
-
166
- if patch_count == 0:
167
- click.echo(f" ✓ {utils.Color.bold(version)} - (empty release)")
168
- else:
169
- click.echo(f" ✓ {utils.Color.bold(version)} - {patch_count} patches")
170
- for patch_id in patches:
171
- click.echo(f" • {patch_id}")
172
-
173
- # Final status
174
- final = result['final_version']
175
- click.echo(f"\n{utils.Color.green('✓ Upgrade complete!')}")
176
- click.echo(f" {current} → {utils.Color.bold(utils.Color.green(final))}")
177
-
178
- # Next steps
179
- if result['target_version']:
180
- # Partial upgrade
181
- click.echo(f"\n📝 Partial upgrade to {result['target_version']} complete.")
182
- click.echo(f" To upgrade further, run: half_orm dev upgrade")
183
- else:
184
- # Full upgrade
185
- click.echo(f"\n📝 Production is now at latest version.")
186
-
187
- # Rollback information
188
- if result['backup_created']:
189
- backup_path = result['backup_created']
190
- click.echo(f"\n💡 To rollback if needed:")
191
- click.echo(f" psql -d {result.get('db_name', 'DATABASE')} -f {backup_path}")
@@ -1 +0,0 @@
1
- 1.0.0-a23
File without changes
File without changes