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

Files changed (44) hide show
  1. pyconvexity/__init__.py +241 -0
  2. pyconvexity/_version.py +1 -0
  3. pyconvexity/core/__init__.py +60 -0
  4. pyconvexity/core/database.py +485 -0
  5. pyconvexity/core/errors.py +106 -0
  6. pyconvexity/core/types.py +400 -0
  7. pyconvexity/dashboard.py +265 -0
  8. pyconvexity/data/README.md +101 -0
  9. pyconvexity/data/__init__.py +17 -0
  10. pyconvexity/data/loaders/__init__.py +3 -0
  11. pyconvexity/data/loaders/cache.py +213 -0
  12. pyconvexity/data/schema/01_core_schema.sql +420 -0
  13. pyconvexity/data/schema/02_data_metadata.sql +120 -0
  14. pyconvexity/data/schema/03_validation_data.sql +507 -0
  15. pyconvexity/data/sources/__init__.py +5 -0
  16. pyconvexity/data/sources/gem.py +442 -0
  17. pyconvexity/io/__init__.py +26 -0
  18. pyconvexity/io/excel_exporter.py +1226 -0
  19. pyconvexity/io/excel_importer.py +1381 -0
  20. pyconvexity/io/netcdf_exporter.py +191 -0
  21. pyconvexity/io/netcdf_importer.py +1802 -0
  22. pyconvexity/models/__init__.py +195 -0
  23. pyconvexity/models/attributes.py +730 -0
  24. pyconvexity/models/carriers.py +159 -0
  25. pyconvexity/models/components.py +611 -0
  26. pyconvexity/models/network.py +503 -0
  27. pyconvexity/models/results.py +148 -0
  28. pyconvexity/models/scenarios.py +234 -0
  29. pyconvexity/solvers/__init__.py +29 -0
  30. pyconvexity/solvers/pypsa/__init__.py +30 -0
  31. pyconvexity/solvers/pypsa/api.py +446 -0
  32. pyconvexity/solvers/pypsa/batch_loader.py +296 -0
  33. pyconvexity/solvers/pypsa/builder.py +655 -0
  34. pyconvexity/solvers/pypsa/clearing_price.py +678 -0
  35. pyconvexity/solvers/pypsa/constraints.py +405 -0
  36. pyconvexity/solvers/pypsa/solver.py +1442 -0
  37. pyconvexity/solvers/pypsa/storage.py +2096 -0
  38. pyconvexity/timeseries.py +330 -0
  39. pyconvexity/validation/__init__.py +25 -0
  40. pyconvexity/validation/rules.py +312 -0
  41. pyconvexity-0.4.8.dist-info/METADATA +148 -0
  42. pyconvexity-0.4.8.dist-info/RECORD +44 -0
  43. pyconvexity-0.4.8.dist-info/WHEEL +5 -0
  44. pyconvexity-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,330 @@
