pyconvexity 0.1.0__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.

Potentially problematic release.


This version of pyconvexity might be problematic. Click here for more details.

@@ -0,0 +1,426 @@
1
+ """
2
+ Network management operations for PyConvexity.
3
+
4
+ Provides operations for creating, managing, and querying energy system networks
5
+ including time periods, carriers, and network configuration.
6
+ """
7
+
8
+ import sqlite3
9
+ import json
10
+ import logging
11
+ from typing import Dict, Any, Optional, List
12
+ from datetime import datetime
13
+
14
+ from pyconvexity.core.types import (
15
+ CreateNetworkRequest, TimePeriod, Network
16
+ )
17
+ from pyconvexity.core.errors import (
18
+ ValidationError, DatabaseError
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def create_network(conn: sqlite3.Connection, request: CreateNetworkRequest) -> int:
25
+ """
26
+ Create a network record and return network ID.
27
+
28
+ Args:
29
+ conn: Database connection
30
+ request: Network creation request
31
+
32
+ Returns:
33
+ ID of the newly created network
34
+
35
+ Raises:
36
+ ValidationError: If required fields are missing
37
+ DatabaseError: If creation fails
38
+ """
39
+
40
+ # Validate required fields
41
+ if not request.start_time:
42
+ raise ValidationError("start_time is required")
43
+ if not request.end_time:
44
+ 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)
48
+ 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]:
68
+ """
69
+ Get network information.
70
+
71
+ Args:
72
+ conn: Database connection
73
+ network_id: Network ID
74
+
75
+ Returns:
76
+ Dictionary with network information
77
+
78
+ Raises:
79
+ ValidationError: If network doesn't exist
80
+ """
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
+
87
+ row = cursor.fetchone()
88
+ if not row:
89
+ raise ValidationError(f"Network with ID {network_id} not found")
90
+
91
+ 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]
100
+ }
101
+
102
+
103
+ def get_network_time_periods(
104
+ conn: sqlite3.Connection,
105
+ network_id: int
106
+ ) -> List[TimePeriod]:
107
+ """
108
+ Get network time periods.
109
+
110
+ Args:
111
+ conn: Database connection
112
+ network_id: Network ID
113
+
114
+ Returns:
115
+ List of TimePeriod objects ordered by period_index
116
+ """
117
+ cursor = conn.execute("""
118
+ SELECT timestamp, period_index
119
+ FROM network_time_periods
120
+ WHERE network_id = ?
121
+ ORDER BY period_index
122
+ """, (network_id,))
123
+
124
+ periods = []
125
+ for row in cursor.fetchall():
126
+ timestamp_str, period_index = row
127
+
128
+ # Convert datetime string to Unix timestamp
129
+ try:
130
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
131
+ timestamp = int(dt.timestamp())
132
+ except ValueError:
133
+ # Fallback: use period_index as timestamp
134
+ timestamp = period_index
135
+
136
+ periods.append(TimePeriod(
137
+ timestamp=timestamp,
138
+ period_index=period_index,
139
+ formatted_time=timestamp_str
140
+ ))
141
+
142
+ return periods
143
+
144
+
145
+ def list_networks(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
146
+ """
147
+ List all networks.
148
+
149
+ Args:
150
+ conn: Database connection
151
+
152
+ Returns:
153
+ List of network dictionaries
154
+ """
155
+ cursor = conn.execute("""
156
+ SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
157
+ FROM networks
158
+ ORDER BY created_at DESC
159
+ """)
160
+
161
+ networks = []
162
+ for row in cursor.fetchall():
163
+ networks.append({
164
+ "id": row[0],
165
+ "name": row[1],
166
+ "description": row[2],
167
+ "created_at": row[3],
168
+ "updated_at": row[4],
169
+ "time_resolution": row[5], # time_interval from DB mapped to time_resolution
170
+ "start_time": row[6], # time_start from DB mapped to start_time
171
+ "end_time": row[7], # time_end from DB mapped to end_time
172
+ })
173
+
174
+ return networks
175
+
176
+
177
+ def create_carrier(
178
+ conn: sqlite3.Connection,
179
+ network_id: int,
180
+ name: str,
181
+ co2_emissions: float = 0.0,
182
+ color: Optional[str] = None,
183
+ nice_name: Optional[str] = None
184
+ ) -> int:
185
+ """
186
+ Create a carrier record and return carrier ID.
187
+
188
+ Args:
189
+ conn: Database connection
190
+ network_id: Network ID
191
+ name: Carrier name
192
+ co2_emissions: CO2 emissions factor
193
+ color: Display color
194
+ nice_name: Human-readable name
195
+
196
+ Returns:
197
+ ID of the newly created carrier
198
+ """
199
+ cursor = conn.execute("""
200
+ INSERT INTO carriers (network_id, name, co2_emissions, color, nice_name)
201
+ VALUES (?, ?, ?, ?, ?)
202
+ """, (network_id, name, co2_emissions, color, nice_name))
203
+
204
+ carrier_id = cursor.lastrowid
205
+ if not carrier_id:
206
+ raise DatabaseError("Failed to create carrier")
207
+
208
+ return carrier_id
209
+
210
+
211
+ def list_carriers(conn: sqlite3.Connection, network_id: int) -> List[Dict[str, Any]]:
212
+ """
213
+ List all carriers for a network.
214
+
215
+ Args:
216
+ conn: Database connection
217
+ network_id: Network ID
218
+
219
+ Returns:
220
+ List of carrier dictionaries
221
+ """
222
+ cursor = conn.execute("""
223
+ SELECT id, network_id, name, co2_emissions, color, nice_name
224
+ FROM carriers
225
+ WHERE network_id = ?
226
+ ORDER BY name
227
+ """, (network_id,))
228
+
229
+ carriers = []
230
+ for row in cursor.fetchall():
231
+ carriers.append({
232
+ "id": row[0],
233
+ "network_id": row[1],
234
+ "name": row[2],
235
+ "co2_emissions": row[3],
236
+ "color": row[4],
237
+ "nice_name": row[5]
238
+ })
239
+
240
+ return carriers
241
+
242
+
243
+ def get_network_config(
244
+ conn: sqlite3.Connection,
245
+ network_id: int,
246
+ scenario_id: Optional[int] = None
247
+ ) -> Dict[str, Any]:
248
+ """
249
+ Get network configuration with scenario-aware fallback.
250
+
251
+ Priority order:
252
+ 1. Scenario-specific config (network_config WHERE scenario_id = X)
253
+ 2. Network default config (network_config WHERE scenario_id IS NULL)
254
+ 3. Legacy column value (networks.unmet_load_active)
255
+ 4. System default value
256
+
257
+ Args:
258
+ conn: Database connection
259
+ network_id: Network ID
260
+ scenario_id: Optional scenario ID
261
+
262
+ Returns:
263
+ Dictionary with network configuration
264
+ """
265
+ config = {}
266
+
267
+ # Load from network_config table with scenario fallback
268
+ cursor = conn.execute("""
269
+ SELECT param_name, param_type, param_value
270
+ FROM network_config
271
+ WHERE network_id = ? AND (scenario_id = ? OR scenario_id IS NULL)
272
+ ORDER BY scenario_id DESC NULLS LAST -- Scenario-specific values first
273
+ """, (network_id, scenario_id))
274
+
275
+ seen_params = set()
276
+ for row in cursor.fetchall():
277
+ param_name, param_type, param_value = row
278
+
279
+ # Skip if we already have this parameter (scenario-specific takes precedence)
280
+ if param_name in seen_params:
281
+ continue
282
+ seen_params.add(param_name)
283
+
284
+ # Parse value based on type
285
+ try:
286
+ if param_type == 'boolean':
287
+ config[param_name] = param_value.lower() == 'true'
288
+ elif param_type == 'real':
289
+ config[param_name] = float(param_value)
290
+ elif param_type == 'integer':
291
+ config[param_name] = int(param_value)
292
+ elif param_type == 'json':
293
+ config[param_name] = json.loads(param_value)
294
+ else: # string
295
+ config[param_name] = param_value
296
+ except (ValueError, json.JSONDecodeError) as e:
297
+ logger.warning(f"Failed to parse config parameter {param_name}: {e}")
298
+ continue
299
+
300
+ # Fallback to legacy column for unmet_load_active if not in config table
301
+ if 'unmet_load_active' not in config:
302
+ cursor = conn.execute("SELECT unmet_load_active FROM networks WHERE id = ?", (network_id,))
303
+ row = cursor.fetchone()
304
+ if row and row[0] is not None:
305
+ config['unmet_load_active'] = bool(row[0])
306
+
307
+ # Apply system defaults for missing parameters
308
+ defaults = {
309
+ 'unmet_load_active': True,
310
+ 'discount_rate': 0.01,
311
+ 'solver_name': 'default'
312
+ }
313
+
314
+ for param, default_value in defaults.items():
315
+ if param not in config:
316
+ config[param] = default_value
317
+
318
+ return config
319
+
320
+
321
+ def set_network_config(
322
+ conn: sqlite3.Connection,
323
+ network_id: int,
324
+ param_name: str,
325
+ param_value: Any,
326
+ param_type: str,
327
+ scenario_id: Optional[int] = None,
328
+ description: Optional[str] = None
329
+ ) -> None:
330
+ """
331
+ Set network configuration parameter.
332
+
333
+ Args:
334
+ conn: Database connection
335
+ network_id: Network ID
336
+ param_name: Parameter name
337
+ param_value: Parameter value
338
+ param_type: Parameter type ('boolean', 'real', 'integer', 'string', 'json')
339
+ scenario_id: Optional scenario ID
340
+ description: Optional parameter description
341
+
342
+ Raises:
343
+ ValidationError: If parameter type is invalid or serialization fails
344
+ """
345
+
346
+ # Validate parameter type
347
+ valid_types = {'boolean', 'real', 'integer', 'string', 'json'}
348
+ if param_type not in valid_types:
349
+ raise ValidationError(f"Invalid parameter type: {param_type}. Must be one of {valid_types}")
350
+
351
+ # Serialize value based on type
352
+ try:
353
+ if param_type == 'boolean':
354
+ serialized = str(param_value).lower()
355
+ if serialized not in {'true', 'false'}:
356
+ raise ValidationError(f"Boolean parameter must be True/False, got: {param_value}")
357
+ elif param_type == 'real':
358
+ serialized = str(float(param_value))
359
+ elif param_type == 'integer':
360
+ serialized = str(int(param_value))
361
+ elif param_type == 'json':
362
+ serialized = json.dumps(param_value)
363
+ else: # string
364
+ serialized = str(param_value)
365
+ except (ValueError, TypeError) as e:
366
+ raise ValidationError(f"Failed to serialize parameter {param_name} as {param_type}: {e}")
367
+
368
+ # Insert or update parameter
369
+ conn.execute("""
370
+ INSERT OR REPLACE INTO network_config
371
+ (network_id, scenario_id, param_name, param_type, param_value, param_description, updated_at)
372
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
373
+ """, (network_id, scenario_id, param_name, param_type, serialized, description))
374
+
375
+
376
+ def get_component_counts(conn: sqlite3.Connection, network_id: int) -> Dict[str, int]:
377
+ """
378
+ Get component counts by type for a network.
379
+
380
+ Args:
381
+ conn: Database connection
382
+ network_id: Network ID
383
+
384
+ Returns:
385
+ Dictionary mapping component types to counts
386
+ """
387
+ cursor = conn.execute("""
388
+ SELECT component_type, COUNT(*) FROM components
389
+ WHERE network_id = ? GROUP BY component_type
390
+ """, (network_id,))
391
+
392
+ counts = {}
393
+ for row in cursor.fetchall():
394
+ counts[row[0].lower()] = row[1]
395
+
396
+ return counts
397
+
398
+
399
+ def get_master_scenario_id(conn: sqlite3.Connection, network_id: int) -> int:
400
+ """Get the master scenario ID for a network"""
401
+ cursor = conn.cursor()
402
+ cursor.execute(
403
+ "SELECT id FROM scenarios WHERE network_id = ? AND is_master = TRUE",
404
+ (network_id,)
405
+ )
406
+ result = cursor.fetchone()
407
+ if not result:
408
+ raise ValidationError(f"No master scenario found for network {network_id}")
409
+ return result[0]
410
+
411
+
412
+ def resolve_scenario_id(conn: sqlite3.Connection, component_id: int, scenario_id: Optional[int]) -> int:
413
+ """Resolve scenario ID - if None, get master scenario ID"""
414
+ if scenario_id is not None:
415
+ return scenario_id
416
+
417
+ # Get network_id from component, then get master scenario
418
+ cursor = conn.cursor()
419
+ cursor.execute("SELECT network_id FROM components WHERE id = ?", (component_id,))
420
+ result = cursor.fetchone()
421
+ if not result:
422
+ from pyconvexity.core.errors import ComponentNotFound
423
+ raise ComponentNotFound(component_id)
424
+
425
+ network_id = result[0]
426
+ return get_master_scenario_id(conn, network_id)
@@ -0,0 +1,17 @@
1
+ """
2
+ Validation module for PyConvexity.
3
+
4
+ Contains data validation rules and type checking functionality.
5
+ """
6
+
7
+ from pyconvexity.validation.rules import (
8
+ get_validation_rule, list_validation_rules, get_all_validation_rules,
9
+ validate_static_value, validate_timeseries_alignment, parse_default_value,
10
+ get_attribute_setter_info
11
+ )
12
+
13
+ __all__ = [
14
+ "get_validation_rule", "list_validation_rules", "get_all_validation_rules",
15
+ "validate_static_value", "validate_timeseries_alignment", "parse_default_value",
16
+ "get_attribute_setter_info"
17
+ ]