half-orm-dev 0.17.3a7__py3-none-any.whl → 0.17.3a9__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.
half_orm_dev/database.py CHANGED
@@ -1224,126 +1224,12 @@ class Database:
1224
1224
 
1225
1225
  return complete_params
1226
1226
 
1227
- @classmethod
1228
- def _load_configuration(cls, database_name):
1229
- """
1230
- Load existing database configuration file, replacing DbConn functionality.
1231
-
1232
- Reads halfORM configuration file and returns connection parameters as a dictionary.
1233
- This method completely replaces DbConn.__init() logic, supporting both minimal
1234
- configurations (PostgreSQL trust mode) and complete parameter sets.
1235
-
1236
- Args:
1237
- database_name (str): Name of the database to load configuration for
1238
-
1239
- Returns:
1240
- dict | None: Connection parameters dictionary with standardized keys:
1241
- - name (str): Database name (always present)
1242
- - user (str): Database user (defaults to $USER environment variable)
1243
- - password (str): Database password (empty string if not set)
1244
- - host (str): Database host (empty string for Unix socket, 'localhost' otherwise)
1245
- - port (int): Database port (5432 if not specified)
1246
- - production (bool): Production environment flag (defaults to False)
1247
- Returns None if configuration file doesn't exist.
1248
-
1249
- Raises:
1250
- FileNotFoundError: If CONF_DIR doesn't exist or isn't accessible
1251
- PermissionError: If configuration file exists but isn't readable
1252
- ValueError: If configuration file format is invalid or corrupted
1253
-
1254
- Examples:
1255
- # Complete configuration file
1256
- config = Database._load_configuration("production_db")
1257
- # Returns: {'name': 'production_db', 'user': 'app_user', 'password': 'secret',
1258
- # 'host': 'db.company.com', 'port': 5432, 'production': True}
1259
-
1260
- # Minimal trust mode configuration (only name=database_name)
1261
- config = Database._load_configuration("local_dev")
1262
- # Returns: {'name': 'local_dev', 'user': 'joel', 'password': '',
1263
- # 'host': '', 'port': 5432, 'production': False}
1264
-
1265
- # Non-existent configuration
1266
- config = Database._load_configuration("unknown_db")
1267
- # Returns: None
1268
-
1269
- Migration Notes:
1270
- - Completely replaces DbConn.__init() and DbConn.__init logic
1271
- - Maintains backward compatibility with existing config files
1272
- - Standardizes return format (int for port, bool for production)
1273
- - Integrates PostgreSQL trust mode defaults directly into Database class
1274
- - Eliminates external DbConn dependency while preserving all functionality
1275
- """
1276
- from half_orm.model import CONF_DIR
1277
-
1278
- # Check if configuration directory exists
1279
- if not os.path.exists(CONF_DIR):
1280
- raise FileNotFoundError(f"Configuration directory {CONF_DIR} doesn't exist")
1281
-
1282
- # Build configuration file path
1283
- config_file = os.path.join(CONF_DIR, database_name)
1284
-
1285
- # Return None if configuration file doesn't exist
1286
- if not os.path.exists(config_file):
1287
- return None
1288
-
1289
- # Check if file is readable before attempting to parse
1290
- if not os.access(config_file, os.R_OK):
1291
- raise PermissionError(f"Configuration file {config_file} is not readable")
1292
-
1293
- # Read configuration file
1294
- config = ConfigParser()
1295
- try:
1296
- config.read(config_file)
1297
- except Exception as e:
1298
- raise ValueError(f"Configuration file format is invalid: {e}")
1299
-
1300
- # Check if [database] section exists
1301
- if not config.has_section('database'):
1302
- raise ValueError("Configuration file format is invalid: missing [database] section")
1303
-
1304
- # Extract configuration values with PostgreSQL defaults
1305
- try:
1306
- name = config.get('database', 'name')
1307
- user = config.get('database', 'user', fallback=os.environ.get('USER', ''))
1308
- password = config.get('database', 'password', fallback='')
1309
- host = config.get('database', 'host', fallback='')
1310
- port_str = config.get('database', 'port', fallback='')
1311
- production_str = config.get('database', 'production', fallback='False')
1312
- docker_container = config.get('database', 'docker_container', fallback='')
1313
-
1314
- # Convert port to int (default 5432 if empty)
1315
- if port_str == '':
1316
- port = 5432
1317
- else:
1318
- port = int(port_str)
1319
-
1320
- # Convert production to bool
1321
- production = config.getboolean('database', 'production', fallback=False)
1322
-
1323
- return {
1324
- 'name': name,
1325
- 'user': user,
1326
- 'password': password,
1327
- 'host': host,
1328
- 'port': port,
1329
- 'production': production,
1330
- 'docker_container': docker_container
1331
- }
1332
-
1333
- except (ValueError, TypeError) as e:
1334
- raise ValueError(f"Configuration file format is invalid: {e}")
1335
-
1336
1227
  def _get_connection_params(self):
