half-orm-dev 0.17.3a4__py3-none-any.whl → 0.17.3a6__py3-none-any.whl

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.
@@ -369,39 +369,118 @@ def release_hotfix(version: Optional[str] = None) -> None:
369
369
 
370
370
 
371
371
  @release.command('apply')
372
- @click.argument('version', type=str, required=False)
373
- def release_apply(version: Optional[str] = None) -> None:
372
+ @click.option(
373
+ '--skip-tests',
374
+ is_flag=True,
375
+ help='Skip running pytest after applying patches'
376
+ )
377
+ def release_apply(skip_tests: bool) -> None:
374
378
  """
375
- Test complete release before deployment (NOT IMPLEMENTED YET).
379
+ Apply all patches for integration testing.
380
+
381
+ Restores the database from production schema and applies ALL patches
382
+ (candidates + staged) to test the complete release integration.
383
+ Optionally runs pytest to validate the release.
376
384
 
377
- Applies all patches from a release file to test the complete
378
- release workflow before production deployment.
385
+ Unlike 'patch apply' which only applies validated (staged) patches,
386
+ this command applies ALL patches including candidates that haven't
387
+ passed individual validation yet. This enables testing the complete
388
+ release before promotion.
379
389
 
380
390
  \b
381
- Args:
382
- version: Release version to test (e.g., "1.3.5-rc1")
383
- If not provided, applies latest RC
391
+ Workflow:
392
+ 1. Validate on release branch (ho-release/X.Y.Z)
393
+ 2. Restore database from model/schema.sql
394
+ 3. Apply all RC patches (if any)
395
+ 4. Apply ALL TOML patches (candidates + staged)
396
+ 5. Generate Python code
397
+ 6. Run pytest (unless --skip-tests)
398
+
399
+ \b
400
+ Requirements:
401
+ - Must be on ho-release/X.Y.Z branch
402
+ - Development release must exist
384
403
 
385
404
  \b
386
405
  Examples:
387
- Test latest RC:
406
+ Apply all patches and run tests:
388
407
  $ half_orm dev release apply
389
408
 
390
- Test specific RC:
391
- $ half_orm dev release apply 1.3.5-rc1
392
-
393
- Test stage release:
394
- $ half_orm dev release apply 1.3.5-stage
409
+ Apply patches without running tests:
410
+ $ half_orm dev release apply --skip-tests
395
411
 
396
412
  \b
397
- Status: 🚧 Not implemented - planned for future release
413
+ Output:
414
+ ✓ Applied 5 patches for version 1.3.6
415
+ ✓ Tests passed (42 tests)
416
+
417
+ Or on failure:
418
+ ✗ Tests failed
419
+ <test output>
398
420
  """
399
- click.echo("🚧 Release testing not implemented yet")
400
- click.echo()
401
- click.echo("Planned workflow:")
402
- click.echo(" 1. Restore database from model/schema.sql")
403
- click.echo(" 2. Apply all patches from release file")
404
- click.echo(" 3. Run comprehensive tests")
405
- click.echo(" 4. Validate final state")
406
- click.echo()
407
- raise NotImplementedError("Release apply not yet implemented")
421
+ try:
422
+ # Get repository instance
423
+ repo = Repo()
424
+ release_mgr = repo.release_manager
425
+
426
+ # Display context
427
+ click.echo("🔄 Applying all release patches for integration testing...")
428
+ click.echo()
429
+
430
+ # Apply release
431
+ result = release_mgr.apply_release(run_tests=not skip_tests)
432
+
433
+ # Display results
434
+ version = result['version']
435
+ patches = result['patches_applied']
436
+ candidates = result.get('candidates_merged', [])
437
+ patch_count = len(patches)
438
+
439
+ click.echo(f"✓ {utils.Color.green('Patches applied successfully!')}")
440
+ click.echo()
441
+ click.echo(f" Version: {utils.Color.bold(version)}")
442
+ click.echo(f" Patches: {utils.Color.bold(str(patch_count))}")
443
+ if candidates:
444
+ click.echo(f" Candidates merged: {utils.Color.bold(str(len(candidates)))}")
445
+ click.echo()
446
+
447
+ # Show candidates merged (simulated merges)
448
+ if candidates:
449
+ click.echo(" Candidate branches merged (simulated):")
450
+ for patch_id in candidates:
451
+ click.echo(f" • ho-patch/{patch_id}")
452
+ click.echo()
453
+
454
+ # Show patches applied
455
+ if patches:
456
+ click.echo(" Applied patches (SQL):")
457
+ for patch_id in patches:
458
+ click.echo(f" • {patch_id}")
459
+ click.echo()
460
+
461
+ # Show test results
462
+ if skip_tests:
463
+ click.echo(f"⚠️ {utils.Color.bold('Tests skipped')} (--skip-tests flag)")
464
+ elif result['tests_passed'] is None:
465
+ click.echo(f"⚠️ {utils.Color.bold('Tests not run')} (pytest not found)")
466
+ elif result['tests_passed']:
467
+ click.echo(f"✓ {utils.Color.green('Tests passed!')}")
468
+ else:
469
+ click.echo(f"✗ {utils.Color.red('Tests failed!')}")
470
+ click.echo()
471
+ click.echo("Test output:")
472
+ click.echo(result['test_output'])
473
+ sys.exit(1)
474
+
475
+ click.echo()
476
+ click.echo("📝 Next steps:")
477
+ if result['tests_passed'] or result['tests_passed'] is None:
478
+ click.echo(f" • Promote to RC: {utils.Color.bold('half_orm dev release promote rc')}")
479
+ else:
480
+ click.echo(f" • Fix failing tests")
481
+ click.echo(f" • Re-run: {utils.Color.bold('half_orm dev release apply')}")
482
+
483
+ except ReleaseManagerError as e:
484
+ click.echo(f"❌ {utils.Color.red('Release apply failed:')}", err=True)
485
+ click.echo(f" {str(e)}", err=True)
486
+ sys.exit(1)
half_orm_dev/database.py CHANGED
@@ -93,6 +93,10 @@ class Database:
93
93
  if get_release and self.__repo.devel:
94
94
  self.__last_release = self.last_release
95
95
 
96
+ @property
97
+ def name(self):
98
+ return self.__name
99
+
96
100
  @property
97
101
  def last_release(self):
98
102
  "Returns the last release"
@@ -177,10 +181,22 @@ class Database:
177
181
  *command_args
178
182
  )
179
183
 
180
- def register_release(self, major, minor, patch, changelog=None):
181
- "Register the release into half_orm_meta.hop_release"
184
+ def register_release(self, major, minor, patch, pre_release='', pre_release_num='', changelog=None):
185
+ """
186
+ Register the release into half_orm_meta.hop_release.
187
+
188
+ Args:
189
+ major: Major version number
190
+ minor: Minor version number
191
+ patch: Patch version number
192
+ pre_release: Pre-release type ('alpha', 'beta', 'rc', or '' for production)
193
+ pre_release_num: Pre-release number (e.g., '1' for rc1)
194
+ changelog: Optional changelog text
195
+ """
182
196
  return self.__model.get_relation_class('half_orm_meta.hop_release')(
183
- major=major, minor=minor, patch=patch, changelog=changelog
197
+ major=major, minor=minor, patch=patch,
198
+ pre_release=pre_release, pre_release_num=pre_release_num,
199
+ changelog=changelog
184
200
  ).ho_insert()
