db-process 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (26) hide show
  1. {db_process-0.1.0 → db_process-0.2.0}/LICENSE +2 -2
  2. db_process-0.2.0/PKG-INFO +226 -0
  3. db_process-0.2.0/README.md +210 -0
  4. {db_process-0.1.0 → db_process-0.2.0}/db_process/__init__.py +20 -1
  5. db_process-0.2.0/db_process/__main__.py +6 -0
  6. db_process-0.2.0/db_process/cli.py +152 -0
  7. db_process-0.2.0/db_process/diagnostics.py +308 -0
  8. {db_process-0.1.0 → db_process-0.2.0}/db_process/runner.py +32 -0
  9. db_process-0.2.0/db_process.egg-info/PKG-INFO +226 -0
  10. {db_process-0.1.0 → db_process-0.2.0}/db_process.egg-info/SOURCES.txt +8 -1
  11. db_process-0.2.0/db_process.egg-info/entry_points.txt +2 -0
  12. {db_process-0.1.0 → db_process-0.2.0}/pyproject.toml +11 -4
  13. db_process-0.2.0/tests/test_cli.py +108 -0
  14. db_process-0.2.0/tests/test_diagnostics.py +192 -0
  15. db_process-0.2.0/tests/test_status.py +59 -0
  16. db_process-0.1.0/PKG-INFO +0 -18
  17. db_process-0.1.0/README.md +0 -2
  18. db_process-0.1.0/db_process.egg-info/PKG-INFO +0 -18
  19. {db_process-0.1.0 → db_process-0.2.0}/db_process/commands.py +0 -0
  20. {db_process-0.1.0 → db_process-0.2.0}/db_process/executable.py +0 -0
  21. {db_process-0.1.0 → db_process-0.2.0}/db_process.egg-info/dependency_links.txt +0 -0
  22. {db_process-0.1.0 → db_process-0.2.0}/db_process.egg-info/requires.txt +0 -0
  23. {db_process-0.1.0 → db_process-0.2.0}/db_process.egg-info/top_level.txt +0 -0
  24. {db_process-0.1.0 → db_process-0.2.0}/setup.cfg +0 -0
  25. {db_process-0.1.0 → db_process-0.2.0}/tests/test_commands.py +0 -0
  26. {db_process-0.1.0 → db_process-0.2.0}/tests/test_executable.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Tokarzewski