1337
1228
  """
1338
1229
  Get current connection parameters for this database instance.
1339
1230
 
1340
- Returns the connection parameters dictionary for this Database instance,
1341
- replacing direct access to DbConn properties. This method serves as the
1342
- unified interface for accessing connection parameters during the migration
1343
- from DbConn to integrated Database functionality.
1344
-
1345
- Uses instance-level caching to avoid repeated file reads within the same
1346
- Database instance lifecycle.
1231
+ Returns the connection parameters dictionary using Model._dbinfo,
1232
+ which is already loaded by half_orm from the configuration file.
1347
1233
 
1348
1234
  Returns:
1349
1235
  dict: Connection parameters dictionary with standardized keys:
@@ -1353,59 +1239,37 @@ class Database:
1353
1239
  - host (str): Database host (empty string for Unix socket)
1354
1240
  - port (int): Database port (5432 default)
1355
1241
  - production (bool): Production environment flag
1356
- Returns dict with defaults if no configuration exists or errors occur.
1357
-
1358
- Examples:
1359
- # Get connection parameters for existing database instance
1360
- db = Database(repo)
1361
- params = db._get_connection_params()
1362
- # Returns: {'name': 'my_db', 'user': 'dev', 'password': '',
1363
- # 'host': 'localhost', 'port': 5432, 'production': False}
1364
-
1365
- # Access specific parameters (replaces DbConn.property access)
1366
- user = db._get_connection_params()['user'] # replaces self.__connection_params.user
1367
- host = db._get_connection_params()['host'] # replaces self.__connection_params.host
1368
- prod = db._get_connection_params()['production'] # replaces self.__connection_params.production
1369
-
1370
- Implementation Notes:
1371
- - Uses _load_configuration() internally but handles all exceptions
1372
- - Provides stable interface - never raises exceptions
1373
- - Returns sensible defaults if configuration is missing/invalid
1374
- - Serves as protective wrapper around _load_configuration()
1375
- - Exceptions from _load_configuration() are caught and handled gracefully
1376
- - Uses instance-level cache to avoid repeated file reads
1377
-
1378
- Migration Notes:
1379
- - Replaces self.__connection_params.user, .host, .port, .production access
1380
- - Serves as transition method during DbConn elimination
1381
- - Maintains compatibility with existing Database instance usage patterns
1382
- - Will be used by state, production, and execute_pg_command properties
1242
+ - docker_container (str): Docker container name (if configured)
1383
1243
  """
1384
1244
  # Return cached parameters if already loaded
1385
1245
  if hasattr(self, '_Database__connection_params_cache') and self.__connection_params_cache is not None:
1386
1246
  return self.__connection_params_cache
1387
1247
 