185
201
 
186
202
  def _generate_schema_sql(self, version: str, model_dir: Path) -> Path:
@@ -271,6 +287,7 @@ class Database:
271
287
 
272
288
  # Construct versioned schema file path
273
289
  schema_file = model_dir / f"schema-{version}.sql"
290
+ temp_schema_file = model_dir / f".schema-{version}.sql.tmp"
274
291
 
275
292
  # Generate schema dump using pg_dump
276
293
  try:
@@ -278,16 +295,45 @@ class Database:
278
295
  'pg_dump',
279
296
  self.__name,
280
297
  '--schema-only',
298
+ '--no-owner',
281
299
  '-f',
282
- str(schema_file)
300
+ str(temp_schema_file)
301
+ )
302
+
303
+ # Filter out version-specific lines for cross-version compatibility
304
+ content = temp_schema_file.read_text()
305
+ filtered_lines = []
306
+ # SET commands that are version-specific and should be removed
307
+ version_specific_sets = (
308
+ 'SET transaction_timeout', # PG17+
283
309
  )
310
+ for line in content.split('\n'):
311
+ # Skip \restrict and \unrestrict lines
312
+ if line.startswith('\\restrict') or line.startswith('\\unrestrict'):
313
+ continue
314
+ # Skip "-- Dumped from/by" comments (version-specific)
315
+ if line.startswith('-- Dumped from') or line.startswith('-- Dumped by'):
316
+ continue
317
+ # Skip version-specific SET commands
318
+ if any(line.startswith(s) for s in version_specific_sets):
319
+ continue
320
+ filtered_lines.append(line)
321
+
322
+ schema_file.write_text('\n'.join(filtered_lines))
284
323
  except Exception as e:
285
324
  raise Exception(f"Failed to generate schema SQL: {e}") from e
325
+ finally:
326
+ # Clean up temporary file
327
+ if temp_schema_file.exists():
328
+ temp_schema_file.unlink()
286
329
 
287
330
  # Generate metadata dump (half_orm_meta data only)
331
+ # Keep only COPY statements to avoid version-specific SET commands
288
332
  metadata_file = model_dir / f"metadata-{version}.sql"
333
+ temp_file = model_dir / f".metadata-{version}.sql.tmp"
289
334
 
290
335
  try:
336
+ # Dump to temporary file
291
337
  self.execute_pg_command(
292
338
  'pg_dump',
293
339
  self.__name,
@@ -296,10 +342,29 @@ class Database:
296
342
  '--table=half_orm_meta.hop_release',
297
343
  '--table=half_orm_meta.hop_release_issue',
298
344
  '-f',
299
- str(metadata_file)
345
+ str(temp_file)
300
346
  )
347
+
348
+ # Filter to keep only COPY blocks (COPY ... FROM stdin; ... \.)
349
+ content = temp_file.read_text()
350
+ filtered_lines = []
351
+ in_copy_block = False
352
+ for line in content.split('\n'):
353
+ if line.startswith('COPY '):
354
+ in_copy_block = True
355
+ if in_copy_block:
356
+ filtered_lines.append(line)
357
+ if line == '\\.':
358
+ in_copy_block = False
359
+ filtered_lines.append('') # Empty line between blocks
360
+
361
+ metadata_file.write_text('\n'.join(filtered_lines))
301
362
  except Exception as e:
302
363
  raise Exception(f"Failed to generate metadata SQL: {e}") from e
364
+ finally:
365
+ # Clean up temporary file
366
+ if temp_file.exists():
367
+ temp_file.unlink()
303
368
 
304
369
  # Create or update symlink
305
370
  symlink_path = model_dir / "schema.sql"
@@ -1916,7 +1916,7 @@ class PatchManager:
1916
1916
  # This also syncs .hop/ to all active branches automatically via decorator
1917
1917
  try:
1918
1918
  self._repo.commit_and_sync_to_active_branches(
1919
- message=f"[HOP] move patch #{patch_id} from candidate to stage %{version}"
1919
+ message=f"[HOP] move patch #{patch_id} from candidate to stage %{version}\nFixes #{patch_id}."
1920
1920
  )
1921
1921
  except Exception as e:
1922
1922
  raise PatchManagerError(f"Failed to commit/push changes: {e}")
@@ -744,27 +744,64 @@ class ReleaseManager:
744
744
  for patch_id in stage_patches:
745
745
  self._repo.patch_manager.apply_patch_files(patch_id, self._repo.model)
746
746
 
747
- def _generate_data_sql_file(self, patch_list: List[str], output_filename: str) -> Optional[Path]:
747
+ def _collect_all_version_patches(self, version: str) -> List[str]:
748
748
  """
749
- Generate data-X.Y.Z.sql file from patches with @HOP:data annotation.
749
+ Collect all patches for a version including hotfixes.
750
+
751
+ Returns patches from:
752
+ 1. Base release (X.Y.Z.txt)
753
+ 2. All hotfixes in order (X.Y.Z-hotfix1.txt, X.Y.Z-hotfix2.txt, ...)
754
+
755
+ Args:
756
+ version: Base version string (e.g., "1.2.0")
757
+
758
+ Returns:
759
+ List of all patch IDs in application order
760
+
761
+ Examples:
762
+ # With 1.2.0.txt containing [a, b] and 1.2.0-hotfix1.txt containing [c]
763
+ patches = mgr._collect_all_version_patches("1.2.0")
764
+ # Returns: ["a", "b", "c"]
765
+ """
766
+ all_patches = []
767
+
768
+ # 1. Base release patches
769
+ base_file = f"{version}.txt"
770
+ all_patches.extend(self.read_release_patches(base_file))
771
+
772
+ # 2. Hotfix patches in order
773
+ hotfix_files = sorted(self._releases_dir.glob(f"{version}-hotfix*.txt"))
774
+ for hotfix_file in hotfix_files:
775
+ all_patches.extend(self.read_release_patches(hotfix_file.name))
776
+
777
+ return all_patches
778
+
779
+ def _generate_data_sql_file(self, patch_list: List[str], version: str) -> Optional[Path]:
780
+ """
781
+ Generate model/data-X.Y.Z.sql file from patches with @HOP:data annotation.
750
782
 
751
783
  Collects all SQL files marked with `-- @HOP:data` from the patch list
752
784
  and concatenates them into a single data SQL file for from-scratch
753
- installations.
785
+ installations (clone, restore_database_from_schema).
786
+
787
+ This file is only generated for production releases. RC and hotfix
788
+ versions don't need this file because:
789
+ - In production upgrades, data is inserted by patch application
790
+ - This file is only for from-scratch installations
754
791
 
755
792
  Args:
756
793
  patch_list: List of patch IDs to process
757
- output_filename: Name of the output file (e.g., "data-0.17.0-rc1.sql")
794
+ version: Version string (e.g., "0.17.0")
758
795
 
759
796
  Returns:
760
- Path to generated file, or None if no data files found
797
+ Path to generated file (model/data-X.Y.Z.sql), or None if no data files found
761
798
 
762
799
  Examples:
763
800
  self._generate_data_sql_file(
764
801
  ["456-auth", "457-roles"],
765
- "data-0.17.0-rc1.sql"
802
+ "0.17.0"
766
803
  )
767
- # Generates releases/data-0.17.0-rc1.sql with data from both patches
804
+ # Generates model/data-0.17.0.sql with data from both patches
768
805
  """
