pyconvexity 0.3.8.post7__py3-none-any.whl → 0.4.1__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.
Files changed (48) hide show
  1. pyconvexity/__init__.py +87 -46
  2. pyconvexity/_version.py +1 -1
  3. pyconvexity/core/__init__.py +3 -5
  4. pyconvexity/core/database.py +111 -103
  5. pyconvexity/core/errors.py +16 -10
  6. pyconvexity/core/types.py +61 -54
  7. pyconvexity/data/__init__.py +0 -1
  8. pyconvexity/data/loaders/cache.py +65 -64
  9. pyconvexity/data/schema/01_core_schema.sql +134 -234
  10. pyconvexity/data/schema/02_data_metadata.sql +38 -168
  11. pyconvexity/data/schema/03_validation_data.sql +327 -264
  12. pyconvexity/data/sources/gem.py +169 -139
  13. pyconvexity/io/__init__.py +4 -10
  14. pyconvexity/io/excel_exporter.py +694 -480
  15. pyconvexity/io/excel_importer.py +817 -545
  16. pyconvexity/io/netcdf_exporter.py +66 -61
  17. pyconvexity/io/netcdf_importer.py +850 -619
  18. pyconvexity/models/__init__.py +109 -59
  19. pyconvexity/models/attributes.py +197 -178
  20. pyconvexity/models/carriers.py +70 -67
  21. pyconvexity/models/components.py +260 -236
  22. pyconvexity/models/network.py +202 -284
  23. pyconvexity/models/results.py +65 -55
  24. pyconvexity/models/scenarios.py +58 -88
  25. pyconvexity/solvers/__init__.py +5 -5
  26. pyconvexity/solvers/pypsa/__init__.py +3 -3
  27. pyconvexity/solvers/pypsa/api.py +150 -134
  28. pyconvexity/solvers/pypsa/batch_loader.py +165 -162
  29. pyconvexity/solvers/pypsa/builder.py +390 -291
  30. pyconvexity/solvers/pypsa/constraints.py +184 -162
  31. pyconvexity/solvers/pypsa/solver.py +968 -666
  32. pyconvexity/solvers/pypsa/storage.py +1377 -671
  33. pyconvexity/timeseries.py +63 -60
  34. pyconvexity/validation/__init__.py +14 -6
  35. pyconvexity/validation/rules.py +95 -84
  36. pyconvexity-0.4.1.dist-info/METADATA +46 -0
  37. pyconvexity-0.4.1.dist-info/RECORD +42 -0
  38. pyconvexity/data/__pycache__/__init__.cpython-313.pyc +0 -0
  39. pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc +0 -0
  40. pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc +0 -0
  41. pyconvexity/data/schema/04_scenario_schema.sql +0 -122
  42. pyconvexity/data/schema/migrate_add_geometries.sql +0 -73
  43. pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc +0 -0
  44. pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc +0 -0
  45. pyconvexity-0.3.8.post7.dist-info/METADATA +0 -138
  46. pyconvexity-0.3.8.post7.dist-info/RECORD +0 -49
  47. {pyconvexity-0.3.8.post7.dist-info → pyconvexity-0.4.1.dist-info}/WHEEL +0 -0
  48. {pyconvexity-0.3.8.post7.dist-info → pyconvexity-0.4.1.dist-info}/top_level.txt +0 -0
@@ -11,484 +11,402 @@ import logging
11
11
  from typing import Dict, Any, Optional, List
12
12
  from datetime import datetime, timezone
13
13
 
14
- from pyconvexity.core.types import (
15
- CreateNetworkRequest, TimePeriod, Network
16
- )
17
- from pyconvexity.core.errors import (
18
- ValidationError, DatabaseError
19
- )
14
+ from pyconvexity.core.types import CreateNetworkRequest, TimePeriod, Network
15
+ from pyconvexity.core.errors import ValidationError, DatabaseError
20
16
 
21
17
  logger = logging.getLogger(__name__)
22
18
 
23
19
 
24
- def create_network(conn: sqlite3.Connection, request: CreateNetworkRequest) -> int:
20
+ def create_network(conn: sqlite3.Connection, request: CreateNetworkRequest) -> None:
25
21
  """
26
- Create a network record and return network ID.
27
-
22
+ Create network metadata (single network per database).
23
+
28
24
  Args:
29
25
  conn: Database connection
30
26
  request: Network creation request
31
-
32
- Returns:
33
- ID of the newly created network
34
-
27
+
35
28
  Raises:
36
29
  ValidationError: If required fields are missing
37
30
  DatabaseError: If creation fails
38
31
  """
