db-process 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tokarzewski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-process
3
+ Version: 0.1.0
4
+ Summary: Python interface to DesignBuilder command line processing.
5
+ Author-email: Bartlomiej Tokarzewski <bartlomiej.tokarzewski@gmail.com>
6
+ Project-URL: Homepage, https://github.com/tokarzewski/db-process
7
+ Project-URL: Bug Tracker, https://github.com/tokarzewski/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
+ DesignBuilder command line processing
@@ -0,0 +1,2 @@
1
+ # db-process
2
+ DesignBuilder command line processing
@@ -0,0 +1,83 @@
1
+ """
2
+ db-process
3
+ ====================================
4
+ Python interface to DesignBuilder command line processing.
5
+
6
+ This package provides:
7
+
8
+ - **Executable discovery** — find DesignBuilder.exe across env vars,
9
+ default install paths, and the system PATH.
10
+ - **Typed command model** — dataclasses for every ``/process=`` command
11
+ documented in DesignBuilder Help, with a fluent builder API.
12
+ - **Subprocess execution** — blocking and non-blocking modes, timeout
13
+ handling, and idle-detection termination.
14
+ """
15
+
16
+ from db_process.commands import (
17
+ ChangeAttributeValue,
18
+ ExportAsXML,
19
+ ExternalCommand,
20
+ ImportLibraryData,
21
+ ImportModelData,
22
+ NoClose,
23
+ ProcessChain,
24
+ RunCalculation,
25
+ Screen,
26
+ SimEndDate,
27
+ SimStartDate,
28
+ SwitchScreen,
29
+ TabChange,
30
+ UseSimManager,
31
+ # Convenience factories
32
+ cfd_simulation,
33
+ daylighting,
34
+ eplus_simulation,
35
+ export_xml,
36
+ heating_and_cooling_design,
37
+ sbem_calculation,
38
+ )
39
+ from db_process.executable import find_designbuilder
40
+ from db_process.runner import (
41
+ RunHandle,
42
+ RunResult,
43
+ find_process,
44
+ kill_process,
45
+ kill_when_idle,
46
+ run,
47
+ run_async,
48
+ )
49
+
50
+ __all__ = [
51
+ # Executable
52
+ "find_designbuilder",
53
+ # Commands
54
+ "ChangeAttributeValue",
55
+ "ExportAsXML",
56
+ "ExternalCommand",
57
+ "ImportLibraryData",
58
+ "ImportModelData",
59
+ "NoClose",
60
+ "ProcessChain",
61
+ "RunCalculation",
62
+ "Screen",
63
+ "SimEndDate",
64
+ "SimStartDate",
65
+ "SwitchScreen",
66
+ "TabChange",
67
+ "UseSimManager",
68
+ # Convenience factories
69
+ "cfd_simulation",
70
+ "daylighting",
71
+ "eplus_simulation",
72
+ "export_xml",
73
+ "heating_and_cooling_design",
74
+ "sbem_calculation",
75
+ # Runner
76
+ "RunHandle",
77
+ "RunResult",
78
+ "find_process",
79
+ "kill_process",
80
+ "kill_when_idle",
81
+ "run",
82
+ "run_async",
83
+ ]
@@ -0,0 +1,449 @@
1
+ """
2
+ commands.py
3
+ ====================================
4
+ Typed representation of DesignBuilder command line process commands.
5
+
6
+ DesignBuilder CLI syntax::
7
+
8
+ "DesignBuilder.exe" "MyFile.dsb" /process=Command1, Command2, ...
9
+
10
+ This module provides dataclasses for each command type and a
11
+ :class:`ProcessChain` builder to compose them into valid ``/process=`` strings.
12
+
13
+ Reference: DesignBuilder Help > Reference > Command Line Processing
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from typing import Optional, Union
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Screen / view commands
25
+ # ---------------------------------------------------------------------------
26
+
27
+ class Screen(str, Enum):
28
+ """Available DesignBuilder screen commands."""
29
+
30
+ SIMULATION = "miGSS"
31
+ HEATING_DESIGN = "miGHL"
32
+ COOLING_DESIGN = "miGHG"
33
+ DAYLIGHTING = "miGDY"
34
+ SBEM_DSM = "miGCalculate"
35
+ CFD = "miGCFD"
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Individual command dataclasses
40
+ # ---------------------------------------------------------------------------
41
+
42
+ @dataclass(frozen=True)
43
+ class SwitchScreen:
44
+ """Change the view to a specific DesignBuilder screen.
45
+
46
+ Examples: miGSS, miGHL, miGHG, miGDY, miGCalculate, miGCFD
47
+ """
48
+
49
+ screen: Screen
50
+
51
+ def render(self) -> str:
52
+ return self.screen.value
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class RunCalculation:
57
+ """Run the calculation for the current screen (miTUpdate).
58
+
59
+ Generally this command should be placed after screen switching
60
+ and before any ChangeAttributeValue commands that follow it.
61
+ """
62
+
63
+ def render(self) -> str:
64
+ return "miTUpdate"
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class TabChange:
69
+ """Change the view to tab *n* on the current screen.
70
+
71
+ Example: ``TabChange_2`` on the Daylighting screen switches to
72
+ the Annual daylighting tab.
73
+ """
74
+
75
+ tab: int
76
+
77
+ def __post_init__(self) -> None:
78
+ if self.tab < 1:
79
+ raise ValueError(f"Tab number must be >= 1, got {self.tab}")
80
+
81
+ def render(self) -> str:
82
+ return f"TabChange_{self.tab}"
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class SimStartDate:
87
+ """Set simulation start date.
88
+
89
+ Parameters are day and month as integers.
90
+ Example: ``SimStartDate 4 5`` sets start to May 4.
91
+ """
92
+
93
+ day: int
94
+ month: int
95
+
96
+ def __post_init__(self) -> None:
97
+ if not (1 <= self.month <= 12):
98
+ raise ValueError(f"Month must be 1-12, got {self.month}")
99
+ if not (1 <= self.day <= 31):
100
+ raise ValueError(f"Day must be 1-31, got {self.day}")
101
+
102
+ def render(self) -> str:
103
+ return f"SimStartDate {self.day} {self.month}"
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class SimEndDate:
108
+ """Set simulation end date.
109
+
110
+ Parameters are day and month as integers.
111
+ Example: ``SimEndDate 11 5`` sets end to May 11.
112
+ """
113
+
114
+ day: int
115
+ month: int
116
+
117
+ def __post_init__(self) -> None:
118
+ if not (1 <= self.month <= 12):
119
+ raise ValueError(f"Month must be 1-12, got {self.month}")
120
+ if not (1 <= self.day <= 31):
121
+ raise ValueError(f"Day must be 1-31, got {self.day}")
122
+
123
+ def render(self) -> str:
124
+ return f"SimEndDate {self.day} {self.month}"
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class ChangeAttributeValue:
129
+ """Change a model attribute at building level.
130
+
131
+ Attribute value types (from documentation):
132
+ - Numeric enums: integer codes (e.g. ``Layout 0``)
133
+ - Booleans: ``1`` for True, ``0`` for False
134
+ - Real numbers: SI units, ``.`` decimal separator
135
+ - Database table selections: numeric Id (obtain via API)
136
+
137
+ Tip: Enable *Show attribute names in tooltips* in
138
+ Tools > Program Options > Interface to find attribute names.
139
+ """
140
+
141
+ attribute: str
142
+ value: Union[str, int, float]
143
+
144
+ def render(self) -> str:
145
+ return f"ChangeAttributeValue {self.attribute} {self.value}"
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class ExternalCommand:
150
+ """Run an external command with optional arguments.
151
+
152
+ The external executable must be on the system PATH or in the
153
+ working directory.
154
+ """
155
+
156
+ command: str
157
+
158
+ def render(self) -> str:
159
+ return f"ExternalCommand_{self.command}"
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class ImportModelData:
164
+ """Import model data from a CSV file previously exported from the model."""
165
+
166
+ filepath: str
167
+
168
+ def render(self) -> str:
169
+ return f"ImportModelData_{self.filepath}"
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class ImportLibraryData:
174
+ """Import library data from a file."""
175
+
176
+ filepath: str
177
+
178
+ def render(self) -> str:
179
+ return f"ImportLibraryData_{self.filepath}"
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class ExportAsXML:
184
+ """Export the model as a dsbXML file (miFExportAsXML)."""
185
+
186
+ def render(self) -> str:
187
+ return "miFExportAsXML"
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class UseSimManager:
192
+ """Ensure the simulation runs using the Simulation Manager.
193
+
194
+ Uses the Server and EnergyPlus server method settings
195
+ currently configured in the model.
196
+ """
197
+
198
+ def render(self) -> str:
199
+ return "UseSimManager"
200
+
201
+
202
+ @dataclass(frozen=True)
203
+ class NoClose:
204
+ """Prevent DesignBuilder from shutting down after all commands."""
205
+
206
+ def render(self) -> str:
207
+ return "NoClose"
208
+
209
+
210
+ # Union of all command types for type hints.
211
+ Command = Union[
212
+ SwitchScreen,
213
+ RunCalculation,
214
+ TabChange,
215
+ SimStartDate,
216
+ SimEndDate,
217
+ ChangeAttributeValue,
218
+ ExternalCommand,
219
+ ImportModelData,
220
+ ImportLibraryData,
221
+ ExportAsXML,
222
+ UseSimManager,
223
+ NoClose,
224
+ ]
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # ProcessChain builder
229
+ # ---------------------------------------------------------------------------
230
+
231
+ @dataclass
232
+ class ProcessChain:
233
+ """Build a ``/process=`` command string from a sequence of commands.
234
+
235
+ Usage::
236
+
237
+ chain = (
238
+ ProcessChain()
239
+ .use_sim_manager()
240
+ .switch_screen(Screen.SIMULATION)
241
+ .sim_start_date(1, 1)
242
+ .sim_end_date(31, 12)
243
+ .change_attribute("OccupancyValue", 0.5)
244
+ .run()
245
+ )
246
+ print(chain.to_string())
247
+ # /process=UseSimManager, miGSS, SimStartDate 1 1, SimEndDate 31 12,
248
+ # ChangeAttributeValue OccupancyValue 0.5, miTUpdate
249
+ """
250
+
251
+ commands: list[Command] = field(default_factory=list)
252
+
253
+ # -- Fluent builder methods -----------------------------------------------
254
+
255
+ def add(self, command: Command) -> ProcessChain:
256
+ """Add an arbitrary command to the chain."""
257
+ self.commands.append(command)
258
+ return self
259
+
260
+ def switch_screen(self, screen: Screen) -> ProcessChain:
261
+ """Add a screen-switching command."""
262
+ return self.add(SwitchScreen(screen))
263
+
264
+ def run(self) -> ProcessChain:
265
+ """Add miTUpdate (run calculation for current screen)."""
266
+ return self.add(RunCalculation())
267
+
268
+ def tab_change(self, tab: int) -> ProcessChain:
269
+ """Switch to a specific tab on the current screen."""
270
+ return self.add(TabChange(tab))
271
+
272
+ def sim_start_date(self, day: int, month: int) -> ProcessChain:
273
+ """Set simulation start date."""
274
+ return self.add(SimStartDate(day, month))
275
+
276
+ def sim_end_date(self, day: int, month: int) -> ProcessChain:
277
+ """Set simulation end date."""
278
+ return self.add(SimEndDate(day, month))
279
+
280
+ def change_attribute(
281
+ self, attribute: str, value: Union[str, int, float]
282
+ ) -> ProcessChain:
283
+ """Change a model attribute value."""
284
+ return self.add(ChangeAttributeValue(attribute, value))
285
+
286
+ def external_command(self, command: str) -> ProcessChain:
287
+ """Run an external command."""
288
+ return self.add(ExternalCommand(command))
289
+
290
+ def import_model_data(self, filepath: str) -> ProcessChain:
291
+ """Import model data from a CSV file."""
292
+ return self.add(ImportModelData(filepath))
293
+
294
+ def import_library_data(self, filepath: str) -> ProcessChain:
295
+ """Import library data from a file."""
296
+ return self.add(ImportLibraryData(filepath))
297
+
298
+ def export_as_xml(self) -> ProcessChain:
299
+ """Export the model as dsbXML."""
300
+ return self.add(ExportAsXML())
301
+
302
+ def use_sim_manager(self) -> ProcessChain:
303
+ """Use the Simulation Manager for the run."""
304
+ return self.add(UseSimManager())
305
+
306
+ def no_close(self) -> ProcessChain:
307
+ """Prevent DesignBuilder from closing after processing."""
308
+ return self.add(NoClose())
309
+
310
+ # -- Rendering ------------------------------------------------------------
311
+
312
+ def to_string(self) -> str:
313
+ """Render the chain as a ``/process=...`` argument string.
314
+
315
+ Raises
316
+ ------
317
+ ValueError
318
+ If the chain contains no commands.
319
+ """
320
+ if not self.commands:
321
+ raise ValueError("ProcessChain is empty — add at least one command.")
322
+ parts = [cmd.render() for cmd in self.commands]
323
+ return "/process=" + ", ".join(parts)
324
+
325
+ def to_list(self) -> list[str]:
326
+ """Return the chain as a list of individual command strings."""
327
+ return [cmd.render() for cmd in self.commands]
328
+
329
+ def __str__(self) -> str:
330
+ return self.to_string()
331
+
332
+ def __repr__(self) -> str:
333
+ return f"ProcessChain({self.commands!r})"
334
+
335
+ def __len__(self) -> int:
336
+ return len(self.commands)
337
+
338
+
339
+ # ---------------------------------------------------------------------------
340
+ # Convenience factory functions
341
+ # ---------------------------------------------------------------------------
342
+
343
+ def eplus_simulation(
344
+ *,
345
+ sim_start_date: Optional[tuple[int, int]] = None,
346
+ sim_end_date: Optional[tuple[int, int]] = None,
347
+ use_sim_manager: bool = False,
348
+ attributes: Optional[list[tuple[str, Union[str, int, float]]]] = None,
349
+ no_close: bool = False,
350
+ ) -> ProcessChain:
351
+ """Build a standard EnergyPlus simulation command chain.
352
+
353
+ This produces the equivalent of what ``db-batch`` creates for
354
+ ``analysis_type="eplus"``.
355
+ """
356
+ chain = ProcessChain()
357
+
358
+ if use_sim_manager:
359
+ chain.use_sim_manager()
360
+
361
+ if sim_start_date:
362
+ chain.sim_start_date(sim_start_date[0], sim_start_date[1])
363
+
364
+ if sim_end_date:
365
+ chain.sim_end_date(sim_end_date[0], sim_end_date[1])
366
+
367
+ if attributes:
368
+ for attr, val in attributes:
369
+ chain.change_attribute(attr, val)
370
+
371
+ chain.switch_screen(Screen.SIMULATION)
372
+ chain.run()
373
+
374
+ if no_close:
375
+ chain.no_close()
376
+
377
+ return chain
378
+
379
+
380
+ def sbem_calculation(
381
+ *,
382
+ sim_start_date: Optional[tuple[int, int]] = None,
383
+ sim_end_date: Optional[tuple[int, int]] = None,
384
+ attributes: Optional[list[tuple[str, Union[str, int, float]]]] = None,
385
+ no_close: bool = False,
386
+ ) -> ProcessChain:
387
+ """Build a standard SBEM/DSM calculation command chain."""
388
+ chain = ProcessChain()
389
+
390
+ if sim_start_date:
391
+ chain.sim_start_date(sim_start_date[0], sim_start_date[1])
392
+
393
+ if sim_end_date:
394
+ chain.sim_end_date(sim_end_date[0], sim_end_date[1])
395
+
396
+ if attributes:
397
+ for attr, val in attributes:
398
+ chain.change_attribute(attr, val)
399
+
400
+ chain.switch_screen(Screen.SBEM_DSM)
401
+ chain.run()
402
+
403
+ if no_close:
404
+ chain.no_close()
405
+
406
+ return chain
407
+
408
+
409
+ def export_xml() -> ProcessChain:
410
+ """Build a command chain to export the model as dsbXML."""
411
+ return ProcessChain().export_as_xml()
412
+
413
+
414
+ def heating_and_cooling_design(*, no_close: bool = False) -> ProcessChain:
415
+ """Run both Heating and Cooling Design calculations."""
416
+ chain = (
417
+ ProcessChain()
418
+ .switch_screen(Screen.HEATING_DESIGN)
419
+ .run()
420
+ .switch_screen(Screen.COOLING_DESIGN)
421
+ .run()
422
+ )
423
+ if no_close:
424
+ chain.no_close()
425
+ return chain
426
+
427
+
428
+ def daylighting(
429
+ *, run_annual: bool = False, no_close: bool = False
430
+ ) -> ProcessChain:
431
+ """Run daylighting calculation, optionally including annual daylighting.
432
+
433
+ When *run_annual* is True, runs both Illuminance (tab 1) and
434
+ Annual daylighting (tab 2) calculations.
435
+ """
436
+ chain = ProcessChain().switch_screen(Screen.DAYLIGHTING).run()
437
+ if run_annual:
438
+ chain.tab_change(2).run()
439
+ if no_close:
440
+ chain.no_close()
441
+ return chain
442
+
443
+
444
+ def cfd_simulation(*, no_close: bool = False) -> ProcessChain:
445
+ """Run a CFD simulation."""
446
+ chain = ProcessChain().switch_screen(Screen.CFD).run()
447
+ if no_close:
448
+ chain.no_close()
449
+ return chain
@@ -0,0 +1,76 @@
1
+ """
2
+ executable.py
3
+ ====================================
4
+ Locate and validate the DesignBuilder executable.
5
+
6
+ Resolution order:
7
+ 1. Explicit path argument
8
+ 2. DESIGNBUILDER_EXE environment variable
9
+ 3. Common Windows installation directories
10
+ 4. System PATH (via shutil.which)
11
+ """
12
+
13
+ import os
14
+ import shutil
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ # Environment variable to override the DesignBuilder executable path.
19
+ ENV_VAR = "DESIGNBUILDER_EXE"
20
+
21
+ # Common DesignBuilder installation paths (Windows).
22
+ DEFAULT_INSTALL_PATHS = [
23
+ Path(r"C:\Program Files (x86)\DesignBuilder\DesignBuilder.exe"),
24
+ Path(r"C:\Program Files\DesignBuilder\DesignBuilder.exe"),
25
+ ]
26
+
27
+
28
+ def find_designbuilder(exe_path: Optional[str | Path] = None) -> Path:
29
+ """Locate the DesignBuilder executable.
30
+
31
+ Parameters
32
+ ----------
33
+ exe_path : str or Path, optional
34
+ Explicit path to DesignBuilder.exe. Takes highest priority.
35
+
36
+ Returns
37
+ -------
38
+ Path
39
+ Resolved path to the executable.
40
+
41
+ Raises
42
+ ------
43
+ FileNotFoundError
44
+ If DesignBuilder cannot be found via any method.
45
+ """
46
+ # 1. Explicit argument
47
+ if exe_path is not None:
48
+ p = Path(exe_path)
49
+ if p.is_file():
50
+ return p
51
+ raise FileNotFoundError(f"DesignBuilder not found at: {p}")
52
+
53
+ # 2. Environment variable
54
+ env = os.environ.get(ENV_VAR)
55
+ if env:
56
+ p = Path(env)
57
+ if p.is_file():
58
+ return p
59
+ raise FileNotFoundError(
60
+ f"{ENV_VAR} is set to '{env}' but the file does not exist."
61
+ )
62
+
63
+ # 3. Common install locations
64
+ for p in DEFAULT_INSTALL_PATHS:
65
+ if p.is_file():
66
+ return p
67
+
68
+ # 4. System PATH
69
+ which = shutil.which("DesignBuilder")
70
+ if which:
71
+ return Path(which)
72
+
73
+ raise FileNotFoundError(
74
+ "Could not find DesignBuilder.exe. "
75
+ f"Set the {ENV_VAR} environment variable or pass the path explicitly."
76
+ )