769
806
  if not patch_list:
770
807
  return None
@@ -777,16 +814,17 @@ class ReleaseManager:
777
814
  # No data files found - skip generation
778
815
  return None
779
816
 
780
- # Generate output file
781
- output_path = self._releases_dir / output_filename
817
+ # Generate output file in model/ directory
818
+ output_filename = f"data-{version}.sql"
819
+ output_path = Path(self._repo.model_dir) / output_filename
782
820
 
783
821
  with output_path.open('w', encoding='utf-8') as out_file:
784
822
  # Write header
785
- out_file.write(f"-- Data file for {output_filename.replace('.sql', '')}\n")
823
+ out_file.write(f"-- Data file for version {version}\n")
786
824
  out_file.write(f"-- Generated from patches: {', '.join(patch_list)}\n")
787
825
  out_file.write(f"-- This file contains reference data (DML) for from-scratch installations\n")
788
826
  out_file.write(f"--\n")
789
- out_file.write(f"-- Usage: psql -f {output_filename}\n")
827
+ out_file.write(f"-- Usage: Automatically loaded by restore_database_from_schema()\n")
790
828
  out_file.write(f"--\n\n")
791
829
 
792
830
  # Concatenate all data files
@@ -811,26 +849,30 @@ class ReleaseManager:
811
849
 
812
850
  except Exception as e:
813
851
  raise ReleaseManagerError(
814
- f"Failed to generate data SQL file {output_filename}: {e}"
852
+ f"Failed to generate data SQL file data-{version}.sql: {e}"
815
853
  )
816
854
 
817
855
  def get_all_release_context_patches(self) -> List[str]:
818
856
  """
819
- Récupère TOUS les patches du contexte de la prochaine release.
857
+ Get all validated patches for the next release context.
858
+
859
+ Sequential application of incremental RCs + staged patches from TOML.
860
+ - rc1: initial patches (e.g., 123, 456, 789)
861
+ - rc2: new patches (e.g., 999)
862
+ - rc3: new patches (e.g., 888, 777)
863
+ - TOML: only "staged" patches (validated via patch merge)
820
864
 
821
- IMPORTANT: Application séquentielle des RC incrémentaux + TOML patches.
822
- - rc1: patches initiaux (ex: 123, 456, 789)
823
- - rc2: patches nouveaux (ex: 999)
824
- - rc3: patches nouveaux (ex: 888, 777)
825
- - TOML: TOUS les patches (candidates + staged) dans l'ordre
865
+ "candidate" patches are NOT included because they have not passed
866
+ the validation process (tests) that occurs during patch merge.
867
+ Only "staged" patches are guaranteed to have passed tests.
826
868
 
827
- Résultat: [123, 456, 789, 999, 888, 777, ...]
869
+ The current patch is applied separately by apply_patch_complete_workflow.
828
870
 
829
- Pas de déduplication car chaque RC est incrémental.
871
+ Note: A future "release apply" command could allow applying all patches
872
+ (candidates + staged) in a temporary branch for integration testing.
830
873
 
831
874
  Returns:
832
- Liste ordonnée des patch IDs (séquence complète)
833
- Inclut RC files + TOUS les patches du TOML (candidates + staged)
875
+ Ordered list of validated patch IDs (RC + staged)
834
876
 
835
877
  Examples:
836
878
  # Production: 1.3.5
@@ -839,14 +881,15 @@ class ReleaseManager:
839
881
  # 1.3.6-patches.toml: {"234": "candidate", "567": "staged"}
840
882
 
841
883
  patches = mgr.get_all_release_context_patches()
842
- # → ["123", "456", "789", "999", "234", "567"]
884
+ # → ["123", "456", "789", "999", "567"]
885
+ # Note: "234" (candidate) is not included - not yet validated
843
886
 
844
- # Pour apply-patch sur patch 888:
887
+ # For apply-patch on patch 234:
845
888
  # 1. Restore DB (1.3.5)
846
889
  # 2. Apply 123, 456, 789 (rc1)
847
890
  # 3. Apply 999 (rc2)
848
- # 4. Apply 234, 567 (from TOML, all patches)
849
- # 5. Apply 888 (patch courant)
891
+ # 4. Apply 567 (staged from TOML)
892
+ # 5. Apply 234 (current patch, applied separately)
850
893
  """
851
894
  next_version = self.get_next_release_version()
852
895
 
@@ -855,20 +898,21 @@ class ReleaseManager:
855
898
 
856
899
  all_patches = []
857
900
 
858
- # 1. Appliquer tous les RC dans l'ordre (incrémentaux)
901
+ # 1. Apply all RCs in order (incremental)
859
902
  rc_files = self._get_label_files(next_version, 'rc')
860
903
  for rc_file in rc_files:
861
904
  patches = self.read_release_patches(rc_file)
862
- # Chaque RC est incrémental, pas besoin de déduplication
905
+ # Each RC is incremental, no deduplication needed
863
906
  all_patches.extend(patches)
864
907
 
865
- # 2. Appliquer TOUS les patches du TOML (candidates + staged)
866
- # Pour les tests et la synchronisation, on veut tous les patches dans l'ordre
908
+ # 2. Apply only "staged" patches from TOML
909
+ # "candidate" patches are excluded because they have not yet passed
910
+ # the validation process (tests) that occurs during patch merge
911
+ # Current patch is applied separately by apply_patch_complete_workflow
867
912
  release_file = ReleaseFile(next_version, self._releases_dir)
868
913
  if release_file.exists():
869
- # get_patches() sans argument retourne TOUS les patches dans l'ordre d'insertion
870
- all_toml_patches = release_file.get_patches()
871
- all_patches.extend(all_toml_patches)
914
+ staged_patches = release_file.get_patches(status="staged")
915
+ all_patches.extend(staged_patches)
872
916
 
873
917
  return all_patches
874
918
 
@@ -1869,7 +1913,7 @@ class ReleaseManager:
1869
1913
  # → Raises: "Backup exists and user declined overwrite"