39
-
32
+
40
33
  # Validate required fields
41
34
  if not request.start_time:
42
35
  raise ValidationError("start_time is required")
43
36
  if not request.end_time:
44
37
  raise ValidationError("end_time is required")
45
-
46
- cursor = conn.execute("""
47
- INSERT INTO networks (name, description, time_start, time_end, time_interval, created_at, updated_at)
38
+
39
+ # Insert into network_metadata table (single row per database)
40
+ conn.execute(
41
+ """
42
+ INSERT INTO network_metadata (name, description, time_start, time_end, time_interval, created_at, updated_at)
48
43
  VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
49
- """, (
50
- request.name,
51
- request.description or f"Created on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
52
- request.start_time,
53
- request.end_time,
54
- request.time_resolution or 'H'
55
- ))
56
-
57
- network_id = cursor.lastrowid
58
- if not network_id:
59
- raise DatabaseError("Failed to create network")
60
-
61
- # Master scenario is automatically created by database trigger
62
- # No need to call create_master_scenario() manually
63
-
64
- return network_id
65
-
66
-
67
- def get_network_info(conn: sqlite3.Connection, network_id: int) -> Dict[str, Any]:
44
+ """,
45
+ (
46
+ request.name,
47
+ request.description
48
+ or f"Created on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
49
+ request.start_time,
50
+ request.end_time,
51
+ request.time_resolution or "PT1H", # ISO 8601 duration format
52
+ ),
53
+ )
54
+
55
+
56
+ def get_network_info(conn: sqlite3.Connection) -> Dict[str, Any]:
68
57
  """
69
- Get network information.
70
-
58
+ Get network information (single network per database).
59
+
71
60
  Args:
72
61
  conn: Database connection
73
- network_id: Network ID
74
-
62
+
75
63
  Returns:
76
64
  Dictionary with network information
77
-
65
+
78
66
  Raises:
79
- ValidationError: If network doesn't exist
67
+ ValidationError: If network metadata doesn't exist
80
68
  """
81
- cursor = conn.execute("""
82
- SELECT id, name, description, time_start, time_end, time_interval, created_at, updated_at
83
- FROM networks
84
- WHERE id = ?
85
- """, (network_id,))
86
-
69
+ cursor = conn.execute(
70
+ """
71
+ SELECT name, description, time_start, time_end, time_interval, created_at, updated_at
72
+ FROM network_metadata
73
+ LIMIT 1
74
+ """
75
+ )
76
+
87
77
  row = cursor.fetchone()
88
78
  if not row:
89
- raise ValidationError(f"Network with ID {network_id} not found")
90
-
79
+ raise ValidationError("No network metadata found in database")
80
+
91
81
  return {
92
- "id": row[0],
93
- "name": row[1],
94
- "description": row[2],
95
- "time_start": row[3],
96
- "time_end": row[4],
97
- "time_interval": row[5],
98
- "created_at": row[6],
99
- "updated_at": row[7]
82
+ "name": row[0],
83
+ "description": row[1],
84
+ "time_start": row[2],
85
+ "time_end": row[3],
86
+ "time_interval": row[4],
87
+ "created_at": row[5],
88
+ "updated_at": row[6],
100
89
  }
101
90
 
102
91
 
103
- def get_network_time_periods(
104
- conn: sqlite3.Connection,
105
- network_id: int
106
- ) -> List[TimePeriod]:
92
+ def get_network_time_periods(conn: sqlite3.Connection) -> List[TimePeriod]:
107
93
  """
108
- Get network time periods using optimized storage.
109
-
94
+ Get network time periods using optimized storage (single network per database).
95
+
110
96
  Args:
111
97
  conn: Database connection
112
- network_id: Network ID
113
-
98
+
114
99
  Returns:
115
100
  List of TimePeriod objects ordered by period_index
116
101
  """
117
- cursor = conn.execute("""
102
+ cursor = conn.execute(
103
+ """
118
104
  SELECT period_count, start_timestamp, interval_seconds
119
105
  FROM network_time_periods
120
- WHERE network_id = ?
121
- """, (network_id,))
122
-
106
+ LIMIT 1
107
+ """
108
+ )
109
+
123
110
  row = cursor.fetchone()
