iotrace 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.
iotrace-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: iotrace
3
+ Version: 0.1.0
4
+ Summary: Python wrapper for NI IO Trace programmatic C API
5
+ Keywords: ni,io-trace,iotrace,nitrace,ni-io-trace,nimi-python,nidcpower,nidmm,niscope,niswitch,nidigital,nidaqmx,nifgen,nirfsa,nirfsg,nise,ni-visa,pyVISA
6
+ Author: Karsten van Zwol
7
+ Author-email: Karsten van Zwol <karsten.van.zwol@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/ktvanzwol/nitrace
25
+ Project-URL: Repository, https://github.com/ktvanzwol/nitrace
26
+ Project-URL: Issues, https://github.com/ktvanzwol/nitrace/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ # NI IO Trace Python API
30
+
31
+ A Python library for controlling [NI IO Trace](https://www.ni.com/docs/en-US/bundle/ni-io-trace/page/overview.html) programmatically. Launch the application, start and stop tracing, write log messages, and close IO Trace — all from Python.
32
+
33
+ ## Requirements
34
+
35
+ - Windows
36
+ - Python 3.10+
37
+ - NI IO Trace installed (part of NI software distributions)
38
+ - [Where Can I Download NI I/O Trace?](https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z000000kJcQSAU)
39
+
40
+ ## Installation
41
+
42
+ Install from PyPI:
43
+
44
+ ```
45
+ pip install iotrace
46
+ ```
47
+
48
+ Or with [uv](https://docs.astral.sh/uv/):
49
+
50
+ ```
51
+ uv add iotrace
52
+ ```
53
+
54
+ Or install directly from GitHub:
55
+
56
+ ```
57
+ pip install git+https://github.com/ktvanzwol/nitrace.git
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ from pathlib import Path
64
+
65
+ import iotrace
66
+
67
+ # Launch the application (minimized by default)
68
+ iotrace.launch_io_trace()
69
+
70
+ # Start tracing to a NI IO Trace log file
71
+ iotrace.start_tracing(
72
+ log_file_setting=iotrace.LogFileSetting.IO_TRACE,
73
+ file_path=Path.cwd() / "trace.nitrace",
74
+ file_write_mode=iotrace.FileWriteMode.CREATE_OR_OVERWRITE,
75
+ )
76
+
77
+ # Insert a marker into the trace log
78
+ iotrace.log_message("Test started")
79
+
80
+ # ... run your NI driver calls ...
81
+
82
+ # Stop tracing and leave the application running to inspect the log.
83
+ iotrace.stop_tracing()
84
+
85
+ print("Trace complete. Log file saved to:", Path.cwd() / "trace.nitrace")
86
+ ```
87
+
88
+ ## CLI
89
+
90
+ A command-line interface is included:
91
+
92
+ ```
93
+ # Launch IO Trace and start tracing to a CSV file
94
+ iotrace start --log-format csv --file trace.csv --write-mode overwrite
95
+
96
+ # Stop tracing and close the application
97
+ iotrace stop --close
98
+ ```
99
+
100
+ ### `iotrace start`
101
+
102
+ | Option | Description |
103
+ |---|---|
104
+ | `--log-format` | Log file format: `none`, `io-trace`, `plain-text`, `csv`, `xml` (default: `none`). |
105
+ | `--file` | Path to the log file. |
106
+ | `--write-mode` | File write mode: `create`, `append`, `overwrite` (default: `create`). |
107
+
108
+ ### `iotrace stop`
109
+
110
+ | Option | Description |
111
+ |---|---|
112
+ | `--close` | Close NI IO Trace after stopping. |
113
+
114
+ ## API Reference
115
+
116
+ - **Functions:**
117
+ - [`get_application_path`](#get_application_path---path)
118
+ - [`launch_io_trace`](#launch_io_tracewindow_statewindowstateminimized---subprocesspopen)
119
+ - [`start_tracing`](#start_tracinglog_file_settinglogfilesettingno_file-file_pathnone-file_write_modefilewritemodecreate_only---none)
120
+ - [`stop_tracing`](#stop_tracing---none)
121
+ - [`log_message`](#log_messagemessage-str---none)
122
+ - [`close_io_trace`](#close_io_tracetimeout-float--100---none)
123
+ - **Logging:**
124
+ - [`IOTraceHandler`](#iotracehandler)
125
+ - **Enums:**
126
+ - [`LogFileSetting`](#logfilesetting)
127
+ - [`FileWriteMode`](#filewritemode)
128
+ - [`WindowState`](#windowstate)
129
+ - [`StatusCode`](#statuscode)
130
+ - **Exceptions:**
131
+ - [`IoTraceError`](#iotraceerror)
132
+
133
+ ### Functions
134
+
135
+ #### `get_application_path() -> Path`
136
+
137
+ Return the filesystem path to the NI IO Trace executable.
138
+
139
+ **Raises:** `IoTraceError` if NI IO Trace is not installed.
140
+
141
+ ---
142
+
143
+ #### `launch_io_trace(window_state=WindowState.MINIMIZED) -> subprocess.Popen`
144
+
145
+ Launch the NI IO Trace application and return the process handle. The application must be running before calling `start_tracing`, `stop_tracing`, `log_message`, or `close_io_trace`.
146
+
147
+ | Parameter | Type | Default | Description |
148
+ |---|---|---|---|
149
+ | `window_state` | `WindowState` | `MINIMIZED` | Initial window state of the application. |
150
+
151
+ **Raises:** `RuntimeError` if the process exits immediately. `IoTraceError` if the application path cannot be resolved.
152
+
153
+ ---
154
+
155
+ #### `start_tracing(log_file_setting=LogFileSetting.NO_FILE, file_path=None, file_write_mode=FileWriteMode.CREATE_ONLY) -> None`
156
+
157
+ Start tracing NI driver calls. NI IO Trace must already be running.
158
+
159
+ | Parameter | Type | Default | Description |
160
+ |---|---|---|---|
161
+ | `log_file_setting` | `LogFileSetting` | `NO_FILE` | Format of the log file. |
162
+ | `file_path` | `str \| Path \| None` | `None` | Path to the log file. Required when `log_file_setting` is not `NO_FILE`. |
163
+ | `file_write_mode` | `FileWriteMode` | `CREATE_ONLY` | How to handle an existing file. |
164
+
165
+ **Raises:** `IoTraceError` if IO Trace is not running, the file already exists with `CREATE_ONLY`, or the settings are invalid.
166
+
167
+ ---
168
+
169
+ #### `stop_tracing() -> None`
170
+
171
+ Stop tracing NI driver calls. The application remains open and tracing can be restarted.
172
+
173
+ **Raises:** `IoTraceError` if tracing was not active.
174
+
175
+ ---
176
+
177
+ #### `log_message(message: str) -> None`
178
+
179
+ Write a custom text entry into the active trace log. Useful for inserting markers to correlate driver calls with application events.
180
+
181
+ | Parameter | Type | Description |
182
+ |---|---|---|
183
+ | `message` | `str` | The text to write. |
184
+
185
+ **Raises:** `IoTraceError` if the IO Trace application has been closed.
186
+
187
+ ---
188
+
189
+ #### `close_io_trace(timeout: float = 10.0) -> None`
190
+
191
+ Close the NI IO Trace application and wait for the process to exit. The application does not need to have been launched by `launch_io_trace` — it may have been started manually.
192
+
193
+ | Parameter | Type | Default | Description |
194
+ |---|---|---|---|
195
+ | `timeout` | `float` | `10.0` | Maximum seconds to wait for the process to exit. |
196
+
197
+ **Raises:** `IoTraceError` if the close command fails. `RuntimeError` if the process does not exit within the timeout.
198
+
199
+ ### Enums
200
+
201
+ #### `LogFileSetting`
202
+
203
+ | Member | Value | Description |
204
+ |---|---|---|
205
+ | `NO_FILE` | -1 | No log file; trace data visible only in the GUI. |
206
+ | `IO_TRACE` | 0 | NI IO Trace binary format (`.iotrace`). |
207
+ | `PLAIN_TEXT` | 1 | Human-readable plain-text file. |
208
+ | `COMMA_SEPARATED` | 2 | Comma-separated values (CSV) file. |
209
+ | `XML` | 3 | XML-formatted file. |
210
+
211
+ #### `FileWriteMode`
212
+
213
+ | Member | Value | Description |
214
+ |---|---|---|
215
+ | `CREATE_ONLY` | 0 | Create a new file. Raises `IoTraceError` if the file exists. |
216
+ | `CREATE_OR_APPEND` | 1 | Append to an existing file or create a new one. |
217
+ | `CREATE_OR_OVERWRITE` | 2 | Overwrite an existing file or create a new one. |
218
+
219
+ #### `WindowState`
220
+
221
+ | Member | Value | Description |
222
+ |---|---|---|
223
+ | `HIDDEN` | 0 | No visible window. |
224
+ | `NORMAL` | 1 | Default (restored) window state. |
225
+ | `MAXIMIZED` | 2 | Window maximized. |
226
+ | `MINIMIZED` | 3 | Window minimized. |
227
+
228
+ #### `StatusCode`
229
+
230
+ | Member | Value | Description |
231
+ |---|---|---|
232
+ | `SUCCESS` | 0 | Call completed successfully. |
233
+ | `FAILED_NO_EXECUTE` | -303200 | Command could not be executed. |
234
+ | `FAILED_INCOMPATIBLE_STATE` | -303201 | Operation not valid in the current state. |
235
+ | `FAILED_UNABLE_TO_OPEN_LOG_FILE` | -303202 | Could not open the specified log file. |
236
+ | `FAILED_GUI_CLOSED` | -303203 | The IO Trace application is not running. |
237
+ | `FAILED_INVALID_SETTINGS` | -303204 | Invalid tracing settings. |
238
+ | `FAILED_BAD_PARAMETER` | -303205 | A parameter value is invalid. |
239
+ | `FAILED_INTERNAL_FAILURE` | -303206 | An internal error occurred. |
240
+ | `FAILED_INVALID_FILE_EXTENSION` | -303207 | The log file extension does not match the format. |
241
+ | `FAILED_BUFFER_TOO_SMALL` | -303208 | The provided buffer is too small. |
242
+ | `FAILED_FILE_ALREADY_EXISTS` | -303209 | The file already exists (with `CREATE_ONLY` mode). |
243
+
244
+ ### Logging
245
+
246
+ #### `IOTraceHandler`
247
+
248
+ `iotrace.logging.IOTraceHandler` is a [`logging.Handler`](https://docs.python.org/3/library/logging.html#handler-objects) subclass that forwards Python log records into the NI IO Trace log via `log_message`. If the IO Trace application is not running, the error is passed to `handleError`.
249
+
250
+ ```python
251
+ import logging
252
+ from iotrace.logging import IOTraceHandler
253
+
254
+ logger = logging.getLogger("my_app")
255
+ handler = IOTraceHandler()
256
+ handler.setFormatter(logging.Formatter("[%(levelname)s] %(name)s - %(message)s"))
257
+ logger.addHandler(handler)
258
+ ```
259
+
260
+ See [`examples/logging_handler.py`](examples/logging_handler.py) for a complete runnable example.
261
+
262
+ ### Exceptions
263
+
264
+ #### `IoTraceError`
265
+
266
+ Raised when an API call returns a non-success status.
267
+
268
+ | Attribute | Type | Description |
269
+ |---|---|---|
270
+ | `status` | `StatusCode` | The status code that triggered the error. |
271
+
272
+ ## License
273
+
274
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,246 @@
1
+ # NI IO Trace Python API
2
+
3
+ A Python library for controlling [NI IO Trace](https://www.ni.com/docs/en-US/bundle/ni-io-trace/page/overview.html) programmatically. Launch the application, start and stop tracing, write log messages, and close IO Trace — all from Python.
4
+
5
+ ## Requirements
6
+
7
+ - Windows
8
+ - Python 3.10+
9
+ - NI IO Trace installed (part of NI software distributions)
10
+ - [Where Can I Download NI I/O Trace?](https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z000000kJcQSAU)
11
+
12
+ ## Installation
13
+
14
+ Install from PyPI:
15
+
16
+ ```
17
+ pip install iotrace
18
+ ```
19
+
20
+ Or with [uv](https://docs.astral.sh/uv/):
21
+
22
+ ```
23
+ uv add iotrace
24
+ ```
25
+
26
+ Or install directly from GitHub:
27
+
28
+ ```
29
+ pip install git+https://github.com/ktvanzwol/nitrace.git
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from pathlib import Path
36
+
37
+ import iotrace
38
+
39
+ # Launch the application (minimized by default)
40
+ iotrace.launch_io_trace()
41
+
42
+ # Start tracing to a NI IO Trace log file
43
+ iotrace.start_tracing(
44
+ log_file_setting=iotrace.LogFileSetting.IO_TRACE,
45
+ file_path=Path.cwd() / "trace.nitrace",
46
+ file_write_mode=iotrace.FileWriteMode.CREATE_OR_OVERWRITE,
47
+ )
48
+
49
+ # Insert a marker into the trace log
50
+ iotrace.log_message("Test started")
51
+
52
+ # ... run your NI driver calls ...
53
+
54
+ # Stop tracing and leave the application running to inspect the log.
55
+ iotrace.stop_tracing()
56
+
57
+ print("Trace complete. Log file saved to:", Path.cwd() / "trace.nitrace")
58
+ ```
59
+
60
+ ## CLI
61
+
62
+ A command-line interface is included:
63
+
64
+ ```
65
+ # Launch IO Trace and start tracing to a CSV file
66
+ iotrace start --log-format csv --file trace.csv --write-mode overwrite
67
+
68
+ # Stop tracing and close the application
69
+ iotrace stop --close
70
+ ```
71
+
72
+ ### `iotrace start`
73
+
74
+ | Option | Description |
75
+ |---|---|
76
+ | `--log-format` | Log file format: `none`, `io-trace`, `plain-text`, `csv`, `xml` (default: `none`). |
77
+ | `--file` | Path to the log file. |
78
+ | `--write-mode` | File write mode: `create`, `append`, `overwrite` (default: `create`). |
79
+
80
+ ### `iotrace stop`
81
+
82
+ | Option | Description |
83
+ |---|---|
84
+ | `--close` | Close NI IO Trace after stopping. |
85
+
86
+ ## API Reference
87
+
88
+ - **Functions:**
89
+ - [`get_application_path`](#get_application_path---path)
90
+ - [`launch_io_trace`](#launch_io_tracewindow_statewindowstateminimized---subprocesspopen)
91
+ - [`start_tracing`](#start_tracinglog_file_settinglogfilesettingno_file-file_pathnone-file_write_modefilewritemodecreate_only---none)
92
+ - [`stop_tracing`](#stop_tracing---none)
93
+ - [`log_message`](#log_messagemessage-str---none)
94
+ - [`close_io_trace`](#close_io_tracetimeout-float--100---none)
95
+ - **Logging:**
96
+ - [`IOTraceHandler`](#iotracehandler)
97
+ - **Enums:**
98
+ - [`LogFileSetting`](#logfilesetting)
99
+ - [`FileWriteMode`](#filewritemode)
100
+ - [`WindowState`](#windowstate)
101
+ - [`StatusCode`](#statuscode)
102
+ - **Exceptions:**
103
+ - [`IoTraceError`](#iotraceerror)
104
+
105
+ ### Functions
106
+
107
+ #### `get_application_path() -> Path`
108
+
109
+ Return the filesystem path to the NI IO Trace executable.
110
+
111
+ **Raises:** `IoTraceError` if NI IO Trace is not installed.
112
+
113
+ ---
114
+
115
+ #### `launch_io_trace(window_state=WindowState.MINIMIZED) -> subprocess.Popen`
116
+
117
+ Launch the NI IO Trace application and return the process handle. The application must be running before calling `start_tracing`, `stop_tracing`, `log_message`, or `close_io_trace`.
118
+
119
+ | Parameter | Type | Default | Description |
120
+ |---|---|---|---|
121
+ | `window_state` | `WindowState` | `MINIMIZED` | Initial window state of the application. |
122
+
123
+ **Raises:** `RuntimeError` if the process exits immediately. `IoTraceError` if the application path cannot be resolved.
124
+
125
+ ---
126
+
127
+ #### `start_tracing(log_file_setting=LogFileSetting.NO_FILE, file_path=None, file_write_mode=FileWriteMode.CREATE_ONLY) -> None`
128
+
129
+ Start tracing NI driver calls. NI IO Trace must already be running.
130
+
131
+ | Parameter | Type | Default | Description |
132
+ |---|---|---|---|
133
+ | `log_file_setting` | `LogFileSetting` | `NO_FILE` | Format of the log file. |
134
+ | `file_path` | `str \| Path \| None` | `None` | Path to the log file. Required when `log_file_setting` is not `NO_FILE`. |
135
+ | `file_write_mode` | `FileWriteMode` | `CREATE_ONLY` | How to handle an existing file. |
136
+
137
+ **Raises:** `IoTraceError` if IO Trace is not running, the file already exists with `CREATE_ONLY`, or the settings are invalid.
138
+
139
+ ---
140
+
141
+ #### `stop_tracing() -> None`
142
+
143
+ Stop tracing NI driver calls. The application remains open and tracing can be restarted.
144
+
145
+ **Raises:** `IoTraceError` if tracing was not active.
146
+
147
+ ---
148
+
149
+ #### `log_message(message: str) -> None`
150
+
151
+ Write a custom text entry into the active trace log. Useful for inserting markers to correlate driver calls with application events.
152
+
153
+ | Parameter | Type | Description |
154
+ |---|---|---|
155
+ | `message` | `str` | The text to write. |
156
+
157
+ **Raises:** `IoTraceError` if the IO Trace application has been closed.
158
+
159
+ ---
160
+
161
+ #### `close_io_trace(timeout: float = 10.0) -> None`
162
+
163
+ Close the NI IO Trace application and wait for the process to exit. The application does not need to have been launched by `launch_io_trace` — it may have been started manually.
164
+
165
+ | Parameter | Type | Default | Description |
166
+ |---|---|---|---|
167
+ | `timeout` | `float` | `10.0` | Maximum seconds to wait for the process to exit. |
168
+
169
+ **Raises:** `IoTraceError` if the close command fails. `RuntimeError` if the process does not exit within the timeout.
170
+
171
+ ### Enums
172
+
173
+ #### `LogFileSetting`
174
+
175
+ | Member | Value | Description |
176
+ |---|---|---|
177
+ | `NO_FILE` | -1 | No log file; trace data visible only in the GUI. |
178
+ | `IO_TRACE` | 0 | NI IO Trace binary format (`.iotrace`). |
179
+ | `PLAIN_TEXT` | 1 | Human-readable plain-text file. |
180
+ | `COMMA_SEPARATED` | 2 | Comma-separated values (CSV) file. |
181
+ | `XML` | 3 | XML-formatted file. |
182
+
183
+ #### `FileWriteMode`
184
+
185
+ | Member | Value | Description |
186
+ |---|---|---|
187
+ | `CREATE_ONLY` | 0 | Create a new file. Raises `IoTraceError` if the file exists. |
188
+ | `CREATE_OR_APPEND` | 1 | Append to an existing file or create a new one. |
189
+ | `CREATE_OR_OVERWRITE` | 2 | Overwrite an existing file or create a new one. |
190
+
191
+ #### `WindowState`
192
+
193
+ | Member | Value | Description |
194
+ |---|---|---|
195
+ | `HIDDEN` | 0 | No visible window. |
196
+ | `NORMAL` | 1 | Default (restored) window state. |
197
+ | `MAXIMIZED` | 2 | Window maximized. |
198
+ | `MINIMIZED` | 3 | Window minimized. |
199
+
200
+ #### `StatusCode`
201
+
202
+ | Member | Value | Description |
203
+ |---|---|---|
204
+ | `SUCCESS` | 0 | Call completed successfully. |
205
+ | `FAILED_NO_EXECUTE` | -303200 | Command could not be executed. |
206
+ | `FAILED_INCOMPATIBLE_STATE` | -303201 | Operation not valid in the current state. |
207
+ | `FAILED_UNABLE_TO_OPEN_LOG_FILE` | -303202 | Could not open the specified log file. |
208
+ | `FAILED_GUI_CLOSED` | -303203 | The IO Trace application is not running. |
209
+ | `FAILED_INVALID_SETTINGS` | -303204 | Invalid tracing settings. |
210
+ | `FAILED_BAD_PARAMETER` | -303205 | A parameter value is invalid. |
211
+ | `FAILED_INTERNAL_FAILURE` | -303206 | An internal error occurred. |
212
+ | `FAILED_INVALID_FILE_EXTENSION` | -303207 | The log file extension does not match the format. |
213
+ | `FAILED_BUFFER_TOO_SMALL` | -303208 | The provided buffer is too small. |
214
+ | `FAILED_FILE_ALREADY_EXISTS` | -303209 | The file already exists (with `CREATE_ONLY` mode). |
215
+
216
+ ### Logging
217
+
218
+ #### `IOTraceHandler`
219
+
220
+ `iotrace.logging.IOTraceHandler` is a [`logging.Handler`](https://docs.python.org/3/library/logging.html#handler-objects) subclass that forwards Python log records into the NI IO Trace log via `log_message`. If the IO Trace application is not running, the error is passed to `handleError`.
221
+
222
+ ```python
223
+ import logging
224
+ from iotrace.logging import IOTraceHandler
225
+
226
+ logger = logging.getLogger("my_app")
227
+ handler = IOTraceHandler()
228
+ handler.setFormatter(logging.Formatter("[%(levelname)s] %(name)s - %(message)s"))
229
+ logger.addHandler(handler)
230
+ ```
231
+
232
+ See [`examples/logging_handler.py`](examples/logging_handler.py) for a complete runnable example.
233
+
234
+ ### Exceptions
235
+
236
+ #### `IoTraceError`
237
+
238
+ Raised when an API call returns a non-success status.
239
+
240
+ | Attribute | Type | Description |
241
+ |---|---|---|
242
+ | `status` | `StatusCode` | The status code that triggered the error. |
243
+
244
+ ## License
245
+
246
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "iotrace"
3
+ version = "0.1.0"
4
+ description = "Python wrapper for NI IO Trace programmatic C API"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Karsten van Zwol", email = "karsten.van.zwol@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ dependencies = []
12
+ keywords = ["ni", "io-trace", "iotrace", "nitrace", "ni-io-trace", "nimi-python", "nidcpower", "nidmm", "niscope", "niswitch", "nidigital", "nidaqmx", "nifgen", "nirfsa", "nirfsg", "nise", "ni-visa", "pyVISA"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Science/Research",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Scientific/Engineering",
25
+ "Topic :: Software Development :: Testing",
26
+ "Topic :: System :: Logging",
27
+ "Typing :: Typed",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/ktvanzwol/nitrace"
32
+ Repository = "https://github.com/ktvanzwol/nitrace"
33
+ Issues = "https://github.com/ktvanzwol/nitrace/issues"
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.11.6,<0.12.0"]
37
+ build-backend = "uv_build"
38
+
39
+ [project.scripts]
40
+ iotrace = "iotrace.__cli__:main"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=9.0.3",
45
+ "pytest-cov>=7.1.0",
46
+ "ruff>=0.11.0",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ line-length = 120
51
+
52
+ [tool.pytest.ini_options]
53
+ markers = [
54
+ "system: tests that require NI IO Trace to be installed",
55
+ ]
56
+
57
+ [tool.ruff.format]
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I"]
61
+
62
+ [tool.coverage.run]
63
+ source = ["iotrace"]
64
+
65
+ [tool.coverage.report]
66
+ omit = ["tests/*"]
@@ -0,0 +1,75 @@
1
+ import argparse
2
+ import sys
3
+
4
+ import iotrace
5
+
6
+
7
+ def main(argv: list[str] | None = None) -> None:
8
+ parser = argparse.ArgumentParser(
9
+ prog="iotrace",
10
+ description="Control NI IO Trace from the command line.",
11
+ )
12
+ subparsers = parser.add_subparsers(dest="command", required=True)
13
+
14
+ # start
15
+ start_parser = subparsers.add_parser("start", help="Start tracing driver calls.")
16
+ start_parser.add_argument(
17
+ "--log-format",
18
+ choices=["none", "io-trace", "plain-text", "csv", "xml"],
19
+ default="none",
20
+ help="Log file format (default: none).",
21
+ )
22
+ start_parser.add_argument(
23
+ "--file",
24
+ default=None,
25
+ help="Path to the log file.",
26
+ )
27
+ start_parser.add_argument(
28
+ "--write-mode",
29
+ choices=["create", "append", "overwrite"],
30
+ default="create",
31
+ help="File write mode (default: create).",
32
+ )
33
+
34
+ # stop
35
+ stop_parser = subparsers.add_parser("stop", help="Stop tracing driver calls.")
36
+ stop_parser.add_argument(
37
+ "--close",
38
+ action="store_true",
39
+ help="Close NI IO Trace after stopping.",
40
+ )
41
+
42
+ args = parser.parse_args(argv)
43
+
44
+ try:
45
+ if args.command == "start":
46
+ iotrace.launch_io_trace(window_state=iotrace.WindowState.MINIMIZED)
47
+ log_format_map = {
48
+ "none": iotrace.LogFileSetting.NO_FILE,
49
+ "io-trace": iotrace.LogFileSetting.IO_TRACE,
50
+ "plain-text": iotrace.LogFileSetting.PLAIN_TEXT,
51
+ "csv": iotrace.LogFileSetting.COMMA_SEPARATED,
52
+ "xml": iotrace.LogFileSetting.XML,
53
+ }
54
+ write_mode_map = {
55
+ "create": iotrace.FileWriteMode.CREATE_ONLY,
56
+ "append": iotrace.FileWriteMode.CREATE_OR_APPEND,
57
+ "overwrite": iotrace.FileWriteMode.CREATE_OR_OVERWRITE,
58
+ }
59
+ iotrace.start_tracing(
60
+ log_file_setting=log_format_map[args.log_format],
61
+ file_path=args.file,
62
+ file_write_mode=write_mode_map[args.write_mode],
63
+ )
64
+ print("Tracing started.")
65
+
66
+ elif args.command == "stop":
67
+ iotrace.stop_tracing()
68
+ print("Tracing stopped.")
69
+ if args.close:
70
+ iotrace.close_io_trace()
71
+ print("NI IO Trace closed.")
72
+
73
+ except iotrace.IoTraceError as exc:
74
+ print(f"Error: {exc}", file=sys.stderr)
75
+ sys.exit(1)
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import enum
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
9
+ __all__ = [
10
+ "LogFileSetting",
11
+ "FileWriteMode",
12
+ "WindowState",
13
+ "StatusCode",
14
+ "IoTraceError",
15
+ "get_application_path",
16
+ "launch_io_trace",
17
+ "start_tracing",
18
+ "stop_tracing",
19
+ "log_message",
20
+ "close_io_trace",
21
+ ]
22
+
23
+
24
+ class LogFileSetting(enum.IntEnum):
25
+ """Controls the log file format used when tracing.
26
+
27
+ Members:
28
+ NO_FILE: Do not write a log file. Trace data is only visible in the
29
+ NI IO Trace GUI.
30
+ IO_TRACE: Write an NI IO Trace binary log file (``.nitrace``).
31
+ PLAIN_TEXT: Write a human-readable plain-text log file.
32
+ COMMA_SEPARATED: Write a comma-separated values (CSV) log file.
33
+ XML: Write an XML-formatted log file.
34
+ """
35
+
36
+ NO_FILE = -1
37
+ IO_TRACE = 0
38
+ PLAIN_TEXT = 1
39
+ COMMA_SEPARATED = 2
40
+ XML = 3
41
+
42
+
43
+ class FileWriteMode(enum.IntEnum):
44
+ """Controls how the log file is created or opened.
45
+
46
+ Members:
47
+ CREATE_ONLY: Create a new file. Raises :class:`IoTraceError` if the
48
+ file already exists.
49
+ CREATE_OR_APPEND: Open an existing file and append to it, or create a
50
+ new file if it does not exist.
51
+ CREATE_OR_OVERWRITE: Overwrite an existing file, or create a new file
52
+ if it does not exist.
53
+ """
54
+
55
+ CREATE_ONLY = 0
56
+ CREATE_OR_APPEND = 1
57
+ CREATE_OR_OVERWRITE = 2
58
+
59
+
60
+ class WindowState(enum.IntEnum):
61
+ """Controls the window state of the NI IO Trace application at launch.
62
+
63
+ Members:
64
+ HIDDEN: Launch the application with no visible window.
65
+ NORMAL: Launch the application in its default (restored) window state.
66
+ MAXIMIZED: Launch the application with the window maximized.
67
+ MINIMIZED: Launch the application with the window minimized.
68
+ """
69
+
70
+ HIDDEN = 0
71
+ NORMAL = 1
72
+ MAXIMIZED = 2
73
+ MINIMIZED = 3
74
+
75
+
76
+ class StatusCode(enum.IntEnum):
77
+ """Status codes returned by NI IO Trace API calls.
78
+
79
+ ``SUCCESS`` indicates the call completed without error. All other members
80
+ represent error conditions and are used to populate
81
+ :attr:`IoTraceError.status`.
82
+ """
83
+
84
+ SUCCESS = 0
85
+ FAILED_NO_EXECUTE = -303200
86
+ FAILED_INCOMPATIBLE_STATE = -303201
87
+ FAILED_UNABLE_TO_OPEN_LOG_FILE = -303202
88
+ FAILED_GUI_CLOSED = -303203
89
+ FAILED_INVALID_SETTINGS = -303204
90
+ FAILED_BAD_PARAMETER = -303205
91
+ FAILED_INTERNAL_FAILURE = -303206
92
+ FAILED_INVALID_FILE_EXTENSION = -303207
93
+ FAILED_BUFFER_TOO_SMALL = -303208
94
+ FAILED_FILE_ALREADY_EXISTS = -303209
95
+
96
+
97
+ class IoTraceError(Exception):
98
+ """Raised when an NI IO Trace API call returns a non-success status.
99
+
100
+ Attributes:
101
+ status: The :class:`CommandStatus` that triggered the error.
102
+ """
103
+
104
+ def __init__(self, status: StatusCode) -> None:
105
+ self.status = status
106
+ super().__init__(f"NI IO Trace API error: {status.name} ({status.value})")
107
+
108
+
109
+ def _check(status_code: int) -> None:
110
+ status = StatusCode(status_code)
111
+ if status != StatusCode.SUCCESS:
112
+ raise IoTraceError(status)
113
+
114
+
115
+ def _load_dll() -> ctypes.WinDLL:
116
+ dll = ctypes.WinDLL("NiSpyLog")
117
+
118
+ # nispy_GetApplicationPath(char*, size_t) -> int
119
+ dll.nispy_GetApplicationPath.argtypes = [ctypes.c_char_p, ctypes.c_size_t]
120
+ dll.nispy_GetApplicationPath.restype = ctypes.c_int
121
+
122
+ # nispy_StartSpying(int, const char*, int) -> int
123
+ dll.nispy_StartSpying.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
124
+ dll.nispy_StartSpying.restype = ctypes.c_int
125
+
126
+ # nispy_StopSpying(void) -> int
127
+ dll.nispy_StopSpying.argtypes = []
128
+ dll.nispy_StopSpying.restype = ctypes.c_int
129
+
130
+ # nispy_WriteTextEntry(const char*) -> int
131
+ dll.nispy_WriteTextEntry.argtypes = [ctypes.c_char_p]
132
+ dll.nispy_WriteTextEntry.restype = ctypes.c_int
133
+
134
+ # nispy_CloseSpy(void) -> int
135
+ dll.nispy_CloseSpy.argtypes = []
136
+ dll.nispy_CloseSpy.restype = ctypes.c_int
137
+
138
+ return dll
139
+
140
+
141
+ _dll: ctypes.WinDLL | None = None
142
+
143
+
144
+ def _get_dll() -> ctypes.WinDLL:
145
+ global _dll
146
+ if _dll is None:
147
+ _dll = _load_dll()
148
+ return _dll
149
+
150
+
151
+ def get_application_path() -> Path:
152
+ """Return the filesystem path to the NI IO Trace executable.
153
+
154
+ Returns:
155
+ A :class:`~pathlib.Path` pointing to the executable.
156
+
157
+ Raises:
158
+ IoTraceError: If NI IO Trace is not installed.
159
+ """
160
+ buf_size = 1024
161
+ buf = ctypes.create_string_buffer(buf_size)
162
+ _check(_get_dll().nispy_GetApplicationPath(buf, buf_size))
163
+ return Path(buf.value.decode())
164
+
165
+
166
+ _WINDOW_STATE_ARGS: dict[WindowState, list[str]] = {
167
+ WindowState.HIDDEN: ["/hidden"],
168
+ WindowState.NORMAL: [],
169
+ WindowState.MAXIMIZED: ["/maximized"],
170
+ WindowState.MINIMIZED: ["/minimized"],
171
+ }
172
+
173
+
174
+ def launch_io_trace(
175
+ window_state: WindowState = WindowState.MINIMIZED,
176
+ ) -> subprocess.Popen:
177
+ """Launch the NI IO Trace application and return the process handle.
178
+
179
+ The application must be running before calls to :func:`start_tracing`,
180
+ :func:`stop_tracing`, :func:`log_message`, or :func:`close_io_trace` can
181
+ succeed.
182
+
183
+ Args:
184
+ window_state: The initial window state of the application. Defaults to
185
+ :attr:`WindowState.MINIMIZED`.
186
+
187
+ Returns:
188
+ The :class:`~subprocess.Popen` instance for the launched process.
189
+
190
+ Raises:
191
+ RuntimeError: If the process exits immediately after being started.
192
+ IoTraceError: If the application path cannot be resolved.
193
+ """
194
+ app_path = get_application_path()
195
+ cmd = [str(app_path), *_WINDOW_STATE_ARGS[window_state]]
196
+ process = subprocess.Popen(cmd)
197
+
198
+ if process.poll() is not None:
199
+ raise RuntimeError(f"NI IO Trace exited immediately with code {process.returncode}")
200
+
201
+ # Try to start a tracing session to verify that the application is ready to accept commands.
202
+ # If this fails, wait a moment and try again, as the application may still be finishing its launch process.
203
+ # This typically happens when the application is launched for the first time.
204
+ for _ in range(3):
205
+ try:
206
+ start_tracing() # Test that we can communicate with the application
207
+ stop_tracing() # Stop the test tracing session immediately
208
+ break # Success, exit the loop
209
+ except IoTraceError as _:
210
+ time.sleep(1) # Wait a moment for the application to finish launching
211
+ else:
212
+ raise RuntimeError("NI IO Trace failed to respond after multiple attempts")
213
+
214
+ return process
215
+
216
+
217
+ def start_tracing(
218
+ log_file_setting: LogFileSetting = LogFileSetting.NO_FILE,
219
+ file_path: str | Path | None = None,
220
+ file_write_mode: FileWriteMode = FileWriteMode.CREATE_ONLY,
221
+ ) -> None:
222
+ """Start tracing NI driver calls.
223
+
224
+ NI IO Trace must already be running (see :func:`launch_io_trace`) before
225
+ calling this function.
226
+
227
+ Args:
228
+ log_file_setting: The format of the log file to write. Use
229
+ :attr:`LogFileSetting.NO_FILE` to trace without writing a file.
230
+ file_path: The path to the log file. Required when *log_file_setting*
231
+ is not :attr:`LogFileSetting.NO_FILE`. Can be a string or
232
+ :class:`~pathlib.Path`.
233
+ file_write_mode: How to handle an existing file at *file_path*.
234
+
235
+ Raises:
236
+ IoTraceError: If the call fails (e.g. IO Trace is not running,
237
+ the file already exists with :attr:`FileWriteMode.CREATE_ONLY`,
238
+ or the settings are invalid).
239
+ """
240
+ path_bytes = str(file_path).encode() if file_path is not None else None
241
+ _check(_get_dll().nispy_StartSpying(int(log_file_setting), path_bytes, int(file_write_mode)))
242
+
243
+
244
+ def stop_tracing() -> None:
245
+ """Stop tracing NI driver calls.
246
+
247
+ Tracing must have been started with :func:`start_tracing` before calling
248
+ this function. The NI IO Trace application remains open and can be
249
+ restarted with another call to :func:`start_tracing`.
250
+
251
+ Raises:
252
+ IoTraceError: If tracing was not active.
253
+ """
254
+ _check(_get_dll().nispy_StopSpying())
255
+
256
+
257
+ def log_message(message: str) -> None:
258
+ """Write a custom text entry into the active NI IO Trace log.
259
+
260
+ This is useful for inserting markers or annotations into a trace session
261
+ to correlate driver calls with application-level events.
262
+
263
+ Args:
264
+ message: The text to write. Will be UTF-8 encoded.
265
+
266
+ Raises:
267
+ IoTraceError: If the IO Trace application has been closed.
268
+ """
269
+ _check(_get_dll().nispy_WriteTextEntry(message.encode()))
270
+
271
+
272
+ def _find_process_ids(exe_name: str) -> list[int]:
273
+ """Return PIDs of all running processes matching *exe_name*."""
274
+ result = subprocess.run(
275
+ ["tasklist", "/FI", f"IMAGENAME eq {exe_name}", "/FO", "CSV", "/NH"],
276
+ capture_output=True,
277
+ text=True,
278
+ )
279
+ pids: list[int] = []
280
+ for line in result.stdout.splitlines():
281
+ parts = line.strip().strip('"').split('","')
282
+ if len(parts) >= 2 and parts[0].lower() == exe_name.lower():
283
+ pids.append(int(parts[1]))
284
+ return pids
285
+
286
+
287
+ def _wait_for_process_exit(exe_name: str, timeout: float) -> None:
288
+ """Block until no processes named *exe_name* are running, or *timeout* expires."""
289
+ deadline = time.monotonic() + timeout
290
+ while time.monotonic() < deadline:
291
+ if not _find_process_ids(exe_name):
292
+ return
293
+ time.sleep(0.25)
294
+ raise RuntimeError(f"{exe_name} did not exit within {timeout} seconds")
295
+
296
+
297
+ def close_io_trace(timeout: float = 10.0) -> None:
298
+ """Close the NI IO Trace application and wait for the process to exit.
299
+
300
+ Sends the close command and then polls until the NI IO Trace process is
301
+ no longer running. The application does not need to have been launched by
302
+ :func:`launch_io_trace` — it may have been started manually.
303
+
304
+ After this call, the application must be relaunched before any further
305
+ tracing can occur.
306
+
307
+ Args:
308
+ timeout: Maximum number of seconds to wait for the process to exit.
309
+
310
+ Raises:
311
+ IoTraceError: If the close command fails.
312
+ RuntimeError: If the process does not exit within *timeout* seconds.
313
+ """
314
+ exe_name = get_application_path().name
315
+ _check(_get_dll().nispy_CloseSpy())
316
+ _wait_for_process_exit(exe_name, timeout)
@@ -0,0 +1,22 @@
1
+ """Logging integration for NI IO Trace."""
2
+
3
+ import logging
4
+
5
+ from iotrace import IoTraceError, log_message
6
+
7
+ __all__ = ["IOTraceHandler"]
8
+
9
+
10
+ class IOTraceHandler(logging.Handler):
11
+ """A logging handler that writes log records into the NI IO Trace log.
12
+
13
+ Each log record is formatted and passed to :func:`iotrace.log_message`,
14
+ making Python log entries appear in the IO Trace log alongside NI driver
15
+ calls.
16
+ """
17
+
18
+ def emit(self, record: logging.LogRecord) -> None:
19
+ try:
20
+ log_message(self.format(record))
21
+ except IoTraceError:
22
+ self.handleError(record)
File without changes