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,120 @@
1
+ """
2
+ PyConvexity - Python library for energy system modeling and optimization.
3
+
4
+ This library provides the core functionality of the Convexity desktop application
5
+ as a reusable, pip-installable package for building and solving energy system models.
6
+ """
7
+
8
+ # Version information
9
+ from pyconvexity._version import __version__
10
+
11
+ __author__ = "Convexity Team"
12
+
13
+ # Core imports - always available
14
+ from pyconvexity.core.errors import (
15
+ PyConvexityError,
16
+ DatabaseError,
17
+ ValidationError,
18
+ ComponentNotFound,
19
+ AttributeNotFound,
20
+ )
21
+
22
+ from pyconvexity.core.types import (
23
+ StaticValue,
24
+ TimeseriesPoint,
25
+ Component,
26
+ Network,
27
+ CreateNetworkRequest,
28
+ CreateComponentRequest,
29
+ )
30
+
31
+ from pyconvexity.core.database import (
32
+ create_database_with_schema,
33
+ database_context,
34
+ open_connection,
35
+ validate_database,
36
+ )
37
+
38
+ # Import main API functions
39
+ from pyconvexity.models import (
40
+ # Component operations
41
+ get_component, create_component, update_component, delete_component,
42
+ list_components_by_type, list_component_attributes,
43
+
44
+ # Attribute operations
45
+ set_static_attribute, set_timeseries_attribute, get_attribute, delete_attribute,
46
+
47
+ # Network operations
48
+ create_network, get_network_info, get_network_time_periods, list_networks,
49
+ create_carrier, list_carriers, get_network_config, set_network_config,
50
+ get_master_scenario_id, resolve_scenario_id,
51
+ )
52
+
53
+ from pyconvexity.validation import (
54
+ get_validation_rule, list_validation_rules, validate_timeseries_alignment
55
+ )
56
+
57
+ # High-level API functions
58
+ __all__ = [
59
+ # Version info
60
+ "__version__",
61
+ "__author__",
62
+
63
+ # Core types
64
+ "StaticValue",
65
+ "TimeseriesPoint",
66
+ "Component",
67
+ "Network",
68
+ "CreateNetworkRequest",
69
+ "CreateComponentRequest",
70
+
71
+ # Database operations
72
+ "create_database_with_schema",
73
+ "database_context",
74
+ "open_connection",
75
+ "validate_database",
76
+
77
+ # Exceptions
78
+ "PyConvexityError",
79
+ "DatabaseError",
80
+ "ValidationError",
81
+ "ComponentNotFound",
82
+ "AttributeNotFound",
83
+
84
+ # Component operations
85
+ "get_component", "create_component", "update_component", "delete_component",
86
+ "list_components_by_type", "list_component_attributes",
87
+
88
+ # Attribute operations
89
+ "set_static_attribute", "set_timeseries_attribute", "get_attribute", "delete_attribute",
90
+
91
+ # Network operations
92
+ "create_network", "get_network_info", "get_network_time_periods", "list_networks",
93
+ "create_carrier", "list_carriers", "get_network_config", "set_network_config",
94
+ "get_master_scenario_id", "resolve_scenario_id",
95
+
96
+ # Validation
97
+ "get_validation_rule", "list_validation_rules", "validate_timeseries_alignment",
98
+ ]
99
+
100
+ # Optional imports with graceful fallbacks
101
+ try:
102
+ from pyconvexity.solvers.pypsa import PyPSASolver
103
+ __all__.append("PyPSASolver")
104
+ except ImportError:
105
+ # PyPSA not available
106
+ pass
107
+
108
+ try:
109
+ from pyconvexity.io.excel import ExcelImporter, ExcelExporter
110
+ __all__.extend(["ExcelImporter", "ExcelExporter"])
111
+ except ImportError:
112
+ # Excel dependencies not available
113
+ pass
114
+
115
+ try:
116
+ from pyconvexity.io.netcdf import NetCDFImporter, NetCDFExporter
117
+ __all__.extend(["NetCDFImporter", "NetCDFExporter"])
118
+ except ImportError:
119
+ # NetCDF dependencies not available
120
+ pass
@@ -0,0 +1,2 @@
1
+ # This file is automatically updated by GitHub Actions during release
2
+ __version__ = "0.1.0" # Default version for local development
@@ -0,0 +1,64 @@
1
+ """
2
+ Core module for PyConvexity.
3
+
4
+ Contains fundamental types, database operations, and error handling.
5
+ """
6
+
7
+ from pyconvexity.core.errors import (
8
+ PyConvexityError,
9
+ DatabaseError,
10
+ ValidationError,
11
+ ComponentNotFound,
12
+ AttributeNotFound,
13
+ InvalidDataType,
14
+ TimeseriesError,
15
+ )
16
+
17
+ from pyconvexity.core.types import (
18
+ StaticValue,
19
+ TimeseriesPoint,
20
+ AttributeValue,
21
+ ValidationRule,
22
+ Component,
23
+ Network,
24
+ TimePeriod,
25
+ TimeseriesValidationResult,
26
+ CreateComponentRequest,
27
+ CreateNetworkRequest,
28
+ )
29
+
30
+ from pyconvexity.core.database import (
31
+ DatabaseContext,
32
+ open_connection,
33
+ validate_database,
34
+ create_database_with_schema,
35
+ )
36
+
37
+ __all__ = [
38
+ # Errors
39
+ "PyConvexityError",
40
+ "DatabaseError",
41
+ "ValidationError",
42
+ "ComponentNotFound",
43
+ "AttributeNotFound",
44
+ "InvalidDataType",
45
+ "TimeseriesError",
46
+
47
+ # Types
48
+ "StaticValue",
49
+ "TimeseriesPoint",
50
+ "AttributeValue",
51
+ "ValidationRule",
52
+ "Component",
53
+ "Network",
54
+ "TimePeriod",
55
+ "TimeseriesValidationResult",
56
+ "CreateComponentRequest",
57
+ "CreateNetworkRequest",
58
+
59
+ # Database
60
+ "DatabaseContext",
61
+ "open_connection",
62
+ "validate_database",
63
+ "create_database_with_schema",
64
+ ]
@@ -0,0 +1,314 @@
1
+ """
2
+ Database connection and schema management for PyConvexity.
3
+
4
+ Provides clean abstractions for database operations with proper connection
5
+ management, schema validation, and resource cleanup.
6
+ """
7
+
8
+ import sqlite3
9
+ import sys
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import Generator, List, Optional
13
+
14
+ from pyconvexity.core.errors import ConnectionError, DatabaseError, ValidationError
15
+
16
+
17
+ class DatabaseContext:
18
+ """
19
+ Context manager for database connections with automatic cleanup.
20
+
21
+ Provides a clean way to manage database connections with proper
22
+ resource cleanup and error handling.
23
+ """
24
+
25
+ def __init__(self, db_path: str, read_only: bool = False):
26
+ self.db_path = db_path
27
+ self.read_only = read_only
28
+ self.connection: Optional[sqlite3.Connection] = None
29
+
30
+ def __enter__(self) -> sqlite3.Connection:
31
+ self.connection = open_connection(self.db_path, read_only=self.read_only)
32
+ return self.connection
33
+
34
+ def __exit__(self, exc_type, exc_val, exc_tb):
35
+ if self.connection:
36
+ if exc_type is None:
37
+ # No exception, commit any pending changes
38
+ self.connection.commit()
39
+ else:
40
+ # Exception occurred, rollback
41
+ self.connection.rollback()
42
+ self.connection.close()
43
+ self.connection = None
44
+
45
+
46
+ @contextmanager
47
+ def database_context(db_path: str, read_only: bool = False) -> Generator[sqlite3.Connection, None, None]:
48
+ """
49
+ Context manager function for database connections.
50
+
51
+ Args:
52
+ db_path: Path to the SQLite database file
53
+ read_only: If True, open in read-only mode
54
+
55
+ Yields:
56
+ sqlite3.Connection: Database connection with proper configuration
57
+
58
+ Example:
59
+ with database_context("model.db") as conn:
60
+ cursor = conn.execute("SELECT * FROM networks")
61
+ networks = cursor.fetchall()
62
+ """
63
+ with DatabaseContext(db_path, read_only) as conn:
64
+ yield conn
65
+
66
+
67
+ def open_connection(db_path: str, read_only: bool = False) -> sqlite3.Connection:
68
+ """
69
+ Open database connection with proper settings.
70
+
71
+ Args:
72
+ db_path: Path to the SQLite database file
73
+ read_only: If True, open in read-only mode
74
+
75
+ Returns:
76
+ sqlite3.Connection: Configured database connection
77
+
78
+ Raises:
79
+ ConnectionError: If database connection fails
80
+ """
81
+ try:
82
+ # Build connection URI for read-only mode if needed
83
+ if read_only:
84
+ uri = f"file:{db_path}?mode=ro"
85
+ conn = sqlite3.connect(uri, uri=True)
86
+ else:
87
+ conn = sqlite3.connect(db_path)
88
+
89
+ # Configure connection
90
+ conn.row_factory = sqlite3.Row # Enable column access by name
91
+ conn.execute("PRAGMA foreign_keys = ON") # Enable foreign key constraints
92
+
93
+ # Set reasonable timeouts
94
+ conn.execute("PRAGMA busy_timeout = 30000") # 30 second timeout
95
+
96
+ return conn
97
+
98
+ except sqlite3.Error as e:
99
+ raise ConnectionError(f"Failed to open database at {db_path}: {e}") from e
100
+
101
+
102
+ def validate_database(conn: sqlite3.Connection) -> None:
103
+ """
104
+ Validate database schema has required tables.
105
+
106
+ Args:
107
+ conn: Database connection to validate
108
+
109
+ Raises:
110
+ ValidationError: If required tables are missing
111
+ """
112
+ required_tables = [
113
+ "networks",
114
+ "components",
115
+ "component_attributes",
116
+ "attribute_validation_rules",
117
+ "carriers",
118
+ "scenarios"
119
+ ]
120
+
121
+ missing_tables = []
122
+
123
+ for table in required_tables:
124
+ cursor = conn.execute(
125
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
126
+ (table,)
127
+ )
128
+ if not cursor.fetchone():
129
+ missing_tables.append(table)
130
+
131
+ if missing_tables:
132
+ raise ValidationError(
133
+ f"Required tables not found in database: {', '.join(missing_tables)}"
134
+ )
135
+
136
+
137
+ def create_database_with_schema(db_path: str) -> None:
138
+ """
139
+ Create a new database and apply the complete schema.
140
+
141
+ Args:
142
+ db_path: Path where the new database should be created
143
+
144
+ Raises:
145
+ DatabaseError: If schema files cannot be found or applied
146
+ """
147
+ db_path_obj = Path(db_path)
148
+
149
+ # Ensure parent directory exists
150
+ if db_path_obj.parent and not db_path_obj.parent.exists():
151
+ db_path_obj.parent.mkdir(parents=True, exist_ok=True)
152
+
153
+ # Remove existing file if it exists, to ensure a clean start
154
+ if db_path_obj.exists():
155
+ db_path_obj.unlink()
156
+
157
+ # Find schema files
158
+ schema_dir = _find_schema_directory()
159
+ if not schema_dir:
160
+ raise DatabaseError("Could not find schema directory")
161
+
162
+ schema_files = [
163
+ "01_core_schema.sql",
164
+ "02_data_metadata.sql",
165
+ "03_validation_data.sql",
166
+ "04_scenario_schema.sql"
167
+ ]
168
+
169
+ # Verify all schema files exist
170
+ missing_files = []
171
+ for filename in schema_files:
172
+ schema_file = schema_dir / filename
173
+ if not schema_file.exists():
174
+ missing_files.append(filename)
175
+
176
+ if missing_files:
177
+ raise DatabaseError(f"Schema files not found: {', '.join(missing_files)}")
178
+
179
+ # Create connection and apply schemas
180
+ try:
181
+ conn = sqlite3.connect(db_path)
182
+
183
+ # Enable foreign key constraints
184
+ conn.execute("PRAGMA foreign_keys = ON")
185
+
186
+ # Execute schemas in order
187
+ for filename in schema_files:
188
+ schema_file = schema_dir / filename
189
+ with open(schema_file, 'r') as f:
190
+ conn.executescript(f.read())
191
+
192
+ conn.close()
193
+
194
+ except sqlite3.Error as e:
195
+ # Clean up partial database on error
196
+ if db_path_obj.exists():
197
+ db_path_obj.unlink()
198
+ raise DatabaseError(f"Failed to create database schema: {e}") from e
199
+
200
+
201
+ def _find_schema_directory() -> Optional[Path]:
202
+ """
203
+ Find the schema directory in various possible locations.
204
+
205
+ Returns:
206
+ Path to schema directory or None if not found
207
+ """
208
+ # Try bundled location first (PyInstaller)
209
+ for p in sys.path:
210
+ candidate = Path(p) / "schema"
211
+ if candidate.exists() and candidate.is_dir():
212
+ return candidate
213
+
214
+ # Try relative to this file (development mode)
215
+ current_file = Path(__file__)
216
+
217
+ # Look for schema in the main project
218
+ # Assuming pyconvexity/src/pyconvexity/core/database.py
219
+ # and schema is at project_root/schema
220
+ project_root = current_file.parent.parent.parent.parent.parent
221
+ dev_schema_dir = project_root / "schema"
222
+ if dev_schema_dir.exists():
223
+ return dev_schema_dir
224
+
225
+ # Try package data location
226
+ try:
227
+ import importlib.resources
228
+ schema_path = importlib.resources.files('pyconvexity') / 'data' / 'schema'
229
+ if schema_path.is_dir():
230
+ return Path(str(schema_path))
231
+ except (ImportError, AttributeError):
232
+ pass
233
+
234
+ return None
235
+
236
+
237
+ def get_database_info(conn: sqlite3.Connection) -> dict:
238
+ """
239
+ Get information about the database structure and contents.
240
+
241
+ Args:
242
+ conn: Database connection
243
+
244
+ Returns:
245
+ Dictionary with database information
246
+ """
247
+ info = {
248
+ "tables": [],
249
+ "networks": 0,
250
+ "components": 0,
251
+ "attributes": 0,
252
+ "scenarios": 0,
253
+ "carriers": 0
254
+ }
255
+
256
+ # Get table list
257
+ cursor = conn.execute(
258
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
259
+ )
260
+ info["tables"] = [row[0] for row in cursor.fetchall()]
261
+
262
+ # Get counts for main entities
263
+ count_queries = {
264
+ "networks": "SELECT COUNT(*) FROM networks",
265
+ "components": "SELECT COUNT(*) FROM components",
266
+ "attributes": "SELECT COUNT(*) FROM component_attributes",
267
+ "scenarios": "SELECT COUNT(*) FROM scenarios",
268
+ "carriers": "SELECT COUNT(*) FROM carriers"
269
+ }
270
+
271
+ for key, query in count_queries.items():
272
+ try:
273
+ cursor = conn.execute(query)
274
+ info[key] = cursor.fetchone()[0]
275
+ except sqlite3.Error:
276
+ # Table might not exist
277
+ info[key] = 0
278
+
279
+ return info
280
+
281
+
282
+ def check_database_compatibility(conn: sqlite3.Connection) -> dict:
283
+ """
284
+ Check if database is compatible with current PyConvexity version.
285
+
286
+ Args:
287
+ conn: Database connection
288
+
289
+ Returns:
290
+ Dictionary with compatibility information
291
+ """
292
+ result = {
293
+ "compatible": True,
294
+ "version": None,
295
+ "issues": [],
296
+ "warnings": []
297
+ }
298
+
299
+ try:
300
+ validate_database(conn)
301
+ except ValidationError as e:
302
+ result["compatible"] = False
303
+ result["issues"].append(str(e))
304
+
305
+ # Check for version information (if we add a version table later)
306
+ try:
307
+ cursor = conn.execute("SELECT version FROM database_version LIMIT 1")
308
+ row = cursor.fetchone()
309
+ if row:
310
+ result["version"] = row[0]
311
+ except sqlite3.Error:
312
+ result["warnings"].append("No version information found in database")
313
+
314
+ return result
@@ -0,0 +1,100 @@
1
+ """
2
+ Error classes for PyConvexity.
3
+
4
+ These mirror the error handling from the original Rust implementation
5
+ while providing Python-specific enhancements.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+
11
+ class PyConvexityError(Exception):
12
+ """Base exception for all PyConvexity errors"""
13
+ pass
14
+
15
+
16
+ class DatabaseError(PyConvexityError):
17
+ """Database-related errors"""
18
+ pass
19
+
20
+
21
+ class ConnectionError(DatabaseError):
22
+ """Database connection failed"""
23
+ pass
24
+
25
+
26
+ class ValidationError(PyConvexityError):
27
+ """Data validation error"""
28
+ pass
29
+
30
+
31
+ class ComponentNotFound(PyConvexityError):
32
+ """Component not found in database"""
33
+
34
+ def __init__(self, component_id: int, message: Optional[str] = None):
35
+ self.component_id = component_id
36
+ if message is None:
37
+ message = f"Component not found: {component_id}"
38
+ super().__init__(message)
39
+
40
+
41
+ class AttributeNotFound(PyConvexityError):
42
+ """Attribute not found for component"""
43
+
44
+ def __init__(self, component_id: int, attribute_name: str, message: Optional[str] = None):
45
+ self.component_id = component_id
46
+ self.attribute_name = attribute_name
47
+ if message is None:
48
+ message = f"Attribute not found: component {component_id}, attribute '{attribute_name}'"
49
+ super().__init__(message)
50
+
51
+
52
+ class InvalidDataType(ValidationError):
53
+ """Invalid data type for attribute"""
54
+
55
+ def __init__(self, expected: str, actual: str, message: Optional[str] = None):
56
+ self.expected = expected
57
+ self.actual = actual
58
+ if message is None:
59
+ message = f"Invalid data type: expected {expected}, got {actual}"
60
+ super().__init__(message)
61
+
62
+
63
+ class TimeseriesError(PyConvexityError):
64
+ """Timeseries serialization/deserialization error"""
65
+ pass
66
+
67
+
68
+ class NetworkNotFound(PyConvexityError):
69
+ """Network not found in database"""
70
+
71
+ def __init__(self, network_id: int, message: Optional[str] = None):
72
+ self.network_id = network_id
73
+ if message is None:
74
+ message = f"Network not found: {network_id}"
75
+ super().__init__(message)
76
+
77
+
78
+ class ScenarioNotFound(PyConvexityError):
79
+ """Scenario not found in database"""
80
+
81
+ def __init__(self, scenario_id: int, message: Optional[str] = None):
82
+ self.scenario_id = scenario_id
83
+ if message is None:
84
+ message = f"Scenario not found: {scenario_id}"
85
+ super().__init__(message)
86
+
87
+
88
+ class CarrierNotFound(PyConvexityError):
89
+ """Carrier not found in database"""
90
+
91
+ def __init__(self, carrier_id: int, message: Optional[str] = None):
92
+ self.carrier_id = carrier_id
93
+ if message is None:
94
+ message = f"Carrier not found: {carrier_id}"
95
+ super().__init__(message)
96
+
97
+
98
+ # Legacy aliases for backward compatibility with existing code
99
+ # These will be deprecated in future versions
100
+ DbError = PyConvexityError