124
111
  if not row:
125
112
  return [] # No time periods defined
126
-
113
+
127
114
  period_count, start_timestamp, interval_seconds = row
128
-
115
+
129
116
  # Generate all time periods computationally
130
117
  periods = []
131
118
  for period_index in range(period_count):
132
119
  timestamp = start_timestamp + (period_index * interval_seconds)
133
-
120
+
134
121
  # Format timestamp as string for compatibility - ALWAYS use UTC to avoid DST duplicates
135
122
  dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
136
123
  formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S")
137
-
138
- periods.append(TimePeriod(
139
- timestamp=timestamp,
140
- period_index=period_index,
141
- formatted_time=formatted_time
142
- ))
143
-
124
+
125
+ periods.append(
126
+ TimePeriod(
127
+ timestamp=timestamp,
128
+ period_index=period_index,
129
+ formatted_time=formatted_time,
130
+ )
131
+ )
132
+
144
133
  return periods
145
134
 
146
135
 
147
136
  def list_networks(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
148
137
  """
149
- List all networks.
150
-
138
+ Get network information (returns single network in list for backward compatibility).
139
+
151
140
  Args:
152
141
  conn: Database connection
153
-
142
+
154
143
  Returns:
155
- List of network dictionaries
144
+ List with single network dictionary (for backward compatibility)
156
145
  """
157
- cursor = conn.execute("""
158
- SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
159
- FROM networks
160
- ORDER BY created_at DESC
161
- """)
162
-
163
- networks = []
164
- for row in cursor.fetchall():
165
- networks.append({
166
- "id": row[0],
167
- "name": row[1],
168
- "description": row[2],
169
- "created_at": row[3],
170
- "updated_at": row[4],
171
- "time_resolution": row[5], # time_interval from DB mapped to time_resolution
172
- "start_time": row[6], # time_start from DB mapped to start_time
173
- "end_time": row[7], # time_end from DB mapped to end_time
174
- })
175
-
176
- return networks
146
+ try:
147
+ network_info = get_network_info(conn)
148
+ return [network_info]
149
+ except ValidationError:
150
+ return []
177
151
 
178
152
 
179
153
  def get_first_network(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]:
180
154
  """
181
- Get the first network (useful for single-network databases).
182
-
155
+ Get network (for backward compatibility with single-network-per-database).
156
+
183
157
  Args:
184
158
  conn: Database connection
185
-
159
+
186
160
  Returns:
187
- Network dictionary or None if no networks exist
161
+ Network dictionary or None if no network exists
188
162
  """
189
- cursor = conn.execute("""
190
- SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
191
- FROM networks
192
- ORDER BY created_at DESC
193
- LIMIT 1
194
- """)
195
-
196
- row = cursor.fetchone()
197
- if not row:
163
+ try:
164
+ return get_network_info(conn)
165
+ except ValidationError:
198
166
  return None
199
-
200
- return {
201
- "id": row[0],
202
- "name": row[1],
203
- "description": row[2],
204
- "created_at": row[3],
205
- "updated_at": row[4],
206
- "time_resolution": row[5],
207
- "start_time": row[6],
208
- "end_time": row[7],
209
- }
210
167
 
211
168
 
212
- def get_network_by_name(conn: sqlite3.Connection, name: str) -> Optional[Dict[str, Any]]:
169
+ def get_network_by_name(
170
+ conn: sqlite3.Connection, name: str
171
+ ) -> Optional[Dict[str, Any]]:
213
172
  """
214
- Get a network by name.
215
-
173
+ Get network by name (for backward compatibility - checks if name matches).
174
+
216
175
  Args:
217
176
  conn: Database connection
218
- name: Network name
219
-
177
+ name: Network name to match
178
+
220
179
  Returns:
221
- Network dictionary or None if not found
180
+ Network dictionary if name matches, None otherwise
222
181
  """
223
- cursor = conn.execute("""
224
- SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
225
- FROM networks
226
- WHERE name = ?
227
- """, (name,))
228
-
229
- row = cursor.fetchone()
230
- if not row:
182
+ try:
183
+ network_info = get_network_info(conn)
184
+ if network_info.get("name") == name:
185
+ return network_info
186
+ return None
187
+ except ValidationError:
231
188
  return None
232
-
233
- return {
234
- "id": row[0],
235
- "name": row[1],
236
- "description": row[2],
237
- "created_at": row[3],
238
- "updated_at": row[4],
239
- "time_resolution": row[5],
240
- "start_time": row[6],
241
- "end_time": row[7],
242
- }
243
189
 
244
190
 
245
191
  def create_carrier(
246
- conn: sqlite3.Connection,
247
- network_id: int,
248
- name: str,
192
+ conn: sqlite3.Connection,
193
+ name: str,
249
194
  co2_emissions: float = 0.0,
250
195
  color: Optional[str] = None,
251
- nice_name: Optional[str] = None
196
+ nice_name: Optional[str] = None,
252
197
  ) -> int:
253
198
  """
254
- Create a carrier record and return carrier ID.
255
-
199
+ Create a carrier record and return carrier ID (single network per database).
200
+
256
201
  Args:
257
202
  conn: Database connection
258
- network_id: Network ID
259
203
  name: Carrier name
260
204
  co2_emissions: CO2 emissions factor
261
205
  color: Display color
262
206
  nice_name: Human-readable name
263
-
207
+
264
208
  Returns:
265
209
  ID of the newly created carrier
266
210
  """
267
- cursor = conn.execute("""
268
- INSERT INTO carriers (network_id, name, co2_emissions, color, nice_name)
269
- VALUES (?, ?, ?, ?, ?)
270
- """, (network_id, name, co2_emissions, color, nice_name))
271
-
211
+ cursor = conn.execute(
212
+ """
213
+ INSERT INTO carriers (name, co2_emissions, color, nice_name)
214
+ VALUES (?, ?, ?, ?)
215
+ """,
216
+ (name, co2_emissions, color, nice_name),
217
+ )
218
+
272
219
  carrier_id = cursor.lastrowid
273
220
  if not carrier_id:
274
221
  raise DatabaseError("Failed to create carrier")
275
-
222
+
276
223
  return carrier_id
277
224
 
278
225
 
279
- def list_carriers(conn: sqlite3.Connection, network_id: int) -> List[Dict[str, Any]]:
226
+ def list_carriers(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
280
227
  """
281
- List all carriers for a network.
282
-
228
+ List all carriers (single network per database).
229
+
283
230
  Args:
284
231
  conn: Database connection
285
- network_id: Network ID
286
-
232
+
287
233
  Returns:
288
234
  List of carrier dictionaries
289
235
  """
290
- cursor = conn.execute("""
291
- SELECT id, network_id, name, co2_emissions, color, nice_name
236
+ cursor = conn.execute(
237
+ """
238
+ SELECT id, name, co2_emissions, color, nice_name
292
239
  FROM carriers
293
- WHERE network_id = ?
294
240
  ORDER BY name
295
- """, (network_id,))
296
-
241
+ """
242
+ )
243
+
297
244
  carriers = []
298
245
  for row in cursor.fetchall():
299
- carriers.append({
300
- "id": row[0],
301
- "network_id": row[1],
302
- "name": row[2],
303
- "co2_emissions": row[3],
304
- "color": row[4],
305
- "nice_name": row[5]
306
- })
307
-
246
+ carriers.append(
247
+ {
248
+ "id": row[0],
249
+ "name": row[1],
250
+ "co2_emissions": row[2],
251
+ "color": row[3],
252
+ "nice_name": row[4],
253
+ }
254
+ )
255
+
308
256
  return carriers
309
257
 
310
258
 
311
259
  def get_network_config(
312
- conn: sqlite3.Connection,
313
- network_id: int,
314
- scenario_id: Optional[int] = None
260
+ conn: sqlite3.Connection, scenario_id: Optional[int] = None
315
261
  ) -> Dict[str, Any]:
316
262
  """
317
- Get network configuration with scenario-aware fallback.
318
-
263
+ Get network configuration with scenario-aware fallback (single network per database).
264
+
319
265
  Priority order:
320
266
  1. Scenario-specific config (network_config WHERE scenario_id = X)
321
267
  2. Network default config (network_config WHERE scenario_id IS NULL)
322
- 3. Legacy column value (networks.unmet_load_active)
323
- 4. System default value
324
-
268
+ 3. System default value
269
+
325
270
  Args:
326
271
  conn: Database connection
327
- network_id: Network ID
328
272
  scenario_id: Optional scenario ID
329
-
273
+
330
274
  Returns:
331
275
  Dictionary with network configuration
332
276
  """
333
277
  config = {}
334
-
278
+
335
279
  # Load from network_config table with scenario fallback
336
- cursor = conn.execute("""
280
+ cursor = conn.execute(
281
+ """
337
282
  SELECT param_name, param_type, param_value
338
283
  FROM network_config
339
- WHERE network_id = ? AND (scenario_id = ? OR scenario_id IS NULL)
284
+ WHERE (scenario_id = ? OR scenario_id IS NULL)
340
285
  ORDER BY scenario_id DESC NULLS LAST -- Scenario-specific values first
341
- """, (network_id, scenario_id))
342
-
286
+ """,
287
+ (scenario_id,),
288
+ )
289
+
343
290
  seen_params = set()
344
291
  for row in cursor.fetchall():
345
292
  param_name, param_type, param_value = row
346
-
293
+
347
294
  # Skip if we already have this parameter (scenario-specific takes precedence)
348
295
  if param_name in seen_params:
349
296
  continue
350
297
  seen_params.add(param_name)
351
-
298
+
352
299
  # Parse value based on type
353
300
  try:
354
- if param_type == 'boolean':
355
- config[param_name] = param_value.lower() == 'true'
356
- elif param_type == 'real':
301
+ if param_type == "boolean":
302
+ config[param_name] = param_value.lower() == "true"
303
+ elif param_type == "real":
357
304
  config[param_name] = float(param_value)
358
- elif param_type == 'integer':
305
+ elif param_type == "integer":
359
306
  config[param_name] = int(param_value)
360
- elif param_type == 'json':
307
+ elif param_type == "json":
361
308
  config[param_name] = json.loads(param_value)
362
309
  else: # string
363
310
  config[param_name] = param_value
364
311
  except (ValueError, json.JSONDecodeError) as e:
365
312
  logger.warning(f"Failed to parse config parameter {param_name}: {e}")
366
313
  continue
367
-
368
- # Fallback to legacy column for unmet_load_active if not in config table
369
- if 'unmet_load_active' not in config:
370
- cursor = conn.execute("SELECT unmet_load_active FROM networks WHERE id = ?", (network_id,))
371
- row = cursor.fetchone()
372
- if row and row[0] is not None:
373
- config['unmet_load_active'] = bool(row[0])
374
-
314
+
375
315
  # Apply system defaults for missing parameters
376
316
  defaults = {
377
- 'unmet_load_active': True,
378
- 'discount_rate': 0.0, # No discounting by default
379
- 'solver_name': 'default'
317
+ "unmet_load_active": True,
318
+ "discount_rate": 0.0, # No discounting by default
319
+ "solver_name": "default",
380
320
  }
381
-
321
+
382
322
  for param, default_value in defaults.items():
383
323
  if param not in config:
384
324
  config[param] = default_value
385
-
325
+
386
326
  return config
387
327
 
388
328
 
389
329
  def set_network_config(
390
330
  conn: sqlite3.Connection,
391
- network_id: int,
392
331
  param_name: str,
393
332
  param_value: Any,
394
333
  param_type: str,
395
334
  scenario_id: Optional[int] = None,
396
- description: Optional[str] = None
335
+ description: Optional[str] = None,
397
336
  ) -> None:
398
337
  """
399
- Set network configuration parameter.
400
-
338
+ Set network configuration parameter (single network per database).
339
+
401
340
  Args:
402
341
  conn: Database connection
403
- network_id: Network ID
404
342
  param_name: Parameter name
405
343
  param_value: Parameter value
406
344
  param_type: Parameter type ('boolean', 'real', 'integer', 'string', 'json')
407
- scenario_id: Optional scenario ID
345
+ scenario_id: Optional scenario ID (NULL for base network)
408
346
  description: Optional parameter description
409
-
347
+
410
348
  Raises:
411
349
  ValidationError: If parameter type is invalid or serialization fails
412
350
  """
413
-
351
+
414
352
  # Validate parameter type
415
- valid_types = {'boolean', 'real', 'integer', 'string', 'json'}
353
+ valid_types = {"boolean", "real", "integer", "string", "json"}
416
354
  if param_type not in valid_types:
417
- raise ValidationError(f"Invalid parameter type: {param_type}. Must be one of {valid_types}")
418
-
355
+ raise ValidationError(
356
+ f"Invalid parameter type: {param_type}. Must be one of {valid_types}"
357
+ )
358
+
419
359
  # Serialize value based on type
420
360
  try:
421
- if param_type == 'boolean':
361
+ if param_type == "boolean":
422
362
  serialized = str(param_value).lower()
423
- if serialized not in {'true', 'false'}:
424
- raise ValidationError(f"Boolean parameter must be True/False, got: {param_value}")
425
- elif param_type == 'real':
363
+ if serialized not in {"true", "false"}:
364
+ raise ValidationError(
365
+ f"Boolean parameter must be True/False, got: {param_value}"
366
+ )
367
+ elif param_type == "real":
426
368
  serialized = str(float(param_value))
427
- elif param_type == 'integer':
369
+ elif param_type == "integer":
428
370
  serialized = str(int(param_value))
429
- elif param_type == 'json':
371
+ elif param_type == "json":
430
372
  serialized = json.dumps(param_value)
431
373
  else: # string
432
374
  serialized = str(param_value)
433
375
  except (ValueError, TypeError) as e:
434
- raise ValidationError(f"Failed to serialize parameter {param_name} as {param_type}: {e}")
435
-
376
+ raise ValidationError(
377
+ f"Failed to serialize parameter {param_name} as {param_type}: {e}"
378
+ )
379
+
436
380
  # Insert or update parameter
437
- conn.execute("""
381
+ conn.execute(
382
+ """
438
383
  INSERT OR REPLACE INTO network_config
439
- (network_id, scenario_id, param_name, param_type, param_value, param_description, updated_at)
440
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
441
- """, (network_id, scenario_id, param_name, param_type, serialized, description))
384
+ (scenario_id, param_name, param_type, param_value, param_description, updated_at)
385
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
386
+ """,
387
+ (scenario_id, param_name, param_type, serialized, description),
388
+ )
442
389
 
443
390
 
444
- def get_component_counts(conn: sqlite3.Connection, network_id: int) -> Dict[str, int]:
391
+ def get_component_counts(conn: sqlite3.Connection) -> Dict[str, int]:
445
392
  """
446
- Get component counts by type for a network.
447
-
393
+ Get component counts by type (single network per database).
394
+
448
395
  Args:
449
396
  conn: Database connection
450
- network_id: Network ID
451
-
397
+
452
398
  Returns:
453
399
  Dictionary mapping component types to counts
454
400
  """
455
- cursor = conn.execute("""
401
+ cursor = conn.execute(
402
+ """
456
403
  SELECT component_type, COUNT(*) FROM components
457
- WHERE network_id = ? GROUP BY component_type
458
- """, (network_id,))
459
-
404
+ GROUP BY component_type
405
+ """
406
+ )
407
+
460
408
  counts = {}
461
409
  for row in cursor.fetchall():
462
410
  counts[row[0].lower()] = row[1]
463
-
464
- return counts
465
-
466
411
 
467
- def get_master_scenario_id(conn: sqlite3.Connection, network_id: int) -> int:
468
- """Get the master scenario ID for a network"""
469
- cursor = conn.cursor()
470
- cursor.execute(
471
- "SELECT id FROM scenarios WHERE network_id = ? AND is_master = TRUE",
472
- (network_id,)
473
- )
474
- result = cursor.fetchone()
475
- if not result:
476
- raise ValidationError(f"No master scenario found for network {network_id}")
477
- return result[0]
478
-
479
-
480
- def resolve_scenario_id(conn: sqlite3.Connection, component_id: int, scenario_id: Optional[int]) -> int:
481
- """Resolve scenario ID - if None, get master scenario ID"""
482
- if scenario_id is not None:
483
- return scenario_id
484
-
485
- # Get network_id from component, then get master scenario
486
- cursor = conn.cursor()
487
- cursor.execute("SELECT network_id FROM components WHERE id = ?", (component_id,))
488
- result = cursor.fetchone()
489
- if not result:
490
- from pyconvexity.core.errors import ComponentNotFound
491
- raise ComponentNotFound(component_id)
492
-
493
- network_id = result[0]
494
- return get_master_scenario_id(conn, network_id)
412
+ return counts