1388
- # Load configuration with defaults
1248
+ # Use connection info from Model._dbinfo (already loaded by half_orm)
1249
+ if self.__model is not None and hasattr(self.__model, '_dbinfo'):
1250
+ dbinfo = self.__model._dbinfo
1251
+ config = {
1252
+ 'name': self.__repo.name,
1253
+ 'user': dbinfo.get('user', os.environ.get('USER', '')),
1254
+ 'password': dbinfo.get('password', ''),
1255
+ 'host': dbinfo.get('host', ''),
1256
+ 'port': int(dbinfo.get('port', 5432) or 5432),
1257
+ 'production': not self.__model._production_mode, # devel=False means production
1258
+ 'docker_container': dbinfo.get('docker_container', ''),
1259
+ }
1260
+ self.__connection_params_cache = config
1261
+ return config
1262
+
1263
+ # Fallback: defaults (should not happen in normal usage)
1389
1264
  config = {
1390
1265
  'name': self.__repo.name,
1391
1266
  'user': os.environ.get('USER', ''),
1392
1267
  'password': '',
1393
1268
  'host': '',
1394
1269
  'port': 5432,
1395
- 'production': False
1270
+ 'production': False,
1271
+ 'docker_container': '',
1396
1272
  }
1397
-
1398
- try:
1399
- # Try to load configuration for this database
1400
- loaded_config = self._load_configuration(self.__repo.name)
1401
- if loaded_config is not None:
1402
- config = loaded_config
1403
- except (FileNotFoundError, PermissionError, ValueError):
1404
- # Handle all possible exceptions from _load_configuration gracefully
1405
- # Return sensible defaults to maintain stable interface
1406
- pass
1407
-
1408
- # Cache the result for subsequent calls
1409
1273
  self.__connection_params_cache = config
1410
1274
  return config
1411
1275
 