3
+ Copyright (c) 2026 DesignBuilder Software
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-process
3
+ Version: 0.2.0
4
+ Summary: Python interface to DesignBuilder command line processing.
5
+ Author: DesignBuilder Software
6
+ Project-URL: Homepage, https://github.com/DesignBuilderSoftware/db-process
7
+ Project-URL: Bug Tracker, https://github.com/DesignBuilderSoftware/db-process/issues
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: psutil>=5.9.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: ruff; extra == "dev"
15
+ Dynamic: license-file
16
+
17
+ # db-process
18
+
19
+ Python interface to DesignBuilder's command-line driver — typed `/process=` commands,
20
+ subprocess execution, process discovery, and diagnostics-log parsing.
21
+
22
+ DesignBuilder is a Windows GUI app, but it exposes a `/process=` argument
23
+ that runs a sequence of commands non-interactively and exits. `db-process`
24
+ wraps that surface in real Python: dataclass commands instead of stringly-typed
25
+ flags, a `ProcessChain` builder, blocking and non-blocking runners, helpers
26
+ to find/kill the process, and a parser for the rolling diagnostics logs
27
+ DesignBuilder writes under `%LOCALAPPDATA%`.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install db-process
33
+ # or, from the cloned repo:
34
+ pip install -e .[dev]
35
+ ```
36
+
37
+ Requires Python 3.10+ and `psutil`. DesignBuilder itself only runs on Windows;
38
+ on other platforms, the package imports fine but `run()` and the process
39
+ helpers will raise `FileNotFoundError`.
40
+
41
+ ## Quick start
42
+
43
+ ### Python — run a model with a chain of commands
44
+
45
+ ```python
46
+ from db_process import ProcessChain, Screen, run
47
+
48
+ chain = (
49
+ ProcessChain()
50
+ .use_sim_manager()
51
+ .switch_screen(Screen.SIMULATION)
52
+ .sim_start_date(1, 1)
53
+ .sim_end_date(31, 12)
54
+ .change_attribute("OccupancyValue", 0.5)
55
+ .run()
56
+ )
57
+
58
+ result = run("models/office.dsb", chain, timeout=600)
59
+ print(result.success, result.duration_seconds)
60
+ ```
61
+
62
+ ### Python — convenience factories for common runs
63
+
64
+ ```python
65
+ from db_process import eplus_simulation, run
66
+
67
+ # A pre-built ProcessChain for "open model, run EnergyPlus, close".
68
+ result = run("models/office.dsb", eplus_simulation())
69
+ ```
70
+
71
+ Other factories: `sbem_calculation`, `export_xml`, `heating_and_cooling_design`,
72
+ `daylighting`, `cfd_simulation`.
73
+
74
+ ### Python — non-blocking with idle-detection kill
75
+
76
+ ```python
77
+ from db_process import run_async
78
+
79
+ handle = run_async("models/office.dsb")
80
+ # Block until DesignBuilder finishes its work and CPU drops, then kill it.
81
+ handle.kill_when_idle(idle_threshold=10, cpu_threshold=0.1)
82
+ ```
83
+
84
+ `kill_when_idle` is the workaround for runs where DesignBuilder finishes the
85
+ calculation but doesn't exit — common with the GUI Sim Manager path.
86
+
87
+ ### CLI
88
+
89
+ ```bash
90
+ db-process status # is DesignBuilder running? show pid + exe path
91
+ db-process open # launch DesignBuilder
92
+ db-process open path/to.dsb # launch with a model
93
+ db-process close # kill any running DesignBuilder
94
+ db-process restart [model] # close + open (with a 1.5 s settle)
95
+
96
+ db-process diag list # list recent diagnostic logs (newest first)
97
+ db-process diag latest # print a one-paragraph summary of the newest log
98
+ db-process diag latest --errors # also list every error line found
99
+ db-process diag summary <path> # summarise a specific log
100
+ ```
101
+
102
+ `db-process` is the entry point installed by the package; you can also run it
103
+ as `python -m db_process`.
104
+
105
+ ## Public API
106
+
107
+ ```python
108
+ from db_process import (
109
+ # Executable discovery
110
+ find_designbuilder, # () -> Path
111
+
112
+ # Process status
113
+ is_running, find_process, status, # status() -> ProcessStatus
114
+ kill_process, kill_when_idle,
115
+ ProcessStatus, # is_running, pid, exe_path
116
+
117
+ # Run
118
+ run, run_async, RunHandle, RunResult,
119
+
120
+ # Command model
121
+ ProcessChain, Screen,
122
+ SwitchScreen, RunCalculation, TabChange,
123
+ SimStartDate, SimEndDate, ChangeAttributeValue,
124
+ ExternalCommand, ImportModelData, ImportLibraryData,
125
+ ExportAsXML, UseSimManager, NoClose,
126
+
127
+ # Convenience factories
128
+ eplus_simulation, sbem_calculation, export_xml,
129
+ heating_and_cooling_design, daylighting, cfd_simulation,
130
+
131
+ # Diagnostics logs
132
+ DiagnosticsLog, find_logs, latest_log,
133
+ parse_filename_timestamp, DEFAULT_DIAGNOSTICS_DIR,
134
+ )
135
+ ```
136
+
137
+ ### Executable discovery
138
+
139
+ `find_designbuilder(exe_path=None)` resolves DesignBuilder.exe in this order:
140
+
141
+ 1. The explicit `exe_path` argument, if given.
142
+ 2. `DESIGNBUILDER_EXE` environment variable.
143
+ 3. `C:\Program Files (x86)\DesignBuilder\DesignBuilder.exe`,
144
+ then `C:\Program Files\DesignBuilder\DesignBuilder.exe`.
145
+ 4. The system `PATH` (`shutil.which("DesignBuilder")`).
146
+
147
+ Raises `FileNotFoundError` with a hint to set `DESIGNBUILDER_EXE` if none match.
148
+
149
+ ### Process status
150
+
151
+ `status()` is a one-shot snapshot:
152
+
153
+ ```python
154
+ from db_process import status
155
+
156
+ s = status()
157
+ # ProcessStatus(is_running=True, pid=12345, exe_path='C:\\...\\DesignBuilder.exe')
158
+ if not s.is_running and s.exe_path:
159
+ # Discovered but not running — safe to launch.
160
+ ...
161
+ ```
162
+
163
+ `exe_path` is populated regardless of whether the process is running, so
164
+ callers can decide whether to launch without a second `find_designbuilder`
165
+ call.
166
+
167
+ ### `ProcessChain` — building a `/process=` argument
168
+
169
+ `ProcessChain` is a fluent builder for the `/process=` command sequence
170
+ documented in DesignBuilder Help. Example output of `chain.to_string()`:
171
+
172
+ ```
173
+ /process=UseSimManager, miGSS, SimStartDate 1 1, SimEndDate 31 12,
174
+ ChangeAttributeValue OccupancyValue 0.5, miTUpdate
175
+ ```
176
+
177
+ Every command is also exposed as a plain dataclass, so you can construct
178
+ chains without the builder when that's clearer.
179
+
180
+ ### Diagnostics logs
181
+
182
+ DesignBuilder writes a per-session diagnostics log to
183
+ `%LOCALAPPDATA%\DesignBuilder\Diagnostics\` named like
184
+ `DesignBuilder_diagnostic_2026-04-23_15-12-08.txt`. `DiagnosticsLog`
185
+ parses one of those files into a structured object:
186
+
187
+ ```python
188
+ from db_process import latest_log, DiagnosticsLog
189
+
190
+ log = DiagnosticsLog.from_file(latest_log())
191
+ print(log.summary())
192
+ print(log.is_complete, log.duration, log.error_count)
193
+ for line_no, text in log.errors:
194
+ print(f"L{line_no}: {text}")
195
+ ```
196
+
197
+ Error detection uses word-boundary matching (`error`, `failed`, `exception`,
198
+ `fatal`, `crash`) plus a small negative list to suppress known benign markers
199
+ (e.g. the `GL_KHR_no_error` OpenGL extension).
200
+
201
+ ## Compatibility
202
+
203
+ - **DesignBuilder**: any version that supports the `/process=` argument.
204
+ Tested against the v25.1 / v26.1 lines.
205
+ - **Python**: 3.10+.
206
+ - **OS**: Windows for live runs. The command model and diagnostics parser
207
+ are pure Python and import on any platform — useful for unit tests on
208
+ Linux CI.
209
+
210
+ ## Development
211
+
212
+ ```bash
213
+ pip install -e .[dev]
214
+ pytest
215
+ ruff check .
216
+ ```
217
+
218
+ Tests are split by area:
219
+
220
+ | File | Covers |
221
+ | --- | --- |
222
+ | `test_commands.py` | `ProcessChain` building + every command's `to_string()` |
223
+ | `test_executable.py` | `find_designbuilder` resolution order |
224
+ | `test_status.py` | `status` / `ProcessStatus` |
225
+ | `test_diagnostics.py` | log parsing, error detection, filename timestamps |
226
+ | `test_cli.py` | CLI argparse wiring (mocked subprocess) |
@@ -0,0 +1,210 @@
1
+ # db-process
2
+
3
+ Python interface to DesignBuilder's command-line driver — typed `/process=` commands,
4
+ subprocess execution, process discovery, and diagnostics-log parsing.
5
+
6
+ DesignBuilder is a Windows GUI app, but it exposes a `/process=` argument
7
+ that runs a sequence of commands non-interactively and exits. `db-process`
8
+ wraps that surface in real Python: dataclass commands instead of stringly-typed
9
+ flags, a `ProcessChain` builder, blocking and non-blocking runners, helpers
10
+ to find/kill the process, and a parser for the rolling diagnostics logs
11
+ DesignBuilder writes under `%LOCALAPPDATA%`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install db-process
17
+ # or, from the cloned repo:
18
+ pip install -e .[dev]
19
+ ```
20
+
21
+ Requires Python 3.10+ and `psutil`. DesignBuilder itself only runs on Windows;
22
+ on other platforms, the package imports fine but `run()` and the process
23
+ helpers will raise `FileNotFoundError`.
24
+
25
+ ## Quick start
26
+
27
+ ### Python — run a model with a chain of commands
28
+
29
+ ```python
30
+ from db_process import ProcessChain, Screen, run
31
+
32
+ chain = (
33
+ ProcessChain()
34
+ .use_sim_manager()
35
+ .switch_screen(Screen.SIMULATION)
36
+ .sim_start_date(1, 1)
37
+ .sim_end_date(31, 12)
38
+ .change_attribute("OccupancyValue", 0.5)
39
+ .run()
40
+ )
41
+
42
+ result = run("models/office.dsb", chain, timeout=600)
43
+ print(result.success, result.duration_seconds)
44
+ ```
45
+
46
+ ### Python — convenience factories for common runs
47
+
48
+ ```python
49
+ from db_process import eplus_simulation, run
50
+
51
+ # A pre-built ProcessChain for "open model, run EnergyPlus, close".
52
+ result = run("models/office.dsb", eplus_simulation())
53
+ ```
54
+
55
+ Other factories: `sbem_calculation`, `export_xml`, `heating_and_cooling_design`,
56
+ `daylighting`, `cfd_simulation`.
57
+
58
+ ### Python — non-blocking with idle-detection kill
59
+
60
+ ```python
61
+ from db_process import run_async
62
+
63
+ handle = run_async("models/office.dsb")
64
+ # Block until DesignBuilder finishes its work and CPU drops, then kill it.
65
+ handle.kill_when_idle(idle_threshold=10, cpu_threshold=0.1)
66
+ ```
67
+
68
+ `kill_when_idle` is the workaround for runs where DesignBuilder finishes the
69
+ calculation but doesn't exit — common with the GUI Sim Manager path.
70
+
71
+ ### CLI
72
+
73
+ ```bash
74
+ db-process status # is DesignBuilder running? show pid + exe path
75
+ db-process open # launch DesignBuilder
76
+ db-process open path/to.dsb # launch with a model
77
+ db-process close # kill any running DesignBuilder
78
+ db-process restart [model] # close + open (with a 1.5 s settle)
79
+
80
+ db-process diag list # list recent diagnostic logs (newest first)
81
+ db-process diag latest # print a one-paragraph summary of the newest log
82
+ db-process diag latest --errors # also list every error line found
83
+ db-process diag summary <path> # summarise a specific log
84
+ ```
85
+
86
+ `db-process` is the entry point installed by the package; you can also run it
87
+ as `python -m db_process`.
88
+
89
+ ## Public API
90
+
91
+ ```python
92
+ from db_process import (
93
+ # Executable discovery
94
+ find_designbuilder, # () -> Path
95
+
96
+ # Process status
97
+ is_running, find_process, status, # status() -> ProcessStatus
98
+ kill_process, kill_when_idle,
99
+ ProcessStatus, # is_running, pid, exe_path
100
+
101
+ # Run
102
+ run, run_async, RunHandle, RunResult,
103
+
104
+ # Command model
105
+ ProcessChain, Screen,
106
+ SwitchScreen, RunCalculation, TabChange,
107
+ SimStartDate, SimEndDate, ChangeAttributeValue,
108
+ ExternalCommand, ImportModelData, ImportLibraryData,
109
+ ExportAsXML, UseSimManager, NoClose,
110
+
111
+ # Convenience factories
112
+ eplus_simulation, sbem_calculation, export_xml,
113
+ heating_and_cooling_design, daylighting, cfd_simulation,
114
+
115
+ # Diagnostics logs
116
+ DiagnosticsLog, find_logs, latest_log,
117
+ parse_filename_timestamp, DEFAULT_DIAGNOSTICS_DIR,
118
+ )
119
+ ```
120
+
121
+ ### Executable discovery
122
+
123
+ `find_designbuilder(exe_path=None)` resolves DesignBuilder.exe in this order:
124
+
125
+ 1. The explicit `exe_path` argument, if given.
126
+ 2. `DESIGNBUILDER_EXE` environment variable.
127
+ 3. `C:\Program Files (x86)\DesignBuilder\DesignBuilder.exe`,
128
+ then `C:\Program Files\DesignBuilder\DesignBuilder.exe`.
129
+ 4. The system `PATH` (`shutil.which("DesignBuilder")`).
130
+
131
+ Raises `FileNotFoundError` with a hint to set `DESIGNBUILDER_EXE` if none match.
132
+
133
+ ### Process status
134
+
135
+ `status()` is a one-shot snapshot:
136
+
137
+ ```python
138
+ from db_process import status
139
+
140
+ s = status()
141
+ # ProcessStatus(is_running=True, pid=12345, exe_path='C:\\...\\DesignBuilder.exe')
142
+ if not s.is_running and s.exe_path:
143
+ # Discovered but not running — safe to launch.
144
+ ...
145
+ ```
146
+
147
+ `exe_path` is populated regardless of whether the process is running, so
148
+ callers can decide whether to launch without a second `find_designbuilder`
149
+ call.
150
+
151
+ ### `ProcessChain` — building a `/process=` argument
152
+
153
+ `ProcessChain` is a fluent builder for the `/process=` command sequence
154
+ documented in DesignBuilder Help. Example output of `chain.to_string()`:
155
+
156
+ ```
157
+ /process=UseSimManager, miGSS, SimStartDate 1 1, SimEndDate 31 12,
158
+ ChangeAttributeValue OccupancyValue 0.5, miTUpdate
159
+ ```
160
+
161
+ Every command is also exposed as a plain dataclass, so you can construct
162
+ chains without the builder when that's clearer.
163
+
164
+ ### Diagnostics logs
165
+
166
+ DesignBuilder writes a per-session diagnostics log to
167
+ `%LOCALAPPDATA%\DesignBuilder\Diagnostics\` named like
168
+ `DesignBuilder_diagnostic_2026-04-23_15-12-08.txt`. `DiagnosticsLog`
169
+ parses one of those files into a structured object:
170
+
171
+ ```python
172
+ from db_process import latest_log, DiagnosticsLog
173
+
174
+ log = DiagnosticsLog.from_file(latest_log())
175
+ print(log.summary())
176
+ print(log.is_complete, log.duration, log.error_count)
177
+ for line_no, text in log.errors:
178
+ print(f"L{line_no}: {text}")
179
+ ```
180
+
181
+ Error detection uses word-boundary matching (`error`, `failed`, `exception`,
182
+ `fatal`, `crash`) plus a small negative list to suppress known benign markers
183
+ (e.g. the `GL_KHR_no_error` OpenGL extension).
184
+
185
+ ## Compatibility
186
+
187
+ - **DesignBuilder**: any version that supports the `/process=` argument.
188
+ Tested against the v25.1 / v26.1 lines.
189
+ - **Python**: 3.10+.
190
+ - **OS**: Windows for live runs. The command model and diagnostics parser
191
+ are pure Python and import on any platform — useful for unit tests on
192
+ Linux CI.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ pip install -e .[dev]
198
+ pytest
199
+ ruff check .
200
+ ```
201
+
202
+ Tests are split by area:
203
+
204
+ | File | Covers |
205
+ | --- | --- |
206
+ | `test_commands.py` | `ProcessChain` building + every command's `to_string()` |
207
+ | `test_executable.py` | `find_designbuilder` resolution order |
208
+ | `test_status.py` | `status` / `ProcessStatus` |
209
+ | `test_diagnostics.py` | log parsing, error detection, filename timestamps |
210
+ | `test_cli.py` | CLI argparse wiring (mocked subprocess) |
@@ -36,15 +36,25 @@ from db_process.commands import (
36
36
  heating_and_cooling_design,
37
37
  sbem_calculation,
38
38
  )
