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.
- pyconvexity/__init__.py +120 -0
- pyconvexity/_version.py +2 -0
- pyconvexity/core/__init__.py +64 -0
- pyconvexity/core/database.py +314 -0
- pyconvexity/core/errors.py +100 -0
- pyconvexity/core/types.py +306 -0
- pyconvexity/models/__init__.py +36 -0
- pyconvexity/models/attributes.py +383 -0
- pyconvexity/models/components.py +464 -0
- pyconvexity/models/network.py +426 -0
- pyconvexity/validation/__init__.py +17 -0
- pyconvexity/validation/rules.py +301 -0
- pyconvexity-0.1.0.dist-info/METADATA +135 -0
- pyconvexity-0.1.0.dist-info/RECORD +16 -0
- pyconvexity-0.1.0.dist-info/WHEEL +5 -0
- pyconvexity-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|