1870
1914
  """
1871
1915
  # Create backups directory if doesn't exist
1872
- backups_dir = Path(self._repo.base_dir) / "backups"
1916
+ backups_dir = Path(self._repo.base_dir) / ".hop" / "backups"
1873
1917
  backups_dir.mkdir(exist_ok=True)
1874
1918
 
1875
1919
  # Build backup filename
@@ -1893,6 +1937,7 @@ class ReleaseManager:
1893
1937
  try:
1894
1938
  self._repo.database.execute_pg_command(
1895
1939
  'pg_dump',
1940
+ self._repo.database.name,
1896
1941
  '-f', str(backup_file),
1897
1942
  )
1898
1943
  except Exception as e:
@@ -2290,10 +2335,18 @@ class ReleaseManager:
2290
2335
  # 1. Apply patches to database (for validation)
2291
2336
  self._apply_release_patches(version)
2292
2337
 
2293
- # 2. Checkout release branch
2338
+ # 2. Register the RC version in half_orm_meta.hop_release
2339
+ version_parts = version.split('.')
2340
+ major, minor, patch_num = map(int, version_parts)
2341
+ self._repo.database.register_release(
2342
+ major, minor, patch_num,
2343
+ pre_release='rc', pre_release_num=str(rc_number)
2344
+ )
2345
+
2346
+ # 3. Checkout release branch
2294
2347
  self._repo.hgit.checkout(release_branch)
2295
2348
 
2296
- # 3. Create RC tag on release branch
2349
+ # 4. Create RC tag on release branch
2297
2350
  self._repo.hgit.create_tag(rc_tag, f"Release Candidate %{version}")
2298
2351
 
2299
2352
  # Push tag
@@ -2309,14 +2362,10 @@ class ReleaseManager:
2309
2362
 
2310
2363
  # Keep TOML file for continued development (don't delete it)
2311
2364
 
2312
- # Generate data-X.Y.Z-rcN.sql if any patches have @HOP:data files
2313
- rc_patches = self.read_release_patches(rc_file.name)
2314
- data_file = self._generate_data_sql_file(
2315
- rc_patches,
2316
- f"data-{version}-rc{rc_number}.sql"
2317
- )
2365
+ # Note: data-X.Y.Z.sql is only generated for production releases
2366
+ # RC releases don't need it - data is inserted via patch application
2318
2367
 
2319
- # Commit RC snapshot and data file (both in .hop/releases/)
2368
+ # Commit RC snapshot (in .hop/releases/)
2320
2369
  # This also syncs .hop/ to all active branches automatically
2321
2370
  self._repo.commit_and_sync_to_active_branches(
2322
2371
  message=f"[HOP] Promote release %{version} to RC {rc_number}"
@@ -2473,6 +2522,11 @@ class ReleaseManager:
2473
2522
  # The database should already be in the correct state from RC
2474
2523
  pass
2475
2524
 
2525
+ # Register the release version in half_orm_meta.hop_release
2526
+ version_parts = version.split('.')
2527
+ major, minor, patch_num = map(int, version_parts)
2528
+ self._repo.database.register_release(major, minor, patch_num)
2529
+
2476
2530
  # Generate schema dump for this production version
2477
2531
  model_dir = Path(self._repo.model_dir)
2478
2532
  self._repo.database._generate_schema_sql(version, model_dir)
@@ -2488,13 +2542,12 @@ class ReleaseManager:
2488
2542
  if toml_file.exists():
2489
2543
  toml_file.unlink()
2490
2544
 
2491
- # Generate data-X.Y.Z.sql if any patches have @HOP:data files
2492
- # This includes patches from stage (incremental after last RC)
2545
+ # Generate model/data-X.Y.Z.sql if any patches have @HOP:data files
2546
+ # This file is used for from-scratch installations (clone, restore)
2493
2547
  prod_patches = self.read_release_patches(prod_file.name)
2494
- self._generate_data_sql_file(
2495
- prod_patches,
2496
- f"data-{version}.sql"
2497
- )
2548
+ data_file = self._generate_data_sql_file(prod_patches, version)
2549
+ if data_file:
2550
+ self._repo.hgit.add(str(data_file))
2498
2551
 
2499
2552
  self._repo.commit_and_sync_to_active_branches(
2500
2553
  message=f"[HOP] Promote release %{version} to production",
@@ -2909,14 +2962,12 @@ class ReleaseManager:
2909
2962
  if toml_file.exists():
2910
2963
  self._repo.hgit.rm(str(toml_file))
2911
2964
 
2912
- # Generate data-X.Y.Z-hotfixN.sql if any patches have @HOP:data files
2913
- hotfix_patches = self.read_release_patches(hotfix_file.name)
2914
- data_file = self._generate_data_sql_file(
2915
- hotfix_patches,
2916
- f"data-{version}-hotfix{hotfix_num}.sql"
2917
- )
2965
+ # Regenerate model/data-X.Y.Z.sql with all patches (original release + all hotfixes)
2966
+ # This ensures from-scratch installations get all data
2967
+ all_patches = self._collect_all_version_patches(version)
2968
+ data_file = self._generate_data_sql_file(all_patches, version)
2918
2969
  if data_file:
2919
- self._repo.hgit.add(str(data_file)) # Add data file if generated
2970
+ self._repo.hgit.add(str(data_file))
2920
2971
 
2921
2972
  # 6. Apply release patches and generate SQL dumps
2922
2973
  self._apply_release_patches(version, True)
@@ -2953,3 +3004,248 @@ class ReleaseManager:
2953
3004
  raise
2954
3005
  except Exception as e:
2955
3006
  raise ReleaseManagerError(f"Failed to promote hotfix: {e}")
3007
+
3008
+ def get_all_release_patches_for_testing(self) -> List[str]:
3009
+ """
3010
+ Get ALL patches for integration testing (candidates + staged).
3011
+
3012
+ Unlike get_all_release_context_patches() which excludes candidates,
3013
+ this method returns ALL patches including those not yet validated.
3014
+ Used by 'release apply' for complete integration testing.
3015
+
3016
+ Returns:
3017
+ Ordered list of ALL patch IDs (RC + candidates + staged)
3018
+
3019
+ Examples:
3020
+ # Production: 1.3.5
3021
+ # 1.3.6-rc1.txt: 123, 456, 789
3022
+ # 1.3.6-patches.toml: {"234": "candidate", "567": "staged"}
3023
+
3024
+ patches = mgr.get_all_release_patches_for_testing()
3025
+ # → ["123", "456", "789", "234", "567"]
3026
+ # All patches included for complete integration testing
3027
+ """
3028
+ next_version = self.get_next_release_version()
3029
+
3030
+ if not next_version:
3031
+ return []
3032
+
3033
+ all_patches = []
3034
+
3035
+ # 1. Apply all RCs in order (incremental)
3036
+ rc_files = self._get_label_files(next_version, 'rc')
3037
+ for rc_file in rc_files:
3038
+ patches = self.read_release_patches(rc_file)
3039
+ all_patches.extend(patches)
3040
+
3041
+ # 2. Apply ALL patches from TOML (candidates + staged)
3042
+ # For integration testing, we want to test the complete release
3043
+ release_file = ReleaseFile(next_version, self._releases_dir)
3044
+ if release_file.exists():
3045
+ all_toml_patches = release_file.get_patches() # No status filter = all
3046
+ all_patches.extend(all_toml_patches)
3047
+
3048
+ return all_patches
3049
+
3050
+ def _cleanup_validate_branch(self, original_branch: Optional[str],
3051
+ validate_branch: Optional[str]) -> None:
3052
+ """
3053
+ Cleanup temporary validation branch after apply_release.
3054
+
3055
+ Switches back to the original branch and deletes the temporary
3056
+ validation branch. Errors are silently ignored for robustness.
3057
+
3058
+ Args:
3059
+ original_branch: Branch to switch back to (may be None)
3060
+ validate_branch: Temporary branch to delete (may be None)
3061
+ """
3062
+ try:
3063
+ if original_branch:
3064
+ self._repo.hgit.checkout(original_branch)
3065
+ except Exception:
3066
+ pass # Best effort
3067
+
3068
+ try:
3069
+ if validate_branch:
3070
+ self._repo.hgit.delete_branch(validate_branch, force=True)
3071
+ except Exception:
3072
+ pass # Best effort
3073
+
3074
+ def apply_release(self, run_tests: bool = True) -> dict:
3075
+ """
3076
+ Apply all patches from current release for integration testing.
3077
+
3078
+ Creates a temporary validation branch (ho-validate/release-X.Y.Z),
3079
+ merges candidate patch branches, restores the database, applies ALL
3080
+ patches (including candidates), optionally runs tests, then cleans up.
3081
+ This simulates a complete release merge without modifying the release branch.
3082
+
3083
+ Unlike 'patch apply' which only applies staged patches,
3084
+ 'release apply' applies ALL patches (candidates + staged) to
3085
+ validate the complete integration.
3086
+
3087
+ Args:
3088
+ run_tests: Whether to run pytest after applying patches (default: True)
3089
+
3090
+ Returns:
3091
+ Dict containing:
3092
+ - version: Release version being tested
3093
+ - patches_applied: List of patch IDs applied
3094
+ - candidates_merged: List of candidate patch branches merged
3095
+ - files_applied: List of SQL/Python files applied
3096
+ - tests_passed: Boolean (None if tests not run)
3097
+ - test_output: Test output (None if tests not run)
3098
+ - status: 'success' or 'failed'
3099
+ - error: Error message if failed
3100
+
3101
+ Raises:
3102
+ ReleaseManagerError: If no release in development or apply fails
3103
+
3104
+ Workflow:
3105
+ 1. Detect current development release
3106
+ 2. Validate we're on release branch
3107
+ 3. Create temporary validation branch
3108
+ 4. Merge candidate patch branches (simulate future merges)
3109
+ 5. Restore database from production schema
3110
+ 6. Apply ALL patches (RC + staged + candidates)
3111
+ 7. Generate Python code
3112
+ 8. Optionally run tests
3113
+ 9. Cleanup: switch back and delete temp branch
3114
+ 10. Return results
3115
+
3116
+ Examples:
3117
+ # Test current release with tests
3118
+ result = release_mgr.apply_release()
3119
+ if result['status'] == 'success':
3120
+ print(f"Release {result['version']} ready!")
3121
+
3122
+ # Test without running tests
3123
+ result = release_mgr.apply_release(run_tests=False)
3124
+ """
3125
+ import subprocess
3126
+
3127
+ validate_branch = None
3128
+ original_branch = None
3129
+
3130
+ try:
3131
+ # 1. Detect current development release
3132
+ next_version = self.get_next_release_version()
3133
+ if not next_version:
3134
+ raise ReleaseManagerError(
3135
+ "No development release found.\n"
3136
+ "Create one with: half_orm dev release create <level>"
3137
+ )
3138
+
3139
+ # 2. Validate we're on a release branch
3140
+ original_branch = self._repo.hgit.branch
3141
+ expected_branch = f"ho-release/{next_version}"
3142
+ if original_branch != expected_branch:
3143
+ raise ReleaseManagerError(
3144
+ f"Must be on release branch {expected_branch}\n"
3145
+ f"Currently on: {original_branch}\n"
3146
+ f"Switch with: git checkout {expected_branch}"
3147
+ )
3148
+
3149
+ # 3. Create temporary validation branch
3150
+ validate_branch = f"ho-validate/release-{next_version}"
3151
+
3152
+ # Delete existing validation branch if it exists
3153
+ try:
3154
+ self._repo.hgit.delete_branch(validate_branch, force=True)
3155
+ except Exception:
3156
+ pass # Branch doesn't exist, that's fine
3157
+
3158
+ # Create and checkout validation branch
3159
+ self._repo.hgit.create_branch(validate_branch)
3160
+ self._repo.hgit.checkout(validate_branch)
3161
+
3162
+ # 4. Merge candidate patch branches to simulate future merges
3163
+ # Staged patches are already merged on ho-release, only candidates need merging
3164
+ release_file = ReleaseFile(next_version, self._releases_dir)
3165
+ candidates_merged = []
3166
+ if release_file.exists():
3167
+ candidate_patches = release_file.get_patches(status="candidate")
3168
+ for patch_id in candidate_patches:
3169
+ patch_branch = f"ho-patch/{patch_id}"
3170
+ try:
3171
+ self._repo.hgit.merge(patch_branch)
3172
+ candidates_merged.append(patch_id)
3173
+ except Exception as e:
3174
+ raise ReleaseManagerError(
3175
+ f"Failed to merge candidate branch {patch_branch}: {e}\n"
3176
+ f"Fix merge conflicts on the patch branch first."
3177
+ )
3178
+
3179
+ # 5. Restore database from production schema
3180
+ self._repo.restore_database_from_schema()
3181
+
3182
+ # 6. Get and apply ALL patches (RC + staged + candidates)
3183
+ all_patches = self.get_all_release_patches_for_testing()
3184
+ all_applied_files = []
3185
+
3186
+ for patch_id in all_patches:
3187
+ files = self._repo.patch_manager.apply_patch_files(
3188
+ patch_id, self._repo.model
3189
+ )
3190
+ all_applied_files.extend(files)
3191
+
3192
+ # 7. Generate Python code
3193
+ from half_orm_dev import modules
3194
+ modules.generate(self._repo)
3195
+
3196
+ # 8. Optionally run tests
3197
+ tests_passed = None
3198
+ test_output = None
3199
+
3200
+ if run_tests:
3201
+ try:
3202
+ result = subprocess.run(
3203
+ ['pytest', '-v'],
3204
+ cwd=self._repo.base_dir,
3205
+ capture_output=True,
3206
+ text=True,
3207
+ timeout=600 # 10 minute timeout
3208
+ )
3209
+ tests_passed = result.returncode == 0
3210
+ test_output = result.stdout + result.stderr
3211
+ except subprocess.TimeoutExpired:
3212
+ tests_passed = False
3213
+ test_output = "Tests timed out after 10 minutes"
3214
+ except FileNotFoundError:
3215
+ tests_passed = None
3216
+ test_output = "pytest not found - tests skipped"
3217
+
3218
+ # 9. Cleanup: switch back to original branch and delete temp branch
3219
+ self._repo.hgit.checkout(original_branch)
3220
+ try:
3221
+ self._repo.hgit.delete_branch(validate_branch, force=True)
3222
+ except Exception:
3223
+ pass # Best effort cleanup
3224
+
3225
+ # 10. Return results
3226
+ return {
3227
+ 'version': next_version,
3228
+ 'patches_applied': all_patches,
3229
+ 'candidates_merged': candidates_merged,
3230
+ 'files_applied': all_applied_files,
3231
+ 'tests_passed': tests_passed,
3232
+ 'test_output': test_output,
3233
+ 'status': 'success' if tests_passed is not False else 'failed',
3234
+ 'error': None
3235
+ }
3236
+
3237
+ except ReleaseManagerError:
3238
+ # Cleanup on error
3239
+ self._cleanup_validate_branch(original_branch, validate_branch)
3240
+ raise
3241
+ except Exception as e:
3242
+ # Cleanup on error
3243
+ self._cleanup_validate_branch(original_branch, validate_branch)
3244
+
3245
+ # Restore DB to clean state on failure
3246
+ try:
3247
+ self._repo.restore_database_from_schema()
3248
+ except Exception:
3249
+ pass # Best effort cleanup
3250
+
3251
+ raise ReleaseManagerError(f"Release apply failed: {e}")
half_orm_dev/repo.py CHANGED
@@ -2218,17 +2218,19 @@ See docs/half_orm_dev.md for complete documentation.
2218
2218
 
2219
2219
  def restore_database_from_schema(self) -> None:
2220
2220
  """