39
+ from db_process.diagnostics import (
40
+ DEFAULT_DIAGNOSTICS_DIR,
41
+ DiagnosticsLog,
42
+ find_logs,
43
+ latest_log,
44
+ parse_filename_timestamp,
45
+ )
39
46
  from db_process.executable import find_designbuilder
40
47
  from db_process.runner import (
48
+ ProcessStatus,
41
49
  RunHandle,
42
50
  RunResult,
43
51
  find_process,
52
+ is_running,
44
53
  kill_process,
45
54
  kill_when_idle,
46
55
  run,
47
56
  run_async,
57
+ status,
48
58
  )
49
59
 
50
60
  __all__ = [
@@ -72,12 +82,21 @@ __all__ = [
72
82
  "export_xml",
73
83
  "heating_and_cooling_design",
74
84
  "sbem_calculation",
75
- # Runner
85
+ # Runner / status
76
86
  "RunHandle",
77
87
  "RunResult",
88
+ "ProcessStatus",
78
89
  "find_process",
90
+ "is_running",
79
91
  "kill_process",
80
92
  "kill_when_idle",
81
93
  "run",
82
94
  "run_async",
95
+ "status",
96
+ # Diagnostics
97
+ "DEFAULT_DIAGNOSTICS_DIR",
98
+ "DiagnosticsLog",
99
+ "find_logs",
100
+ "latest_log",
101
+ "parse_filename_timestamp",
83
102
  ]
@@ -0,0 +1,6 @@
1
+ """Allow `python -m db_process [subcommand]`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,152 @@
1
+ """db-process CLI.
2
+
3
+ Small command-line wrapper over the existing process-control primitives:
4
+
5
+ db-process status # is DB running? where's the exe?
6
+ db-process close # kill any running DesignBuilder
7
+ db-process open [MODEL.dsb] # launch DB; opens MODEL if given
8
+ db-process restart [MODEL.dsb] # close + open
9
+
10
+ Equivalent to:
11
+
12
+ python -m db_process <subcommand> [args]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import subprocess
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+
23
+ from .diagnostics import DiagnosticsLog, find_logs, latest_log
24
+ from .executable import find_designbuilder
25
+ from .runner import find_process, is_running, kill_process, run_async, status as proc_status
26
+
27
+
28
+ def cmd_status(args: argparse.Namespace) -> int:
29
+ s = proc_status()
30
+ print(f"DesignBuilder exe : {s.exe_path or '<not found>'}")
31
+ if not s.is_running:
32
+ print("DesignBuilder process : not running")
33
+ return 1
34
+ print(f"DesignBuilder process : pid={s.pid}")
35
+ return 0
36
+
37
+
38
+ def cmd_close(args: argparse.Namespace) -> int:
39
+ if kill_process():
40
+ print("Closed running DesignBuilder.")
41
+ return 0
42
+ print("No DesignBuilder process to close.")
43
+ return 1
44
+
45
+
46
+ def cmd_open(args: argparse.Namespace) -> int:
47
+ exe = find_designbuilder()
48
+ if args.model is not None:
49
+ print(f"Opening {args.model} with {exe} ...")
50
+ run_async(str(args.model))
51
+ else:
52
+ print(f"Launching {exe} (no model)...")
53
+ subprocess.Popen([str(exe)])
54
+ return 0
55
+
56
+
57
+ def cmd_restart(args: argparse.Namespace) -> int:
58
+ if kill_process():
59
+ print(f"Closed existing process; settling for {args.settle}s ...")
60
+ time.sleep(args.settle)
61
+ return cmd_open(args)
62
+
63
+
64
+ def cmd_diag_list(args: argparse.Namespace) -> int:
65
+ paths = find_logs(args.dir)
66
+ if not paths:
67
+ print("No diagnostic logs found.")
68
+ return 1
69
+ for p in paths[: args.limit]:
70
+ size_kb = p.stat().st_size // 1024
71
+ print(f"{p.stat().st_mtime:>13.0f} {size_kb:>5} KB {p.name}")
72
+ return 0
73
+
74
+
75
+ def cmd_diag_latest(args: argparse.Namespace) -> int:
76
+ path = latest_log(args.dir)
77
+ if path is None:
78
+ print("No diagnostic logs found.")
79
+ return 1
80
+ log = DiagnosticsLog.from_file(path)
81
+ print(log.summary())
82
+ if args.errors and log.errors:
83
+ print("\nError lines:")
84
+ for line_no, text in log.errors:
85
+ print(f" {line_no:5}: {text}")
86
+ return 0
87
+
88
+
89
+ def cmd_diag_summary(args: argparse.Namespace) -> int:
90
+ log = DiagnosticsLog.from_file(args.path)
91
+ print(log.summary())
92
+ if args.errors and log.errors:
93
+ print("\nError lines:")
94
+ for line_no, text in log.errors:
95
+ print(f" {line_no:5}: {text}")
96
+ return 0
97
+
98
+
99
+ def build_parser() -> argparse.ArgumentParser:
100
+ p = argparse.ArgumentParser(prog="db-process",
101
+ description="DesignBuilder process control")
102
+ sub = p.add_subparsers(dest="cmd", required=True)
103
+
104
+ sub.add_parser("status", help="Show DesignBuilder exe path and PID").set_defaults(func=cmd_status)
105
+ sub.add_parser("close", help="Kill any running DesignBuilder").set_defaults(func=cmd_close)
106
+
107
+ p_open = sub.add_parser("open", help="Launch DesignBuilder")
108
+ p_open.add_argument("model", nargs="?", type=Path, default=None,
109
+ help="Optional .dsb model to open")
110
+ p_open.set_defaults(func=cmd_open)
111
+
112
+ p_restart = sub.add_parser("restart", help="Close then open DesignBuilder")
113
+ p_restart.add_argument("model", nargs="?", type=Path, default=None,
114
+ help="Optional .dsb model to open after restart")
115
+ p_restart.add_argument("--settle", type=float, default=1.5,
116
+ help="Seconds to wait between close and open (default 1.5)")
117
+ p_restart.set_defaults(func=cmd_restart)
118
+
119
+ # ── diag — diagnostic-log inspection ──────────────────────────────────
120
+ p_diag = sub.add_parser("diag", help="Inspect DesignBuilder diagnostic logs")
121
+ diag_sub = p_diag.add_subparsers(dest="diag_cmd", required=True)
122
+
123
+ p_diag_list = diag_sub.add_parser("list", help="List recent diagnostic logs")
124
+ p_diag_list.add_argument("--dir", type=Path, default=None,
125
+ help="Diagnostics folder (default: LOCALAPPDATA/DesignBuilder/Diagnostics)")
126
+ p_diag_list.add_argument("-n", "--limit", type=int, default=10,
127
+ help="Number of logs to show (default 10)")
128
+ p_diag_list.set_defaults(func=cmd_diag_list)
129
+
130
+ p_diag_latest = diag_sub.add_parser("latest", help="Summarise the newest log")
131
+ p_diag_latest.add_argument("--dir", type=Path, default=None,
132
+ help="Diagnostics folder")
133
+ p_diag_latest.add_argument("--errors", action="store_true",
134
+ help="Also print every error line found")
135
+ p_diag_latest.set_defaults(func=cmd_diag_latest)
136
+
137
+ p_diag_sum = diag_sub.add_parser("summary", help="Summarise a specific log")
138
+ p_diag_sum.add_argument("path", type=Path, help="Path to a diagnostic log")
139
+ p_diag_sum.add_argument("--errors", action="store_true",
140
+ help="Also print every error line found")
141
+ p_diag_sum.set_defaults(func=cmd_diag_summary)
142
+
143
+ return p
144
+
145
+
146
+ def main(argv: list[str] | None = None) -> int:
147
+ args = build_parser().parse_args(argv)
148
+ return args.func(args)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ sys.exit(main())