1
+ """
2
+ High-level timeseries API for PyConvexity.
3
+
4
+ This module provides the main interface for working with timeseries data,
5
+ matching the efficient patterns used in the Rust implementation.
6
+
7
+ Key Features:
8
+ - Ultra-fast binary serialization (matches Rust exactly)
9
+ - Array-based data structures for maximum performance
10
+ - Unified API for getting/setting timeseries data
11
+ - Backward compatibility with legacy point-based format
12
+ - Efficient sampling and filtering operations
13
+ """
14
+
15
+ import sqlite3
16
+ from typing import List, Optional, Union
17
+ import numpy as np
18
+
19
+ from pyconvexity.core.database import database_context
20
+ from pyconvexity.core.types import Timeseries, TimeseriesMetadata
21
+ from pyconvexity.models.attributes import (
22
+ get_timeseries as _get_timeseries,
23
+ get_timeseries_metadata as _get_timeseries_metadata,
24
+ set_timeseries_attribute,
25
+ serialize_values_to_binary,
26
+ deserialize_values_from_binary,
27
+ get_timeseries_length_from_binary,
28
+ )
29
+
30
+
31
+ # ============================================================================
32
+ # HIGH-LEVEL TIMESERIES API
33
+ # ============================================================================
34
+
35
+
36
+ def get_timeseries(
37
+ db_path: str,
38
+ component_id: int,
39
+ attribute_name: str,
40
+ scenario_id: Optional[int] = None,
41
+ start_index: Optional[int] = None,
42
+ end_index: Optional[int] = None,
43
+ max_points: Optional[int] = None,
44
+ ) -> Timeseries:
45
+ """
46
+ Get timeseries data with efficient array-based format.
47
+
48
+ This is the main function for retrieving timeseries data. It returns
49
+ a Timeseries object with values as a flat array for maximum performance.
50
+
51
+ Args:
52
+ db_path: Path to the database file
53
+ component_id: Component ID
54
+ attribute_name: Name of the attribute (e.g., 'p', 'p_set', 'marginal_cost')
55
+ scenario_id: Scenario ID (uses master scenario if None)
56
+ start_index: Start index for range queries (optional)
57
+ end_index: End index for range queries (optional)
58
+ max_points: Maximum number of points for sampling (optional)
59
+
60
+ Returns:
61
+ Timeseries object with efficient array-based data
62
+
63
+ Example:
64
+ >>> ts = get_timeseries("model.db", component_id=123, attribute_name="p")
65
+ >>> print(f"Length: {ts.length}, Values: {ts.values[:5]}")
66
+ Length: 8760, Values: [100.5, 95.2, 87.3, 92.1, 88.7]
67
+
68
+ # Get a subset of the data
69
+ >>> ts_subset = get_timeseries("model.db", 123, "p", start_index=100, end_index=200)
70
+ >>> print(f"Subset length: {ts_subset.length}")
71
+ Subset length: 100
72
+
73
+ # Sample large datasets
74
+ >>> ts_sampled = get_timeseries("model.db", 123, "p", max_points=1000)
75
+ >>> print(f"Sampled from {ts.length} to {ts_sampled.length} points")
76
+ """
77
+ with database_context(db_path, read_only=True) as conn:
78
+ return _get_timeseries(
79
+ conn,
80
+ component_id,
81
+ attribute_name,
82
+ scenario_id,
83
+ start_index,
84
+ end_index,
85
+ max_points,
86
+ )
87
+
88
+
89
+ def get_timeseries_metadata(
90
+ db_path: str,
91
+ component_id: int,
92
+ attribute_name: str,
93
+ scenario_id: Optional[int] = None,
94
+ ) -> TimeseriesMetadata:
95
+ """
96
+ Get timeseries metadata without loading the full data.
97
+
98
+ This is useful for checking the size and properties of a timeseries
99
+ before deciding whether to load the full data.
100
+
101
+ Args:
102
+ db_path: Path to the database file
103
+ component_id: Component ID
104
+ attribute_name: Name of the attribute
105
+ scenario_id: Scenario ID (uses master scenario if None)
106
+
107
+ Returns:
108
+ TimeseriesMetadata with length and type information
109
+
110
+ Example:
111
+ >>> meta = get_timeseries_metadata("model.db", 123, "p")
112
+ >>> print(f"Length: {meta.length}, Type: {meta.data_type}, Unit: {meta.unit}")
113
+ Length: 8760, Type: float, Unit: MW
114
+ """
115
+ with database_context(db_path, read_only=True) as conn:
116
+ return _get_timeseries_metadata(conn, component_id, attribute_name, scenario_id)
117
+
118
+
119
+ def set_timeseries(
120
+ db_path: str,
121
+ component_id: int,
122
+ attribute_name: str,
123
+ values: Union[List[float], np.ndarray, Timeseries],
124
+ scenario_id: Optional[int] = None,
125
+ ) -> None:
126
+ """
127
+ Set timeseries data using efficient array-based format.
128
+
129
+ This is the main function for storing timeseries data. It accepts
130
+ various input formats and stores them efficiently in the database.
131
+
132
+ Args:
133
+ db_path: Path to the database file
134
+ component_id: Component ID
135
+ attribute_name: Name of the attribute
136
+ values: Timeseries values as list, numpy array, or Timeseries object
137
+ scenario_id: Scenario ID (uses master scenario if None)
138
+
139
+ Example:
140
+ # Set from a list
141
+ >>> values = [100.5, 95.2, 87.3, 92.1, 88.7]
142
+ >>> set_timeseries("model.db", 123, "p_set", values)
143
+
144
+ # Set from numpy array
145
+ >>> import numpy as np
146
+ >>> values = np.random.normal(100, 10, 8760) # Hourly data for a year
147
+ >>> set_timeseries("model.db", 123, "p_max_pu", values)
148
+
149
+ # Set from existing Timeseries object
150
+ >>> ts = get_timeseries("model.db", 456, "p")
151
+ >>> set_timeseries("model.db", 123, "p_set", ts)
152
+ """
153
+ # Convert input to list of floats
154
+ if isinstance(values, Timeseries):
155
+ values_list = values.values
156
+ elif isinstance(values, np.ndarray):
157
+ values_list = values.tolist()
158
+ elif isinstance(values, list):
159
+ values_list = [float(v) for v in values]
160
+ else:
161
+ raise ValueError("values must be List[float], numpy.ndarray, or Timeseries")
162
+
163
+ with database_context(db_path) as conn:
164
+ set_timeseries_attribute(
165
+ conn, component_id, attribute_name, values_list, scenario_id
166
+ )
167
+
168
+
169
+ def get_multiple_timeseries(
170
+ db_path: str, requests: List[dict], max_points: Optional[int] = None
171
+ ) -> List[Timeseries]:
172
+ """
173
+ Get multiple timeseries efficiently in a single database connection.
174
+
175
+ This is more efficient than calling get_timeseries multiple times
176
+ when you need to load many timeseries from the same database.
177
+
178
+ Args:
179
+ db_path: Path to the database file
180
+ requests: List of dicts with keys: component_id, attribute_name, scenario_id (optional)
181
+ max_points: Maximum number of points for sampling (applied to all)
182
+
183
+ Returns:
184
+ List of Timeseries objects in the same order as requests
185
+
186
+ Example:
187
+ >>> requests = [
188
+ ... {"component_id": 123, "attribute_name": "p"},
189
+ ... {"component_id": 124, "attribute_name": "p"},
190
+ ... {"component_id": 125, "attribute_name": "p", "scenario_id": 2}
191
+ ... ]
192
+ >>> timeseries_list = get_multiple_timeseries("model.db", requests)
193
+ >>> print(f"Loaded {len(timeseries_list)} timeseries")
194
+ """
195
+ results = []
196
+
197
+ with database_context(db_path, read_only=True) as conn:
198
+ for request in requests:
199
+ component_id = request["component_id"]
200
+ attribute_name = request["attribute_name"]
201
+ scenario_id = request.get("scenario_id")
202
+
203
+ ts = _get_timeseries(
204
+ conn, component_id, attribute_name, scenario_id, None, None, max_points
205
+ )
206
+ results.append(ts)
207
+
208
+ return results
209
+
210
+
211
+ # ============================================================================
212
+ # UTILITY FUNCTIONS
213
+ # ============================================================================
214
+
215
+
216
+ def timeseries_to_numpy(timeseries: Timeseries) -> np.ndarray:
217
+ """
218
+ Convert Timeseries to numpy array for scientific computing.
219
+
220
+ Args:
221
+ timeseries: Timeseries object
222
+
223
+ Returns:
224
+ numpy array with float32 dtype for memory efficiency
225
+
226
+ Example:
227
+ >>> ts = get_timeseries("model.db", 123, "p")
228
+ >>> arr = timeseries_to_numpy(ts)
229
+ >>> print(f"Mean: {arr.mean():.2f}, Std: {arr.std():.2f}")
230
+ """
231
+ return np.array(timeseries.values, dtype=np.float32)
232
+
233
+
234
+ def numpy_to_timeseries(
235
+ array: np.ndarray,
236
+ data_type: str = "float",
237
+ unit: Optional[str] = None,
238
+ is_input: bool = True,
239
+ ) -> Timeseries:
240
+ """
241
+ Convert numpy array to Timeseries object.
242
+
243
+ Args:
244
+ array: numpy array of values
245
+ data_type: Data type string (default: "float")
246
+ unit: Unit string (optional)
247
+ is_input: Whether this is input data (default: True)
248
+
249
+ Returns:
250
+ Timeseries object
251
+
252
+ Example:
253
+ >>> import numpy as np
254
+ >>> arr = np.random.normal(100, 10, 8760)
255
+ >>> ts = numpy_to_timeseries(arr, unit="MW")
256
+ >>> print(f"Created timeseries with {ts.length} points")
257
+ """
258
+ values = array.tolist() if hasattr(array, "tolist") else list(array)
259
+ return Timeseries(
260
+ values=[float(v) for v in values],
261
+ length=len(values),
262
+ start_index=0,
263
+ data_type=data_type,
264
+ unit=unit,
265
+ is_input=is_input,
266
+ )
267
+
268
+
269
+ def validate_timeseries_alignment(
270
+ db_path: str, values: Union[List[float], np.ndarray, Timeseries]
271
+ ) -> dict:
272
+ """
273
+ Validate that timeseries data aligns with network time periods.
274
+
275
+ Args:
276
+ db_path: Path to the database file
277
+ values: Timeseries values to validate
278
+
279
+ Returns:
280
+ Dictionary with validation results
281
+
282
+ Example:
283
+ >>> values = [100.0] * 8760 # Hourly data for a year
284
+ >>> result = validate_timeseries_alignment("model.db", 1, values)
285
+ >>> if result["is_valid"]:
286
+ ... print("Timeseries is properly aligned")
287
+ ... else:
288
+ ... print(f"Alignment issues: {result['issues']}")
289
+ """
290
+ # Convert to list of floats
291
+ if isinstance(values, Timeseries):
292
+ values_list = values.values
293
+ elif isinstance(values, np.ndarray):
294
+ values_list = values.tolist()
295
+ elif isinstance(values, list):
296
+ values_list = [float(v) for v in values]
297
+ else:
298
+ raise ValueError("values must be List[float], numpy.ndarray, or Timeseries")
299
+
300
+ with database_context(db_path, read_only=True) as conn:
301
+ # Get network time periods
302
+ from pyconvexity.models.network import get_network_time_periods
303
+
304
+ try:
305
+ time_periods = get_network_time_periods(conn)
306
+ expected_length = len(time_periods)
307
+ actual_length = len(values_list)
308
+
309
+ is_valid = actual_length == expected_length
310
+ issues = []
311
+
312
+ if actual_length < expected_length:
313
+ issues.append(f"Missing {expected_length - actual_length} time periods")
314
+ elif actual_length > expected_length:
315
+ issues.append(f"Extra {actual_length - expected_length} time periods")
316
+
317
+ return {
318
+ "is_valid": is_valid,
319
+ "expected_length": expected_length,
320
+ "actual_length": actual_length,
321
+ "issues": issues,
322
+ }
323
+
324
+ except Exception as e:
325
+ return {
326
+ "is_valid": False,
327
+ "expected_length": 0,
328
+ "actual_length": len(values_list),
329
+ "issues": [f"Failed to get network time periods: {e}"],
330
+ }
@@ -0,0 +1,25 @@
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,
9
+ list_validation_rules,
10
+ get_all_validation_rules,
11
+ validate_static_value,
12
+ validate_timeseries_alignment,
13
+ parse_default_value,
14
+ get_attribute_setter_info,
15
+ )
16
+
17
+ __all__ = [
18
+ "get_validation_rule",
19
+ "list_validation_rules",
20
+ "get_all_validation_rules",
21
+ "validate_static_value",
22
+ "validate_timeseries_alignment",
23
+ "parse_default_value",
24
+ "get_attribute_setter_info",
25
+ ]
@@ -0,0 +1,312 @@
1
+ """
2
+ Validation rules and operations for PyConvexity.
3
+
4
+ Provides validation logic for component attributes, data types, and timeseries alignment.
5
+ """
6
+
7
+ import sqlite3
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, Optional, List
11
+
12
+ from pyconvexity.core.types import (
13
+ ValidationRule,
14
+ StaticValue,
15
+ TimePeriod,
16
+ TimeseriesValidationResult,
17
+ )
18
+ from pyconvexity.core.errors import ValidationError, InvalidDataType
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def get_validation_rule(
24
+ conn: sqlite3.Connection, component_type: str, attribute_name: str
25
+ ) -> ValidationRule:
26
+ """
27
+ Get validation rule for a specific component type and attribute.
28
+
29
+ Args:
30
+ conn: Database connection
31
+ component_type: Type of component (e.g., "BUS", "GENERATOR")
32
+ attribute_name: Name of the attribute
33
+
34
+ Returns:
35
+ ValidationRule object with all validation information
36
+
37
+ Raises:
38
+ ValidationError: If no validation rule is found
39
+ """
40
+ cursor = conn.execute(
41
+ """
42
+ SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
43
+ is_required, is_input, description
44
+ FROM attribute_validation_rules
45
+ WHERE component_type = ? AND attribute_name = ?
46
+ """,
47
+ (component_type, attribute_name),
48
+ )
49
+
50
+ row = cursor.fetchone()
51
+ if not row:
52
+ raise ValidationError(
53
+ f"No validation rule found for {component_type}.{attribute_name}"
54
+ )
55
+
56
+ allowed_storage_types = row[5]
57
+ allows_static = allowed_storage_types in ("static", "static_or_timeseries")
58
+ allows_timeseries = allowed_storage_types in ("timeseries", "static_or_timeseries")
59
+
60
+ # Parse default value
61
+ default_value = None
62
+ if row[4]: # default_value_string
63
+ default_value = parse_default_value(row[4])
64
+
65
+ return ValidationRule(
66
+ component_type=row[0],
67
+ attribute_name=row[1],
68
+ data_type=row[2],
69
+ unit=row[3],
70
+ default_value_string=row[4],
71
+ allowed_storage_types=allowed_storage_types,
72
+ allows_static=allows_static,
73
+ allows_timeseries=allows_timeseries,
74
+ is_required=bool(row[6]),
75
+ is_input=bool(row[7]),
76
+ description=row[8],
77
+ default_value=default_value,
78
+ )
79
+
80
+
81
+ def list_validation_rules(
82
+ conn: sqlite3.Connection, component_type: str
83
+ ) -> List[ValidationRule]:
84
+ """
85
+ List validation rules for a component type.
86
+
87
+ Args:
88
+ conn: Database connection
89
+ component_type: Type of component
90
+
91
+ Returns:
92
+ List of ValidationRule objects
93
+ """
94
+ cursor = conn.execute(
95
+ """
96
+ SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
97
+ is_required, is_input, description
98
+ FROM attribute_validation_rules
99
+ WHERE component_type = ?
100
+ ORDER BY attribute_name
101
+ """,
102
+ (component_type,),
103
+ )
104
+
105
+ rules = []
106
+ for row in cursor.fetchall():
107
+ allowed_storage_types = row[5]
108
+ allows_static = allowed_storage_types in ("static", "static_or_timeseries")
109
+ allows_timeseries = allowed_storage_types in (
110
+ "timeseries",
111
+ "static_or_timeseries",
112
+ )
113
+
114
+ # Parse default value
115
+ default_value = None
116
+ if row[4]: # default_value_string
117
+ default_value = parse_default_value(row[4])
118
+
119
+ rules.append(
120
+ ValidationRule(
121
+ component_type=row[0],
122
+ attribute_name=row[1],
123
+ data_type=row[2],
124
+ unit=row[3],
125
+ default_value_string=row[4],
126
+ allowed_storage_types=allowed_storage_types,
127
+ allows_static=allows_static,
128
+ allows_timeseries=allows_timeseries,
129
+ is_required=bool(row[6]),
130
+ is_input=bool(row[7]),
131
+ description=row[8],
132
+ default_value=default_value,
133
+ )
134
+ )
135
+
136
+ return rules
137
+
138
+
139
+ def get_all_validation_rules(conn: sqlite3.Connection) -> Dict[str, Any]:
140
+ """
141
+ Get all validation rules from the database.
142
+ This replaces the need to load the entire JSON file into memory.
143
+
144
+ Args:
145
+ conn: Database connection
146
+
147
+ Returns:
148
+ Dictionary mapping component types to their validation rules
149
+ """
150
+ try:
151
+ cursor = conn.execute(
152
+ """
153
+ SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
154
+ is_required, is_input, description
155
+ FROM attribute_validation_rules
156
+ """
157
+ )
158
+
159
+ rules = {}
160
+ for row in cursor.fetchall():
161
+ component_type = row[0]
162
+ attribute_name = row[1]
163
+ data_type = row[2]
164
+ unit = row[3]
165
+ default_value = row[4]
166
+ allowed_storage_types = row[5]
167
+ is_required = bool(row[6])
168
+ is_input = bool(row[7])
169
+ description = row[8]
170
+
171
+ if component_type not in rules:
172
+ rules[component_type] = {}
173
+
174
+ rules[component_type][attribute_name] = {
175
+ "data_type": data_type,
176
+ "unit": unit,
177
+ "default_value": default_value,
178
+ "allowed_storage_types": allowed_storage_types,
179
+ "is_required": is_required,
180
+ "is_input": is_input,
181
+ "description": description,
182
+ }
183
+
184
+ return rules
185
+ except Exception as e:
186
+ logger.error(f"Error getting all validation rules: {e}")
187
+ return {}
188
+
189
+
190
+ def validate_static_value(value: StaticValue, rule: ValidationRule) -> None:
191
+ """
192
+ Validate static value against rule.
193
+
194
+ Args:
195
+ value: StaticValue to validate
196
+ rule: ValidationRule to validate against
197
+
198
+ Raises:
199
+ InvalidDataType: If value type doesn't match rule
200
+ """
201
+ value_type = value.data_type()
202
+
203
+ if value_type != rule.data_type:
204
+ raise InvalidDataType(expected=rule.data_type, actual=value_type)
205
+
206
+
207
+ def validate_timeseries_alignment(
208
+ conn: sqlite3.Connection, timeseries: List[float]
209
+ ) -> TimeseriesValidationResult:
210
+ """
211
+ Validate timeseries alignment with network periods (single network per database).
212
+
213
+ Args:
214
+ conn: Database connection
215
+ timeseries: List of timeseries points to validate
216
+
217
+ Returns:
218
+ TimeseriesValidationResult with validation details
219
+ """
220
+ # Get network time periods
221
+ from pyconvexity.models.network import get_network_time_periods
222
+
223
+ network_periods = get_network_time_periods(conn)
224
+ network_period_indices = {p.period_index for p in network_periods}
225
+
226
+ # Get provided period indices
227
+ provided_period_indices = {p.period_index for p in timeseries}
228
+
229
+ # Find missing and extra periods
230
+ missing_periods = list(network_period_indices - provided_period_indices)
231
+ extra_periods = list(provided_period_indices - network_period_indices)
232
+
233
+ is_valid = len(missing_periods) == 0 and len(extra_periods) == 0
234
+
235
+ return TimeseriesValidationResult(
236
+ is_valid=is_valid,
237
+ missing_periods=missing_periods,
238
+ extra_periods=extra_periods,
239
+ total_network_periods=len(network_periods),
240
+ provided_periods=len(timeseries),
241
+ )
242
+
243
+
244
+ def parse_default_value(s: str) -> Optional[StaticValue]:
245
+ """
246
+ Parse default value string.
247
+
248
+ Args:
249
+ s: String representation of default value
250
+
251
+ Returns:
252
+ StaticValue object or None if parsing fails
253
+ """
254
+ # Try to parse as JSON first
255
+ try:
256
+ value = json.loads(s)
257
+ if isinstance(value, float):
258
+ return StaticValue(value)
259
+ elif isinstance(value, int):
260
+ return StaticValue(value)
261
+ elif isinstance(value, bool):
262
+ return StaticValue(value)
263
+ elif isinstance(value, str):
264
+ return StaticValue(value)
265
+ else:
266
+ return None
267
+ except (json.JSONDecodeError, ValueError):
268
+ # Fallback to string
269
+ return StaticValue(s)
270
+
271
+
272
+ def get_attribute_setter_info(
273
+ conn: sqlite3.Connection,
274
+ component_type: str,
275
+ attribute_name: str,
276
+ ) -> Dict[str, Any]:
277
+ """
278
+ Get the appropriate function name for setting an attribute.
279
+
280
+ Args:
281
+ conn: Database connection
282
+ component_type: Type of component
283
+ attribute_name: Name of the attribute
284
+
285
+ Returns:
286
+ Dictionary with setter function information
287
+
288
+ Raises:
289
+ ValidationError: If attribute or data type is unknown
290
+ """
291
+ rule = get_validation_rule(conn, component_type, attribute_name)
292
+
293
+ function_name = {
294
+ "float": "set_float_attribute",
295
+ "int": "set_integer_attribute",
296
+ "boolean": "set_boolean_attribute",
297
+ "string": "set_string_attribute",
298
+ }.get(rule.data_type)
299
+
300
+ if not function_name:
301
+ raise ValidationError(f"Unknown data type: {rule.data_type}")
302
+
303
+ return {
304
+ "function_name": function_name,
305
+ "data_type": rule.data_type,
306
+ "allows_static": rule.allows_static,
307
+ "allows_timeseries": rule.allows_timeseries,
308
+ "is_required": rule.is_required,
309
+ "default_value": rule.default_value_string,
310
+ "unit": rule.unit,
311
+ "description": rule.description,
312
+ }