2221
- Restore database from model/schema.sql and model/metadata-X.Y.Z.sql.
2221
+ Restore database from model/schema.sql, metadata, and data files.
2222
2222
 
2223
2223
  Restores database to clean production state by dropping all user schemas
2224
- a clean baseline before applying patch files during patch development.
2224
+ and loading schema, metadata, and reference data. Used for from-scratch
2225
+ installations (clone) and patch development (patch apply).
2225
2226
 
2226
2227
  Process:
2227
2228
  1. Verify model/schema.sql exists (file or symlink)
2228
2229
  2. Drop all user schemas with CASCADE (no superuser privileges needed)
2229
2230
  3. Load schema structure from model/schema.sql using psql -f
2230
2231
  4. Load half_orm_meta data from model/metadata-X.Y.Z.sql using psql -f (if exists)
2231
- 5. Reload halfORM Model metadata cache
2232
+ 5. Load reference data from model/data-*.sql files up to current version
2233
+ 6. Reload halfORM Model metadata cache
2232
2234
 
2233
2235
  The method uses DROP SCHEMA CASCADE instead of dropdb/createdb, allowing
2234
2236
  operation without CREATEDB privilege or superuser access. This makes it
@@ -2238,20 +2240,24 @@ See docs/half_orm_dev.md for complete documentation.
2238
2240
  - Accepts model/schema.sql as regular file or symlink
