half-orm-dev 0.17.3a5__py3-none-any.whl → 0.17.3a7__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.
@@ -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
 
@@ -2291,10 +2335,18 @@ class ReleaseManager:
2291
2335
  # 1. Apply patches to database (for validation)
2292
2336
  self._apply_release_patches(version)
2293
2337
 
2294
- # 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
2295
2347
  self._repo.hgit.checkout(release_branch)
2296
2348
 
2297
- # 3. Create RC tag on release branch
2349
+ # 4. Create RC tag on release branch
2298
2350
  self._repo.hgit.create_tag(rc_tag, f"Release Candidate %{version}")
2299
2351
 
2300
2352
  # Push tag
@@ -2310,14 +2362,10 @@ class ReleaseManager:
2310
2362
 
2311
2363
  # Keep TOML file for continued development (don't delete it)
2312
2364
 
2313
- # Generate data-X.Y.Z-rcN.sql if any patches have @HOP:data files
2314
- rc_patches = self.read_release_patches(rc_file.name)
2315
- data_file = self._generate_data_sql_file(
2316
- rc_patches,
2317
- f"data-{version}-rc{rc_number}.sql"
2318
- )
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
2319
2367
 
2320
- # Commit RC snapshot and data file (both in .hop/releases/)
2368
+ # Commit RC snapshot (in .hop/releases/)
2321
2369
  # This also syncs .hop/ to all active branches automatically
2322
2370
  self._repo.commit_and_sync_to_active_branches(
2323
2371
  message=f"[HOP] Promote release %{version} to RC {rc_number}"
@@ -2474,6 +2522,11 @@ class ReleaseManager:
2474
2522
  # The database should already be in the correct state from RC
2475
2523
  pass
2476
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
+
2477
2530
  # Generate schema dump for this production version
2478
2531
  model_dir = Path(self._repo.model_dir)
2479
2532
  self._repo.database._generate_schema_sql(version, model_dir)
@@ -2489,13 +2542,12 @@ class ReleaseManager:
2489
2542
  if toml_file.exists():
2490
2543
  toml_file.unlink()
2491
2544
 
2492
- # Generate data-X.Y.Z.sql if any patches have @HOP:data files
2493
- # 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)
2494
2547
  prod_patches = self.read_release_patches(prod_file.name)
2495
- self._generate_data_sql_file(
2496
- prod_patches,
2497
- f"data-{version}.sql"
2498
- )
2548
+ data_file = self._generate_data_sql_file(prod_patches, version)
2549
+ if data_file:
2550
+ self._repo.hgit.add(str(data_file))
2499
2551
 
2500
2552
  self._repo.commit_and_sync_to_active_branches(
2501
2553
  message=f"[HOP] Promote release %{version} to production",
@@ -2910,14 +2962,12 @@ class ReleaseManager:
2910
2962
  if toml_file.exists():
2911
2963
  self._repo.hgit.rm(str(toml_file))
2912
2964
 
2913
- # Generate data-X.Y.Z-hotfixN.sql if any patches have @HOP:data files
2914
- hotfix_patches = self.read_release_patches(hotfix_file.name)
2915
- data_file = self._generate_data_sql_file(
2916
- hotfix_patches,
2917
- f"data-{version}-hotfix{hotfix_num}.sql"
2918
- )
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)
2919
2969
  if data_file:
2920
- self._repo.hgit.add(str(data_file)) # Add data file if generated
2970
+ self._repo.hgit.add(str(data_file))
2921
2971
 
2922
2972
  # 6. Apply release patches and generate SQL dumps
2923
2973
  self._apply_release_patches(version, True)
@@ -2954,3 +3004,248 @@ class ReleaseManager:
2954
3004
  raise
2955
3005
  except Exception as e:
2956
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}")