@@ -0,0 +1,204 @@
1
+ """
2
+ Migration: Convert TOML patches to dict format with merge_commit.
3
+
4
+ This migration converts the old TOML format:
5
+ [patches]
6
+ "1-auth" = "staged"
7
+ "2-api" = "candidate"
8
+
9
+ To the new dict format:
10
+ [patches]
11
+ "1-auth" = { status = "staged", merge_commit = "abc123de" }
12
+ "2-api" = { status = "candidate" }
13
+
14
+ For staged patches, the merge_commit hash is retrieved from git history.
15
+ """
16
+
17
+ from pathlib import Path
18
+ import subprocess
19
+ import sys
20
+
21
+ try:
22
+ import tomli
23
+ except ImportError:
24
+ import tomllib as tomli
25
+
26
+ try:
27
+ import tomli_w
28
+ except ImportError:
29
+ raise ImportError(
30
+ "tomli_w is required for this migration. "
31
+ "Install it with: pip install tomli_w"
32
+ )
33
+
34
+
35
+ def get_description():
36
+ """Return migration description."""
37
+ return "Convert TOML patches to dict format with merge_commit"
38
+
39
+
40
+ def find_merge_commit(repo, patch_id: str, version: str) -> str:
41
+ """
42
+ Find the merge commit hash for a staged patch.
43
+
44
+ Searches git history for the commit that merged the patch branch
45
+ into the release branch.
46
+
47
+ Args:
48
+ repo: Repo instance
49
+ patch_id: Patch identifier (e.g., "456-user-auth")
50
+ version: Release version (e.g., "0.17.0")
51
+
52
+ Returns:
53
+ Commit hash (8 characters) or empty string if not found
54
+ """
55
+ release_branch = f"ho-release/{version}"
56
+
57
+ try:
58
+ # Search for merge commit message pattern
59
+ # Pattern: [HOP] Merge #PATCH_ID into %"VERSION"
60
+ result = subprocess.run(
61
+ ['git', 'log', '--all', '--grep', f'Merge #{patch_id}',
62
+ '--format=%H', '-n', '1'],
63
+ cwd=repo.base_dir,
64
+ capture_output=True,
65
+ text=True,
66
+ check=True
67
+ )
68
+
69
+ commit_hash = result.stdout.strip()
70
+ if commit_hash:
71
+ return commit_hash[:8]
72
+
73
+ # Fallback: search for the move to stage commit
74
+ # Pattern: [HOP] move patch #PATCH_ID from candidate to stage
75
+ result = subprocess.run(
76
+ ['git', 'log', '--all', '--grep', f'move patch #{patch_id}',
77
+ '--format=%H', '-n', '1'],
78
+ cwd=repo.base_dir,
79
+ capture_output=True,
80
+ text=True,
81
+ check=True
82
+ )
83
+
84
+ commit_hash = result.stdout.strip()
85
+ if commit_hash:
86
+ # Get the parent commit (the merge commit is the one before the move)
87
+ result = subprocess.run(
88
+ ['git', 'rev-parse', f'{commit_hash}^'],
89
+ cwd=repo.base_dir,
90
+ capture_output=True,
91
+ text=True,
92
+ check=True
93
+ )
94
+ parent_hash = result.stdout.strip()
95
+ if parent_hash:
96
+ return parent_hash[:8]
97
+
98
+ except subprocess.CalledProcessError:
99
+ pass
100
+
101
+ return ""
102
+
103
+
104
+ def migrate(repo):
105
+ """
106
+ Execute migration: Convert TOML patches to dict format.
107
+
108
+ For each X.Y.Z-patches.toml file:
109
+ 1. Read current content
110
+ 2. Check if already in dict format
111
+ 3. Convert to dict format:
112
+ - candidates: { status = "candidate" }
113
+ - staged: { status = "staged", merge_commit = "..." }
114
+ 4. Find merge_commit from git history for staged patches
115
+ 5. Write updated TOML file
116
+
117
+ Args:
118
+ repo: Repo instance
119
+ """
120
+ print("Migrating TOML patches to dict format with merge_commit...")
121
+
122
+ releases_dir = Path(repo.releases_dir)
123
+ if not releases_dir.exists():
124
+ print(" No releases directory found, skipping migration.")
125
+ return
126
+
127
+ # Find all TOML patches files
128
+ toml_files = list(releases_dir.glob("*-patches.toml"))
129
+
130
+ if not toml_files:
131
+ print(" No TOML patches files found, skipping migration.")
132
+ return
133
+
134
+ migrated_count = 0
135
+
136
+ for toml_file in toml_files:
137
+ # Extract version from filename
138
+ version = toml_file.stem.replace('-patches', '')
139
+
140
+ print(f" Processing {version}...")
141
+
142
+ try:
143
+ # Read current TOML content
144
+ with toml_file.open('rb') as f:
145
+ data = tomli.load(f)
146
+
147
+ patches = data.get("patches", {})
148
+
149
+ if not patches:
150
+ print(f" No patches in {version}, skipping")
151
+ continue
152
+
153
+ # Check if already in dict format
154
+ first_value = next(iter(patches.values()))
155
+ if isinstance(first_value, dict):
156
+ print(f" Already in dict format, skipping")
157
+ continue
158
+
159
+ # Convert to dict format
160
+ new_patches = {}
161
+ staged_without_commit = []
162
+
163
+ for patch_id, status in patches.items():
164
+ if status == "candidate":
165
+ new_patches[patch_id] = {"status": "candidate"}
166
+ elif status == "staged":
167
+ # Find merge commit from git history
168
+ merge_commit = find_merge_commit(repo, patch_id, version)
169
+ if merge_commit:
170
+ new_patches[patch_id] = {
171
+ "status": "staged",
172
+ "merge_commit": merge_commit
173
+ }
174
+ else:
175
+ # No merge_commit found, store without it
176
+ new_patches[patch_id] = {"status": "staged"}
177
+ staged_without_commit.append(patch_id)
178
+ else:
179
+ # Unknown status, preserve as-is in dict format
180
+ new_patches[patch_id] = {"status": status}
181
+
182
+ # Update data and write
183
+ data["patches"] = new_patches
184
+
185
+ with toml_file.open('wb') as f:
186
+ tomli_w.dump(data, f)
187
+
188
+ print(f" Converted {len(patches)} patch(es)")
189
+ if staged_without_commit:
190
+ print(f" Warning: No merge_commit found for: {', '.join(staged_without_commit)}",
191
+ file=sys.stderr)
192
+
193
+ migrated_count += 1
194
+
195
+ except Exception as e:
196
+ print(f" Error processing {version}: {e}", file=sys.stderr)
197
+ continue
198
+
199
+ repo.hgit.add('.hop')
200
+
201
+ if migrated_count > 0:
202
+ print(f"\nMigration complete: {migrated_count} file(s) converted to dict format")
203
+ else:
204
+ print("\nNo files needed migration")