2239
2241
  - Symlink typically points to versioned schema-X.Y.Z.sql file
2240
2242
  - Follows symlink automatically during psql execution
2241
- - Deduces metadata file version from schema.sql symlink target
2242
- - If metadata-X.Y.Z.sql doesn't exist, continues without error (backward compatibility)
2243
+ - Deduces version from schema.sql symlink target for metadata and data files
2244
+ - Missing metadata/data files are silently skipped (backward compatibility)
2245
+
2246
+ Data Files:
2247
+ - model/data-X.Y.Z.sql contains reference data from @HOP:data patches
2248
+ - All data files up to current version are loaded in version order
2249
+ - Example: for version 1.2.0, loads data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql
2243
2250
 
2244
2251
  Error Handling:
2245
2252
  - Raises RepoError if model/schema.sql not found
2246
2253
  - Raises RepoError if schema drop fails
2247
- - Raises RepoError if psql schema load fails
2248
- - Raises RepoError if psql metadata load fails (when file exists)
2254
+ - Raises RepoError if psql schema/metadata/data load fails
2249
2255
  - Database state rolled back on any failure
2250
2256
 
2251
2257
  Usage Context:
2258
+ - Called by clone_repo workflow (from-scratch installation)
2252
2259
  - Called by apply-patch workflow (Step 1: Database Restoration)
2253
- - Ensures clean state before applying patch SQL files
2254
- - Part of isolated patch testing strategy
2260
+ - Ensures clean state with all reference data before applying patches
2255
2261
 
2256
2262
  Returns:
2257
2263
  None
@@ -2263,31 +2269,23 @@ See docs/half_orm_dev.md for complete documentation.
2263
2269
  Examples:
2264
2270
  # Restore database from model/schema.sql before applying patch
2265
2271
  repo.restore_database_from_schema()
2266
- # Database now contains clean production schema + half_orm_meta data
2272
+ # Database now contains: schema + metadata + reference data
2267
2273
 
2268
2274
  # Typical apply-patch workflow
2269
- repo.restore_database_from_schema() # Step 1: Clean state + metadata
2275
+ repo.restore_database_from_schema() # Step 1: Clean state + all data
2270
2276
  patch_mgr.apply_patch_files("456-user-auth", repo.model) # Step 2: Apply patch
2271
2277
 
2272
- # With versioned schema and metadata
2278
+ # With versioned files
2273
2279
  # If schema.sql → schema-1.2.3.sql exists
2274
- # Then metadata-1.2.3.sql is loaded automatically (if it exists)
2275
-
2276
- # Error handling
2277
- try:
2278
- repo.restore_database_from_schema()
2279
- except RepoError as e:
2280
- print(f"Database restoration failed: {e}")
2281
- # Handle error: check schema.sql exists, verify permissions
2280
+ # Then loads: metadata-1.2.3.sql, data-0.1.0.sql, data-1.0.0.sql, data-1.2.3.sql
2282
2281
 
2283
2282
  Notes:
2284
2283
  - Uses DROP SCHEMA CASCADE - no superuser or CREATEDB privilege required
2285
2284
  - Works on cloud databases (AWS RDS, Azure Database, etc.)
2286
2285
  - Uses Model.reconnect(reload=True) to refresh metadata cache
2287
2286
  - Supports both schema.sql file and schema.sql -> schema-X.Y.Z.sql symlink
2288
- - Metadata file is optional (backward compatibility with older schemas)
2287
+ - Metadata and data files are optional (backward compatibility)
2289
2288
  - All PostgreSQL commands use repository connection configuration
