plexos2duckdb 0.1.0b7__py3-none-win_amd64.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.
- plexos2duckdb/__init__.py +9 -0
- plexos2duckdb/api.py +464 -0
- plexos2duckdb/py.typed +0 -0
- plexos2duckdb-0.1.0b7.data/scripts/plexos2duckdb.exe +0 -0
- plexos2duckdb-0.1.0b7.dist-info/METADATA +190 -0
- plexos2duckdb-0.1.0b7.dist-info/RECORD +10 -0
- plexos2duckdb-0.1.0b7.dist-info/WHEEL +4 -0
- plexos2duckdb-0.1.0b7.dist-info/licenses/LICENSE +25 -0
- plexos2duckdb-0.1.0b7.dist-info/licenses/NOTICE.md +14 -0
- plexos2duckdb-0.1.0b7.dist-info/sboms/plexos2duckdb.cyclonedx.json +12303 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Python interface for the ``plexos2duckdb`` command-line tool.
|
|
2
|
+
|
|
3
|
+
This package currently provides a small subprocess-backed wrapper class for
|
|
4
|
+
the installed CLI. Native Rust bindings are not exposed at this time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .api import PLEXOS2DuckDB, PLEXOS2DuckDBError
|
|
8
|
+
|
|
9
|
+
__all__ = ["PLEXOS2DuckDB", "PLEXOS2DuckDBError"]
|
plexos2duckdb/api.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import duckdb
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PLEXOS2DuckDBError(RuntimeError):
|
|
14
|
+
"""Raised when the plexos2duckdb CLI returns an error."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PLEXOS2DuckDB:
|
|
18
|
+
"""Python workflow wrapper around the ``plexos2duckdb`` CLI.
|
|
19
|
+
|
|
20
|
+
Use this class when you want to start from a PLEXOS solution ZIP or XML
|
|
21
|
+
file, convert it to DuckDB, access conversion metadata, inspect table
|
|
22
|
+
inventory, and run SQL queries against the result from Python.
|
|
23
|
+
|
|
24
|
+
Typical workflow:
|
|
25
|
+
|
|
26
|
+
1. Construct a client from the source solution file.
|
|
27
|
+
|
|
28
|
+
>>> client = PLEXOS2DuckDB("Model DAY_AHEAD Solution.zip")
|
|
29
|
+
|
|
30
|
+
2. Convert it to DuckDB. The output path is optional and defaults
|
|
31
|
+
to the input path with a ``.duckdb`` suffix.
|
|
32
|
+
If the output file already exists, the conversion will error out
|
|
33
|
+
unless you pass ``force=True``.
|
|
34
|
+
|
|
35
|
+
>>> output_path = client.convert(force=True)
|
|
36
|
+
|
|
37
|
+
3. Open the generated database as a context manager for repeated queries.
|
|
38
|
+
|
|
39
|
+
>>> with client as db:
|
|
40
|
+
... rows = db.query("SELECT COUNT(*) FROM information_schema.tables")
|
|
41
|
+
|
|
42
|
+
Alternatively, you can use the `query()` method for one-off queries without managing a connection:
|
|
43
|
+
|
|
44
|
+
>>> rows = client.query("SELECT COUNT(*) FROM information_schema.tables")
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
input_path: str | pathlib.Path | None = None,
|
|
50
|
+
*,
|
|
51
|
+
output_path: str | pathlib.Path | None = None,
|
|
52
|
+
executable: str | pathlib.Path | None = None,
|
|
53
|
+
read_only: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._explicit_executable = executable is not None
|
|
56
|
+
self._executable = (
|
|
57
|
+
pathlib.Path(executable)
|
|
58
|
+
if executable is not None
|
|
59
|
+
else self._resolve_executable()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self._input_path = pathlib.Path(input_path) if input_path is not None else None
|
|
63
|
+
self._output_path = self._normalize_output_path(output_path, self._input_path)
|
|
64
|
+
self._read_only = read_only
|
|
65
|
+
self._connection: duckdb.DuckDBPyConnection | None = None
|
|
66
|
+
|
|
67
|
+
def __enter__(self) -> PLEXOS2DuckDB:
|
|
68
|
+
self.open()
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def executable(self) -> pathlib.Path:
|
|
76
|
+
return self._executable
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def input_path(self) -> pathlib.Path | None:
|
|
80
|
+
return self._input_path
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def output_path(self) -> pathlib.Path | None:
|
|
84
|
+
return self._output_path
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def database(self) -> pathlib.Path | None:
|
|
88
|
+
return self._output_path
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def connection(self) -> duckdb.DuckDBPyConnection:
|
|
92
|
+
if self._connection is None:
|
|
93
|
+
raise PLEXOS2DuckDBError(
|
|
94
|
+
"No managed DuckDB connection is open. Run `convert()` first and then use `with client as db`, call `open()`, or use `connect()` for a temporary connection."
|
|
95
|
+
)
|
|
96
|
+
return self._connection
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def metadata(self) -> Table:
|
|
100
|
+
return self._build_metadata_table(self._load_metadata())
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def inventory(self) -> Table:
|
|
104
|
+
return self._build_inventory_table(self._load_inventory())
|
|
105
|
+
|
|
106
|
+
def inspect(
|
|
107
|
+
self,
|
|
108
|
+
database: str | pathlib.Path | None = None,
|
|
109
|
+
*,
|
|
110
|
+
format: str = "table",
|
|
111
|
+
) -> Table | dict[str, Any]:
|
|
112
|
+
database_path = self._resolve_output_path(database)
|
|
113
|
+
completed = self._run_command(
|
|
114
|
+
[
|
|
115
|
+
"inspect",
|
|
116
|
+
"--input",
|
|
117
|
+
str(database_path),
|
|
118
|
+
"--format-diagnostics",
|
|
119
|
+
"json",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
payload = json.loads(completed.stdout)
|
|
123
|
+
if format == "json":
|
|
124
|
+
return payload
|
|
125
|
+
if format != "table":
|
|
126
|
+
raise PLEXOS2DuckDBError("inspect format must be either 'table' or 'json'")
|
|
127
|
+
return self._build_metadata_table(payload["metadata"])
|
|
128
|
+
|
|
129
|
+
def open(
|
|
130
|
+
self,
|
|
131
|
+
database: str | pathlib.Path | None = None,
|
|
132
|
+
*,
|
|
133
|
+
read_only: bool | None = None,
|
|
134
|
+
) -> duckdb.DuckDBPyConnection:
|
|
135
|
+
database_path = self._resolve_output_path(database)
|
|
136
|
+
if read_only is not None:
|
|
137
|
+
self._read_only = read_only
|
|
138
|
+
if self._connection is None:
|
|
139
|
+
self._connection = duckdb.connect(
|
|
140
|
+
str(database_path), read_only=self._read_only
|
|
141
|
+
)
|
|
142
|
+
return self._connection
|
|
143
|
+
|
|
144
|
+
def close(self) -> None:
|
|
145
|
+
if self._connection is not None:
|
|
146
|
+
self._connection.close()
|
|
147
|
+
self._connection = None
|
|
148
|
+
|
|
149
|
+
def _repr_items(self) -> list[tuple[str, Any]]:
|
|
150
|
+
items: list[tuple[str, Any]] = []
|
|
151
|
+
|
|
152
|
+
if self._input_path is not None:
|
|
153
|
+
items.append(("input_path", str(self._input_path)))
|
|
154
|
+
if self._output_path is not None:
|
|
155
|
+
items.append(("output_path", str(self._output_path)))
|
|
156
|
+
if not self._read_only:
|
|
157
|
+
items.append(("read_only", False))
|
|
158
|
+
if self._explicit_executable:
|
|
159
|
+
items.append(("executable", str(self._executable)))
|
|
160
|
+
|
|
161
|
+
return items
|
|
162
|
+
|
|
163
|
+
def __repr__(self) -> str:
|
|
164
|
+
body = ", ".join(f"{key}={value!r}" for key, value in self._repr_items())
|
|
165
|
+
return f"{type(self).__name__}({body})"
|
|
166
|
+
|
|
167
|
+
def _repr_pretty_(self, printer: Any, cycle: bool) -> None:
|
|
168
|
+
if cycle:
|
|
169
|
+
printer.text(f"{type(self).__name__}(...)")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
items = self._repr_items()
|
|
173
|
+
if not items:
|
|
174
|
+
printer.text(f"{type(self).__name__}()")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
printer.text(f"{type(self).__name__}(")
|
|
178
|
+
with printer.group(2, "", ")"):
|
|
179
|
+
for index, (key, value) in enumerate(items):
|
|
180
|
+
if index:
|
|
181
|
+
printer.text(",")
|
|
182
|
+
printer.breakable()
|
|
183
|
+
printer.text(f"{key}=")
|
|
184
|
+
printer.pretty(value)
|
|
185
|
+
|
|
186
|
+
def version(self) -> str:
|
|
187
|
+
completed = self._run_command(["--version"])
|
|
188
|
+
return completed.stdout.strip()
|
|
189
|
+
|
|
190
|
+
def convert(
|
|
191
|
+
self,
|
|
192
|
+
input_path: str | pathlib.Path | None = None,
|
|
193
|
+
output_path: str | pathlib.Path | None = None,
|
|
194
|
+
*,
|
|
195
|
+
force: bool = False,
|
|
196
|
+
in_memory: bool = False,
|
|
197
|
+
n_threads: int | None = None,
|
|
198
|
+
on_event: Callable[[dict[str, Any]], None] | None = None,
|
|
199
|
+
) -> pathlib.Path:
|
|
200
|
+
input_path = self._resolve_input_path(input_path)
|
|
201
|
+
requested_output_path = (
|
|
202
|
+
pathlib.Path(output_path) if output_path is not None else self._output_path
|
|
203
|
+
)
|
|
204
|
+
args = [
|
|
205
|
+
"convert",
|
|
206
|
+
"--input",
|
|
207
|
+
str(input_path),
|
|
208
|
+
"--format-diagnostics",
|
|
209
|
+
"json",
|
|
210
|
+
"--no-progress-bar",
|
|
211
|
+
]
|
|
212
|
+
if requested_output_path is not None:
|
|
213
|
+
args.extend(["--output", str(requested_output_path)])
|
|
214
|
+
if force:
|
|
215
|
+
args.append("--force")
|
|
216
|
+
if in_memory:
|
|
217
|
+
args.append("--in-memory")
|
|
218
|
+
if n_threads is not None:
|
|
219
|
+
args.extend(["--n-threads", str(n_threads)])
|
|
220
|
+
|
|
221
|
+
if on_event is None:
|
|
222
|
+
completed = self._run_command(args)
|
|
223
|
+
events = [
|
|
224
|
+
json.loads(line)
|
|
225
|
+
for line in completed.stdout.splitlines()
|
|
226
|
+
if line.strip()
|
|
227
|
+
]
|
|
228
|
+
else:
|
|
229
|
+
events = self._run_convert_stream(args, on_event)
|
|
230
|
+
|
|
231
|
+
for event in reversed(events):
|
|
232
|
+
if event.get("event") == "completed":
|
|
233
|
+
path = pathlib.Path(event["output"])
|
|
234
|
+
self._input_path = input_path
|
|
235
|
+
self._output_path = path
|
|
236
|
+
return path
|
|
237
|
+
raise PLEXOS2DuckDBError("Conversion did not emit a completion event")
|
|
238
|
+
|
|
239
|
+
def connect(
|
|
240
|
+
self, database: str | pathlib.Path | None = None, *, read_only: bool = True
|
|
241
|
+
) -> duckdb.DuckDBPyConnection:
|
|
242
|
+
database_path = self._resolve_output_path(database)
|
|
243
|
+
return duckdb.connect(str(database_path), read_only=read_only)
|
|
244
|
+
|
|
245
|
+
def query(
|
|
246
|
+
self,
|
|
247
|
+
sql: str,
|
|
248
|
+
parameters: list[Any] | tuple[Any, ...] | None = None,
|
|
249
|
+
*,
|
|
250
|
+
database: str | pathlib.Path | None = None,
|
|
251
|
+
read_only: bool = True,
|
|
252
|
+
) -> list[tuple[Any, ...]]:
|
|
253
|
+
if database is None and self._connection is not None:
|
|
254
|
+
result = self._connection.execute(sql, parameters or [])
|
|
255
|
+
return result.fetchall()
|
|
256
|
+
|
|
257
|
+
connection = self.connect(database, read_only=read_only)
|
|
258
|
+
try:
|
|
259
|
+
result = connection.execute(sql, parameters or [])
|
|
260
|
+
return result.fetchall()
|
|
261
|
+
finally:
|
|
262
|
+
connection.close()
|
|
263
|
+
|
|
264
|
+
def _resolve_executable(self) -> pathlib.Path:
|
|
265
|
+
import sysconfig
|
|
266
|
+
|
|
267
|
+
module_scripts_dir = pathlib.Path(__file__).resolve().parents[2] / "bin"
|
|
268
|
+
candidates = [
|
|
269
|
+
module_scripts_dir / "plexos2duckdb",
|
|
270
|
+
module_scripts_dir / "plexos2duckdb.exe",
|
|
271
|
+
pathlib.Path(sysconfig.get_path("scripts")) / "plexos2duckdb",
|
|
272
|
+
pathlib.Path(sysconfig.get_path("scripts")) / "plexos2duckdb.exe",
|
|
273
|
+
]
|
|
274
|
+
for candidate in candidates:
|
|
275
|
+
if candidate.exists():
|
|
276
|
+
return candidate
|
|
277
|
+
resolved = shutil.which("plexos2duckdb")
|
|
278
|
+
if resolved is not None:
|
|
279
|
+
return pathlib.Path(resolved)
|
|
280
|
+
raise PLEXOS2DuckDBError(
|
|
281
|
+
"Could not find the bundled `plexos2duckdb` executable or a PATH fallback. Install the package with `pip install plexos2duckdb` or pass `executable=` explicitly."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _quote_ident(self, identifier: str) -> str:
|
|
285
|
+
return '"' + identifier.replace('"', '""') + '"'
|
|
286
|
+
|
|
287
|
+
def _load_metadata(
|
|
288
|
+
self, database: str | pathlib.Path | None = None
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
database_path = self._resolve_output_path(database)
|
|
291
|
+
connection = self.connect(database_path)
|
|
292
|
+
try:
|
|
293
|
+
rows = connection.execute(
|
|
294
|
+
"SELECT key, value FROM main.plexos2duckdb"
|
|
295
|
+
).fetchall()
|
|
296
|
+
finally:
|
|
297
|
+
connection.close()
|
|
298
|
+
|
|
299
|
+
metadata = {key: value for key, value in rows}
|
|
300
|
+
return {
|
|
301
|
+
"database": str(database_path),
|
|
302
|
+
"converter_version": metadata.get("plexos2duckdb_version", ""),
|
|
303
|
+
"source_file": metadata.get("plexos_file", ""),
|
|
304
|
+
"model_name": metadata.get("model_name", ""),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def _load_inventory(
|
|
308
|
+
self, database: str | pathlib.Path | None = None
|
|
309
|
+
) -> list[dict[str, Any]]:
|
|
310
|
+
connection = self.connect(database)
|
|
311
|
+
try:
|
|
312
|
+
rows = connection.execute(
|
|
313
|
+
"""
|
|
314
|
+
SELECT table_schema, table_name, table_type
|
|
315
|
+
FROM information_schema.tables
|
|
316
|
+
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
|
|
317
|
+
ORDER BY table_schema, table_name
|
|
318
|
+
"""
|
|
319
|
+
).fetchall()
|
|
320
|
+
inventory = []
|
|
321
|
+
for schema, table_name, table_type in rows:
|
|
322
|
+
kind = "table" if table_type == "BASE TABLE" else "view"
|
|
323
|
+
row_count = "-"
|
|
324
|
+
if table_type == "BASE TABLE":
|
|
325
|
+
quoted_schema = self._quote_ident(schema)
|
|
326
|
+
quoted_table = self._quote_ident(table_name)
|
|
327
|
+
count = connection.execute(
|
|
328
|
+
f"SELECT COUNT(*) FROM {quoted_schema}.{quoted_table}"
|
|
329
|
+
).fetchone()[0]
|
|
330
|
+
row_count = str(count)
|
|
331
|
+
inventory.append(
|
|
332
|
+
{
|
|
333
|
+
"schema": schema,
|
|
334
|
+
"table": table_name,
|
|
335
|
+
"kind": kind,
|
|
336
|
+
"row_count": row_count,
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
return inventory
|
|
340
|
+
finally:
|
|
341
|
+
connection.close()
|
|
342
|
+
|
|
343
|
+
def _build_metadata_table(self, metadata: dict[str, Any]) -> Table:
|
|
344
|
+
table = Table(title="PLEXOS2DuckDB Metadata")
|
|
345
|
+
table.add_column("Field")
|
|
346
|
+
table.add_column("Value")
|
|
347
|
+
for key in ("database", "converter_version", "source_file", "model_name"):
|
|
348
|
+
table.add_row(key, str(metadata.get(key, "")))
|
|
349
|
+
return table
|
|
350
|
+
|
|
351
|
+
def _build_inventory_table(self, inventory: list[dict[str, Any]]) -> Table:
|
|
352
|
+
table = Table(title="PLEXOS2DuckDB Inventory")
|
|
353
|
+
table.add_column("Schema")
|
|
354
|
+
table.add_column("Table")
|
|
355
|
+
table.add_column("Kind")
|
|
356
|
+
table.add_column("Row Count", justify="right")
|
|
357
|
+
for row in inventory:
|
|
358
|
+
table.add_row(
|
|
359
|
+
str(row.get("schema", "")),
|
|
360
|
+
str(row.get("table", "")),
|
|
361
|
+
str(row.get("kind", "")),
|
|
362
|
+
str(row.get("row_count", "")),
|
|
363
|
+
)
|
|
364
|
+
return table
|
|
365
|
+
|
|
366
|
+
def _display_state(self) -> dict[str, Any]:
|
|
367
|
+
return {
|
|
368
|
+
"input_path": str(self._input_path)
|
|
369
|
+
if self._input_path is not None
|
|
370
|
+
else None,
|
|
371
|
+
"output_path": str(self._output_path)
|
|
372
|
+
if self._output_path is not None
|
|
373
|
+
else None,
|
|
374
|
+
"output_exists": self._output_path.exists()
|
|
375
|
+
if self._output_path is not None
|
|
376
|
+
else False,
|
|
377
|
+
"connection_open": self._connection is not None,
|
|
378
|
+
"read_only": self._read_only,
|
|
379
|
+
"executable": str(self._executable),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def _normalize_output_path(
|
|
383
|
+
self,
|
|
384
|
+
output_path: str | pathlib.Path | None,
|
|
385
|
+
input_path: pathlib.Path | None,
|
|
386
|
+
) -> pathlib.Path | None:
|
|
387
|
+
if output_path is not None:
|
|
388
|
+
path = pathlib.Path(output_path)
|
|
389
|
+
return path if path.suffix else path.with_suffix(".duckdb")
|
|
390
|
+
if input_path is None:
|
|
391
|
+
return None
|
|
392
|
+
if input_path.suffix.lower() in {".zip", ".xml"}:
|
|
393
|
+
candidate = input_path.with_suffix(".duckdb")
|
|
394
|
+
return candidate if candidate.exists() else None
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def _resolve_input_path(
|
|
398
|
+
self, input_path: str | pathlib.Path | None
|
|
399
|
+
) -> pathlib.Path:
|
|
400
|
+
if input_path is not None:
|
|
401
|
+
return pathlib.Path(input_path)
|
|
402
|
+
if self._input_path is not None:
|
|
403
|
+
return self._input_path
|
|
404
|
+
raise PLEXOS2DuckDBError("An input solution path is required for conversion")
|
|
405
|
+
|
|
406
|
+
def _resolve_output_path(self, database: str | pathlib.Path | None) -> pathlib.Path:
|
|
407
|
+
if database is not None:
|
|
408
|
+
return pathlib.Path(database)
|
|
409
|
+
if self._output_path is not None:
|
|
410
|
+
return self._output_path
|
|
411
|
+
raise PLEXOS2DuckDBError(
|
|
412
|
+
"An output DuckDB path is required for this operation. Run `convert()` first, pass `output_path=` in the constructor, or provide `database=` explicitly."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _run_convert_stream(
|
|
416
|
+
self,
|
|
417
|
+
args: list[str],
|
|
418
|
+
on_event: Callable[[dict[str, Any]], None],
|
|
419
|
+
) -> list[dict[str, Any]]:
|
|
420
|
+
process = subprocess.Popen(
|
|
421
|
+
[str(self._executable), *args],
|
|
422
|
+
text=True,
|
|
423
|
+
stdout=subprocess.PIPE,
|
|
424
|
+
stderr=subprocess.PIPE,
|
|
425
|
+
)
|
|
426
|
+
events: list[dict[str, Any]] = []
|
|
427
|
+
stdout = process.stdout
|
|
428
|
+
if stdout is None:
|
|
429
|
+
process.kill()
|
|
430
|
+
raise PLEXOS2DuckDBError("Failed to capture conversion output")
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
for line in stdout:
|
|
434
|
+
line = line.strip()
|
|
435
|
+
if not line:
|
|
436
|
+
continue
|
|
437
|
+
event = json.loads(line)
|
|
438
|
+
events.append(event)
|
|
439
|
+
on_event(event)
|
|
440
|
+
finally:
|
|
441
|
+
stdout.close()
|
|
442
|
+
|
|
443
|
+
stderr = process.stderr.read() if process.stderr is not None else ""
|
|
444
|
+
if process.stderr is not None:
|
|
445
|
+
process.stderr.close()
|
|
446
|
+
return_code = process.wait()
|
|
447
|
+
if return_code != 0:
|
|
448
|
+
message = stderr.strip() or "Unknown error"
|
|
449
|
+
raise PLEXOS2DuckDBError(message)
|
|
450
|
+
return events
|
|
451
|
+
|
|
452
|
+
def _run_command(self, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
453
|
+
completed = subprocess.run(
|
|
454
|
+
[str(self._executable), *args],
|
|
455
|
+
text=True,
|
|
456
|
+
capture_output=True,
|
|
457
|
+
check=False,
|
|
458
|
+
)
|
|
459
|
+
if completed.returncode != 0:
|
|
460
|
+
message = (
|
|
461
|
+
completed.stderr.strip() or completed.stdout.strip() or "Unknown error"
|
|
462
|
+
)
|
|
463
|
+
raise PLEXOS2DuckDBError(message)
|
|
464
|
+
return completed
|
plexos2duckdb/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plexos2duckdb
|
|
3
|
+
Version: 0.1.0b7
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Rust
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Dist: duckdb>=1.4.0
|
|
18
|
+
Requires-Dist: rich>=13.0.0
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: NOTICE.md
|
|
21
|
+
Summary: A command line tool to convert PLEXOS solution files to a DuckDB database.
|
|
22
|
+
Keywords: plexos,duckdb,cli,energy
|
|
23
|
+
Home-Page: https://github.com/epri-dev/plexos2duckdb
|
|
24
|
+
Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
|
|
25
|
+
License-Expression: BSD-3-Clause
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
28
|
+
Project-URL: Documentation, https://github.com/epri-dev/plexos2duckdb
|
|
29
|
+
Project-URL: Homepage, https://github.com/epri-dev/plexos2duckdb
|
|
30
|
+
Project-URL: Issues, https://github.com/epri-dev/plexos2duckdb/issues
|
|
31
|
+
Project-URL: Repository, https://github.com/epri-dev/plexos2duckdb
|
|
32
|
+
|
|
33
|
+
# plexos2duckdb
|
|
34
|
+
|
|
35
|
+
[](https://github.com/epri-dev/plexos2duckdb/releases)
|
|
36
|
+
[](https://github.com/epri-dev/plexos2duckdb/blob/main/LICENSE)
|
|
37
|
+
[]()
|
|
38
|
+
[](https://github.com/epri-dev/plexos2duckdb/releases)
|
|
39
|
+
|
|
40
|
+
This is a command line tool to convert PLEXOS solution files to a DuckDB database.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Option 1: Install with pip
|
|
45
|
+
|
|
46
|
+
On supported platforms, you can install the command-line tool directly with pip:
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
pip install plexos2duckdb
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then verify the install:
|
|
53
|
+
|
|
54
|
+
```shell
|
|
55
|
+
plexos2duckdb --version
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Note that this requires a published wheel for your platform.
|
|
59
|
+
|
|
60
|
+
Basic Python usage:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from plexos2duckdb import PLEXOS2DuckDB
|
|
64
|
+
|
|
65
|
+
client = PLEXOS2DuckDB("Model DAY_AHEAD Solution.zip")
|
|
66
|
+
output_path = client.convert()
|
|
67
|
+
|
|
68
|
+
with client as db:
|
|
69
|
+
rows = db.query("SELECT COUNT(*) FROM information_schema.tables")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Option 2: Download prebuilt binary
|
|
73
|
+
|
|
74
|
+
Prebuilt binaries are available from the
|
|
75
|
+
[GitHub Releases](https://github.com/epri-dev/plexos2duckdb/releases) page.
|
|
76
|
+
|
|
77
|
+
1. Go to the [latest release](https://github.com/epri-dev/plexos2duckdb/releases/latest) page on
|
|
78
|
+
GitHub.
|
|
79
|
+
2. Download the appropriate binary for your operating system.
|
|
80
|
+
3. Extract the archive:
|
|
81
|
+
|
|
82
|
+
```shell
|
|
83
|
+
tar -xzf plexos2duckdb-<platform>.tar.gz
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
or on Windows:
|
|
87
|
+
|
|
88
|
+
```powershell
|
|
89
|
+
Expand-Archive plexos2duckdb-<platform>.zip -DestinationPath .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
4. Copy the binary to a directory in your `PATH`, e.g.:
|
|
93
|
+
|
|
94
|
+
```shell
|
|
95
|
+
# MacOS/Linux
|
|
96
|
+
cp plexos2duckdb ~/local/bin/
|
|
97
|
+
# Windows
|
|
98
|
+
copy plexos2duckdb.exe %USERPROFILE%\local\bin\
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You will have to make sure `~/local/bin/` is in your `PATH`.
|
|
102
|
+
|
|
103
|
+
### Option 3: Build from Source
|
|
104
|
+
|
|
105
|
+
If you prefer to build from source, ensure you have [Rust](https://www.rust-lang.org/tools/install)
|
|
106
|
+
and [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) installed.
|
|
107
|
+
|
|
108
|
+
1. Clone the repository:
|
|
109
|
+
|
|
110
|
+
```shell
|
|
111
|
+
git clone https://github.com/epri-dev/plexos2duckdb.git
|
|
112
|
+
cd plexos2duckdb
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
2. Build the binary:
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
cargo build --release
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The compiled binary will be located at:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
target/release/plexos2duckdb
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
3. Copy the binary to a directory in your `PATH`:
|
|
128
|
+
|
|
129
|
+
```shell
|
|
130
|
+
# MacOS/Linux
|
|
131
|
+
cp target/release/plexos2duckdb ~/local/bin/
|
|
132
|
+
# Windows
|
|
133
|
+
copy target\release\plexos2duckdb.exe %USERPROFILE%\local\bin\
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
You will have to make sure `~/local/bin/` is in your `PATH`.
|
|
137
|
+
|
|
138
|
+
## Quickstart
|
|
139
|
+
|
|
140
|
+
Verify installation is successful by checking the version:
|
|
141
|
+
|
|
142
|
+
```shell
|
|
143
|
+
plexos2duckdb --version
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run the help command to see available options:
|
|
147
|
+
|
|
148
|
+
```shell
|
|
149
|
+
plexos2duckdb --help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Convert a solution zip file to a duckdb database:
|
|
153
|
+
|
|
154
|
+
```shell
|
|
155
|
+
plexos2duckdb convert --input "Model-DayAhead-Solution.zip" --output "Model-DayAhead-Solution.duckdb"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
If the output database already exists, re-run with `--force` to overwrite it:
|
|
159
|
+
|
|
160
|
+
```shell
|
|
161
|
+
plexos2duckdb convert --input "Model-DayAhead-Solution.zip" --output "Model-DayAhead-Solution.duckdb" --force
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Inspect an existing database to view metadata, source file info, model name, and table inventory
|
|
165
|
+
with row counts:
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
plexos2duckdb inspect --input "Model-DayAhead-Solution.duckdb"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For machine-readable diagnostics in automation or CI, use JSON output:
|
|
172
|
+
|
|
173
|
+
```shell
|
|
174
|
+
plexos2duckdb inspect --input "Model-DayAhead-Solution.duckdb" --format-diagnostics json
|
|
175
|
+
plexos2duckdb convert --input "Model-DayAhead-Solution.zip" --output "Model-DayAhead-Solution.duckdb" --format-diagnostics json
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Generate shell completions to stdout with the `generate-shell-completions` subcommand:
|
|
179
|
+
|
|
180
|
+
```shell
|
|
181
|
+
plexos2duckdb generate-shell-completions bash > ~/.local/share/bash-completion/completions/plexos2duckdb
|
|
182
|
+
plexos2duckdb generate-shell-completions zsh > ~/.zfunc/_plexos2duckdb
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
You may use any
|
|
186
|
+
[duckdb compatible database viewer](https://duckdb.org/docs/stable/core_extensions/ui) to
|
|
187
|
+
interactively explore the data with SQL:
|
|
188
|
+
|
|
189
|
+
<img width="1728" height="775" alt="image" src="https://github.com/user-attachments/assets/ad829556-bef1-4982-b7b3-f7a62d225985" />
|
|
190
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
plexos2duckdb/__init__.py,sha256=Vki08dnr0iGk9-Dn6UcrJT59_kyVEs9aIaol2UpM2Vg,328
|
|
2
|
+
plexos2duckdb/api.py,sha256=GqtJ3zXf2qbUEq0Ffc-CmmRmHLHnkdcEhKJZT8pXkaY,16847
|
|
3
|
+
plexos2duckdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
plexos2duckdb-0.1.0b7.data/scripts/plexos2duckdb.exe,sha256=8yj-cvSlzjYprHTbcPUEVH4j5UF6FarZMAwdaLenJkg,25124864
|
|
5
|
+
plexos2duckdb-0.1.0b7.dist-info/METADATA,sha256=aC-CbM7Knu6hr6KyaEGGgC49q8H00fA2bgyLh-8o70E,5885
|
|
6
|
+
plexos2duckdb-0.1.0b7.dist-info/WHEEL,sha256=uJOc2U-Q1x95AlblQcqMRb3iR4QnPtdI7X2ycPN99rM,94
|
|
7
|
+
plexos2duckdb-0.1.0b7.dist-info/licenses/LICENSE,sha256=EhG_vWNKAkrkYgHCPApwnrGsxsuYpOnV6NutaHuNowI,1489
|
|
8
|
+
plexos2duckdb-0.1.0b7.dist-info/licenses/NOTICE.md,sha256=fo78WZqgNyQfbkRTPXFeZhcqUCUIXHXoWkmcr_VVm6k,687
|
|
9
|
+
plexos2duckdb-0.1.0b7.dist-info/sboms/plexos2duckdb.cyclonedx.json,sha256=69mTWJSaz-sX8p0eWFyb3i0biDDI4HFaY-m637NFkN0,392342
|
|
10
|
+
plexos2duckdb-0.1.0b7.dist-info/RECORD,,
|