2290
- - Version deduction: schema.sql → schema-1.2.3.sql ⇒ metadata-1.2.3.sql
2291
2289
  """
2292
2290
  # 1. Verify model/schema.sql exists
2293
2291
  schema_path = Path(self.model_dir) / "schema.sql"
@@ -2318,15 +2316,16 @@ See docs/half_orm_dev.md for complete documentation.
2318
2316
  self.database.execute_pg_command(
2319
2317
  'psql', '-d', self.name, '-f', str(metadata_path)
2320
2318
  )
2321
- # Optional: Log success (can be removed if too verbose)
2322
- # print(f"✓ Loaded metadata from {metadata_path.name}")
2323
2319
  except Exception as e:
2324
2320
  raise RepoError(
2325
2321
  f"Failed to load metadata from {metadata_path.name}: {e}"
2326
2322
  ) from e
2327
2323
  # else: metadata file doesn't exist, continue without error (backward compatibility)
2328
2324
 
2329
- # 5. Reload half_orm metadata cache
2325
+ # 5. Load data files from model/data-*.sql (all versions up to current)
2326
+ self._load_data_files(schema_path)
2327
+
2328
+ # 6. Reload half_orm metadata cache
2330
2329
  self.model.reconnect(reload=True)
2331
2330
 
2332
2331
  except RepoError:
@@ -2380,6 +2379,76 @@ See docs/half_orm_dev.md for complete documentation.
2380
2379
 
2381
2380
  return metadata_path
2382
2381
 
2382
+ def _load_data_files(self, schema_path: Path) -> None:
2383
+ """
2384
+ Load all data files from model/data-*.sql up to current version.
2385
+
2386
+ Data files contain reference data (DML) from patches with @HOP:data annotation.
2387
+ They are loaded in version order for from-scratch installations.
2388
+
2389
+ Args:
2390
+ schema_path: Path to model/schema.sql (used to deduce current version)
2391
+
2392
+ Process:
2393
+ 1. Deduce current version from schema.sql symlink
2394
+ 2. Find all data-*.sql files in model/
2395
+ 3. Sort by version (semantic versioning)
2396
+ 4. Load each file up to current version using psql -f
2397
+
2398
+ Examples:
2399
+ # schema.sql → schema-1.2.0.sql
2400
+ # model/ contains: data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql, data-2.0.0.sql
2401
+ # Loads: data-0.1.0.sql, data-1.0.0.sql, data-1.2.0.sql (skips 2.0.0)
2402
+ """
2403
+ # Deduce current version from schema.sql symlink
2404
+ if not schema_path.is_symlink():
2405
+ return # No version info, skip data loading
2406
+
2407
+ try:
2408
+ target = Path(os.readlink(schema_path))
2409
+ except OSError:
2410
+ return
2411
+
2412
+ match = re.match(r'schema-(\d+\.\d+\.\d+)\.sql$', target.name)
2413
+ if not match:
2414
+ return
2415
+
2416
+ current_version = match.group(1)
2417
+ current_tuple = tuple(map(int, current_version.split('.')))
2418
+
2419
+ # Find all data files
2420
+ model_dir = schema_path.parent
2421
+ data_files = list(model_dir.glob("data-*.sql"))
2422
+
2423
+ if not data_files:
2424
+ return # No data files to load
2425
+
2426
+ # Parse and sort by version
2427
+ versioned_files = []
2428
+ for data_file in data_files:
2429
+ match = re.match(r'data-(\d+\.\d+\.\d+)\.sql$', data_file.name)
2430
+ if match:
2431
+ version = match.group(1)
2432
+ version_tuple = tuple(map(int, version.split('.')))
2433
+ versioned_files.append((version_tuple, data_file))
2434
+
2435
+ # Sort by version tuple
2436
+ versioned_files.sort(key=lambda x: x[0])
2437
+
2438
+ # Load each file up to current version
2439
+ for version_tuple, data_file in versioned_files:
2440
+ if version_tuple > current_tuple:
2441
+ break # Stop at versions beyond current
2442
+
2443
+ try:
2444
+ self.database.execute_pg_command(
2445
+ 'psql', '-d', self.name, '-f', str(data_file)
2446
+ )
2447
+ except Exception as e:
2448
+ raise RepoError(
2449
+ f"Failed to load data from {data_file.name}: {e}"
2450
+ ) from e
2451
+
2383
2452
  @classmethod
2384
2453
  def clone_repo(cls,
2385
2454
  git_origin: str,
@@ -170,10 +170,11 @@ half_orm dev upgrade <version> # Deploy specific version
170
170
  {package_name}/
171
171
  ├── .hop/ # half-orm-dev metadata
172
172
  │ ├── config # Repository configuration
173
- │ ├── model/ # Database schemas
173
+ │ ├── model/ # Database schemas and data
174
174
  │ │ ├── schema.sql # Current production schema (symlink)
175
175
  │ │ ├── schema-X.Y.Z.sql # Versioned schemas
176
- │ │ └── metadata-X.Y.Z.sql # half_orm_meta data dumps
176
+ │ │ ├── metadata-X.Y.Z.sql # half_orm_meta data dumps
177
+ │ │ └── data-X.Y.Z.sql # Reference data from @HOP:data patches
177
178
  │ └── releases/ # Release tracking files
178
179
  │ ├── X.Y.Z-patches.toml # Development releases (mutable)
179
180
  │ ├── X.Y.Z-rcN.txt # Release candidates (immutable)
@@ -199,6 +200,39 @@ All development happens on patch branches, merged into release branches, then pr
199
200
 
200
201
  ---
201
202
 
203
+ ## 💾 Data Persistence (@HOP:data)
204
+
205
+ For reference data that must be loaded with every database installation (lookup tables, default roles, etc.), use the `@HOP:data` annotation:
206
+
207
+ ```sql
208
+ -- @HOP:data
209
+ -- This file will be included in model/data-X.Y.Z.sql
210
+
211
+ INSERT INTO roles (name, description)
212
+ VALUES ('admin', 'Administrator')
213
+ ON CONFLICT (name) DO NOTHING;
214
+
215
+ INSERT INTO permissions (name)
216
+ VALUES ('read'), ('write'), ('delete')
217
+ ON CONFLICT DO NOTHING;
218
+ ```
219
+
220
+ ### How it works
221
+
222
+ 1. **In patches**: SQL files starting with `-- @HOP:data` contain reference data
223
+ 2. **Production promote**: All `@HOP:data` files are consolidated into `model/data-X.Y.Z.sql`
224
+ 3. **Clone/Restore**: Data files are loaded automatically after schema restoration
225
+ 4. **Production upgrade**: Data is inserted via normal patch application (no special handling)
226
+
227
+ ### Best practices
228
+
229
+ - Use `ON CONFLICT DO NOTHING` or `ON CONFLICT DO UPDATE` for idempotency
230
+ - Keep data files small and focused (one concern per file)
231
+ - Number your SQL files to control execution order: `01_roles.sql`, `02_permissions.sql`
232
+ - Only use for **reference data**, not user-generated data
233
+
234
+ ---
235
+
202
236
  ## 📚 Documentation
203
237
 
204
238
  - **half-orm-dev**: https://github.com/half-orm/half-orm-dev
half_orm_dev/version.txt CHANGED
@@ -1 +1 @@
1
- 0.17.3-a4
1
+ 0.17.3-a6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: half_orm_dev
3
- Version: 0.17.3a4
3
+ Version: 0.17.3a6
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,17 +1,17 @@
1
1
  half_orm_dev/__init__.py,sha256=0JpUPey1gacxXuIFGcpD2nTGso73fkak72qzTHttAJk,18
2
2
  half_orm_dev/cli_extension.py,sha256=kwX3M11_rwr0pFcqHK_bpI3Pp4ztfTCVz2gLfTmzfeA,1066
3
- half_orm_dev/database.py,sha256=aRfCvxolJPN0kQUoEleaVjZAnyiWo-dEXq8xkKYC8pM,60088
3
+ half_orm_dev/database.py,sha256=0sq96tseMUWAluoxoxwbTIJrzvBYMzW163D8Ssdj73g,62793
4
4
  half_orm_dev/decorators.py,sha256=JKv_Z_JZUr-s-Vz551temHZhhecPfbvyhTbByRDjVAQ,4901
5
5
  half_orm_dev/hgit.py,sha256=VdzCCQ__xG1IGJaGq4-rrhbA1bNkDw_dBqkUNIeTONg,58045
6
6
  half_orm_dev/migration_manager.py,sha256=9RpciH8nyQrF0xV31kAeaYKkQl24Di1VHt-mAjjHhzM,14854
7
7
  half_orm_dev/modules.py,sha256=4jfVb2yboRgb9mcO0sMF-iLigcZFTHEm4VRLN6GQXM4,16796
8
- half_orm_dev/patch_manager.py,sha256=rq0C1W6sXExMsgLwIXr7nwhpcRW28z5l5Tjy-vsqvwE,100973
8
+ half_orm_dev/patch_manager.py,sha256=HAnQR4m8E0hyKCpPme_XiHI4K7qFkCT2DvSgCaa524s,100993
9
9
  half_orm_dev/patch_validator.py,sha256=QNe1L6k_xwsnrOTcb3vkW2D0LbqrCRcZOGPnVyspVRk,10871
10
10
  half_orm_dev/release_file.py,sha256=0c9NBhAQ6YpiC3HWj8VtZcfvvZxW2ITk1NEQ60AO0sI,9880
11
- half_orm_dev/release_manager.py,sha256=wCJUKLCDkOgbytqSBaU_NArLcJZPVnwP2W5UiRMwz1Y,113367
12
- half_orm_dev/repo.py,sha256=GOgJnDRa1g5sAVX1xi00ff90tF9L8e0S_KhCc3SoCZ0,95685
11
+ half_orm_dev/release_manager.py,sha256=AXjjtIWAdv0yy6DkP_eGYKqDDvejmbxnz_QiMRLvwws,125510
12
+ half_orm_dev/repo.py,sha256=h-nsB6z2xph9ortO02g9DcPXeef9Rk2D5WI3Yc3h1M4,98253
13
13
  half_orm_dev/utils.py,sha256=M3yViUFfsO7Cp9MYSoUSkCZ6R9w_4jW45UDZUOT8FhI,1493
14
- half_orm_dev/version.txt,sha256=EHE9HXre16bBNUi8QiB6DlSqUvIFkJnA01jyvO9CKrM,10
14
+ half_orm_dev/version.txt,sha256=ODC9rbINfRfZYa57a3OOULq415Mxf7YFomwPl96Qm3M,10
15
15
  half_orm_dev/cli/__init__.py,sha256=0CbMj8OIhZmglWakK7NhYPn302erUTEg2VHOdm1hRTQ,163
16
16
  half_orm_dev/cli/main.py,sha256=3SVTl5WraNTSY6o7LfvE1dUHKg_RcuVaHHDIn_oINv4,11701
17
17
  half_orm_dev/cli/commands/__init__.py,sha256=UhWf0AnWqy4gyFo2SJQv8pL_YJ43pE_c9TgopcjzKDg,1490
@@ -21,7 +21,7 @@ half_orm_dev/cli/commands/clone.py,sha256=JUDDt-vz_WvGkm5HDFuZ3KZbclLyPaE4h665n8
21
21
  half_orm_dev/cli/commands/init.py,sha256=N0TXUL1ExW-DdpNrs4xiXymtSHHLh5fCbPMTBjw2Iwg,12548
22
22
  half_orm_dev/cli/commands/migrate.py,sha256=iEz3DoFX22WwaYDo_WUKaF-pFohaLWoUrDmdLCin2wc,4047
23
23
  half_orm_dev/cli/commands/patch.py,sha256=sl8mv1mlq-KnKItFV_HBfZ93G5CbdviQRIAYocuMDDo,12810
24
- half_orm_dev/cli/commands/release.py,sha256=Ywsp4FBDGSHkss3RYLb52SzhGP8x4RsFUbppHXKQvdk,14736
24
+ half_orm_dev/cli/commands/release.py,sha256=yPp_3NaukhZF3smo6zNsFKtxHwB2RgZnUlomdlM6Sno,17574
25
25
  half_orm_dev/cli/commands/restore.py,sha256=n9SP8n1EQUduvDoA0qxpSUQpphc48X-NovnocyGl98I,236
26
26
  half_orm_dev/cli/commands/sync.py,sha256=D0Prr8W1ySYjP3D8H4MB05KHccFbhB8z2qB3Bs00swA,274
27
27
  half_orm_dev/cli/commands/todo.py,sha256=kL5QU-IjPWmnrKG8L4qk1vb5PDZfY88EFExICiNeLhA,2981
@@ -38,7 +38,7 @@ half_orm_dev/patches/sql/half_orm_meta.sql,sha256=Vl2YzEWpWdam-tC0ZE8iNMeTRzEHpx
38
38
  half_orm_dev/templates/.gitignore,sha256=RmvQ9D46T9vpRxhYjjY5WUjGVbuyFUMsH059wC7sPBM,140
39
39
  half_orm_dev/templates/MANIFEST.in,sha256=53BeBuKi8UtBWB6IG3VQZk9Ow8Iye6Zs14sP-gVyVDA,25
40
40
  half_orm_dev/templates/Pipfile,sha256=u3lGJSk5HZwz-EOTrOdBYrkhGV6zgVtrrRPivrO5rmA,182
41
- half_orm_dev/templates/README,sha256=wE7X1-FB_n8c8cEkTkaPSivszTEK5t1eYkbKh0-M9zI,6293
41
+ half_orm_dev/templates/README,sha256=YgNl52Jk7jvwTaB7fLgrXHxOtCECQEl8AqgvImrzaWA,7522
42
42
  half_orm_dev/templates/conftest_template,sha256=DopLw67b5cptCYUtmAcQzr5Gz_kzNwpMO6r3goihiks,1206
43
43
  half_orm_dev/templates/init_module_template,sha256=o3RAnhGayYUF7NEyI8bcI6JHmAZb2wPVNF-FdrjOnQU,345
44
44
  half_orm_dev/templates/module_template_1,sha256=hRa0PiI6-dpBKNXJ9PuDuGocdrq712ujlSJGfJcXOh8,271
@@ -50,9 +50,9 @@ half_orm_dev/templates/sql_adapter,sha256=kAP5y7Qml3DKsbZLUeoVpeXjbQcWltHjkDznED
50
50
  half_orm_dev/templates/warning,sha256=4hlZ_rRdpmkXxOeRoVd9xnXBARYXn95e-iXrD1f2u7k,490
51
51
  half_orm_dev/templates/git-hooks/pre-commit,sha256=Hf084pqeiOebrv4xzA0aiaHbIXswmmNO-dSIXUfzMK0,4707
52
52
  half_orm_dev/templates/git-hooks/prepare-commit-msg,sha256=zknOGGoaWKC97zfga2Xl2i_psnNo9MJbrEBuN91eHNw,1070
53
- half_orm_dev-0.17.3a4.dist-info/licenses/AUTHORS,sha256=eWxqzRdLOt2gX0FMQj_wui03Od3jdlwa8xNe9tl84g0,113
54
- half_orm_dev-0.17.3a4.dist-info/licenses/LICENSE,sha256=ufhxlSi6mttkGQTsGWrEoB3WA_fCPJ6-k07GSVBgyPw,644
55
- half_orm_dev-0.17.3a4.dist-info/METADATA,sha256=XZ8XlqVBMYhf9Z-PBBfUWGKkvTUwhtWnir80HpWi8RA,16149
56
- half_orm_dev-0.17.3a4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- half_orm_dev-0.17.3a4.dist-info/top_level.txt,sha256=M5hEsWfn5Kw0HL-VnNmS6Jw-3cwRyjims5a8cr18eTM,13
58
- half_orm_dev-0.17.3a4.dist-info/RECORD,,
53
+ half_orm_dev-0.17.3a6.dist-info/licenses/AUTHORS,sha256=eWxqzRdLOt2gX0FMQj_wui03Od3jdlwa8xNe9tl84g0,113
54
+ half_orm_dev-0.17.3a6.dist-info/licenses/LICENSE,sha256=ufhxlSi6mttkGQTsGWrEoB3WA_fCPJ6-k07GSVBgyPw,644
55
+ half_orm_dev-0.17.3a6.dist-info/METADATA,sha256=POjMIdgH4C38nJeXy2VOtLR7jknjFrhhUmfrSCyimbQ,16149
56
+ half_orm_dev-0.17.3a6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
57
+ half_orm_dev-0.17.3a6.dist-info/top_level.txt,sha256=M5hEsWfn5Kw0HL-VnNmS6Jw-3cwRyjims5a8cr18eTM,13
58
+ half_orm_dev-0.17.3a6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5