nvq 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nvq/__init__.py ADDED
@@ -0,0 +1,186 @@
1
+ """nvq runs the nvq binary and hands back what it says about NVIDIA cards. The linux x86_64 and aarch64
2
+ wheels carry the binary (bin/nvq, built in manylinux2014: glibc 2.17 and newer), so `pip install nvq` is
3
+ the whole install, and a version of this package runs exactly the binary released with it. The wheel for
4
+ any other platform carries none: NVQ(path=...) names one there.
5
+
6
+ n = nvq.NVQ() # the package's own binary; NVQ(path=...) runs another
7
+ cards = n.list() # every card, lost ones included; no CUDA context opened
8
+ probes = n.probe_all() # does each card run a kernel, and its capability sheet
9
+ for ev in n.watch(every=1.0): ... # Xid events, lost cards, samples
10
+
11
+ Documents are the parsed JSON itself, keys exactly as the contract names them (card["tempC"]). Their
12
+ types (documents.py) are TypedDicts generated from the JSON Schema the binary prints with `nvq schema`
13
+ (`make types`), so a type checker knows every key and a value the card cannot report is a key that is
14
+ absent, never 0. `nvq` on the command line is the binary itself."""
15
+
16
+ # NVQ.list shadows the builtin inside the class body: annotations there must not be evaluated.
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ from collections.abc import Generator
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from . import documents
30
+ from .documents import (
31
+ Card,
32
+ EventEccDouble,
33
+ EventLost,
34
+ EventSample,
35
+ EventStart,
36
+ EventWaitError,
37
+ EventXid,
38
+ ListOk,
39
+ ProbeFailed,
40
+ ProbeOk,
41
+ )
42
+
43
+ __all__ = [
44
+ "BINARY",
45
+ "NVQ",
46
+ "SCHEMA",
47
+ "Card",
48
+ "Error",
49
+ "EventEccDouble",
50
+ "EventLost",
51
+ "EventSample",
52
+ "EventStart",
53
+ "EventWaitError",
54
+ "EventXid",
55
+ "ListOk",
56
+ "ProbeFailed",
57
+ "ProbeOk",
58
+ "WatchEvent",
59
+ "documents",
60
+ ]
61
+
62
+ type WatchEvent = EventStart | EventXid | EventEccDouble | EventLost | EventWaitError | EventSample
63
+
64
+ SCHEMA = 1
65
+ """The document version this package reads."""
66
+
67
+ BINARY = Path(__file__).parent / "bin" / "nvq"
68
+ """The binary this wheel carries; absent from the wheel for platforms nvq does not run on."""
69
+
70
+
71
+ class Error(Exception):
72
+ """A failure nvq reported as a document: `code` from the schema, nvq's `exit` code. When NVML is
73
+ unusable, `list` holds the kernel's roster all the same, every card in state "unknown"."""
74
+
75
+ def __init__(self, code: str, detail: str, exit: int, list: ListOk | None = None):
76
+ super().__init__(f"nvq: {code}: {detail} (exit {exit})")
77
+ self.code = code
78
+ self.detail = detail
79
+ self.exit = exit
80
+ self.list = list
81
+
82
+
83
+ @dataclass(frozen=True, kw_only=True)
84
+ class NVQ:
85
+ """The binary to run: `path`, or when None the one this wheel carries.
86
+
87
+ `deadline` (seconds) bounds one list or probe inside nvq itself (--deadline-ms); None keeps nvq's
88
+ default (10 s list, 60 s probe). Each call's `timeout` bounds the whole process from outside: nvq is
89
+ killed and subprocess.TimeoutExpired raised."""
90
+
91
+ path: Path | str | None = None
92
+ deadline: float | None = None
93
+
94
+ def list(self, timeout: float | None = None) -> ListOk:
95
+ """Every card the kernel bound. A lost card is a card with state "lost", not an error."""
96
+ doc, code = self._run(["list"], timeout)
97
+ if "cards" not in doc:
98
+ raise _error(doc, code)
99
+ if "error" in doc:
100
+ raise Error(doc["error"]["code"], doc["error"]["detail"], code, doc)
101
+ return doc
102
+
103
+ def probe(self, uuid: str, timeout: float | None = None) -> ProbeOk | ProbeFailed:
104
+ """Runs a kernel on one card, with only that card visible to CUDA. It opens a CUDA context: do
105
+ not aim it at a card mid-work you care about. A failed step is a ProbeFailed, not an error."""
106
+ doc, code = self._run(["probe", uuid], timeout)
107
+ if "ok" not in doc:
108
+ raise _error(doc, code)
109
+ return doc
110
+
111
+ def probe_all(
112
+ self, timeout: float | None = None
113
+ ) -> list[ProbeOk | ProbeFailed | documents.ErrorDocument]:
114
+ """Probes every card at once, each in its own process: a hung card cannot stall the others. An
115
+ entry is a child's own document, so a child that failed as a whole is an error document."""
116
+ doc, code = self._run(["probe", "all"], timeout)
117
+ if "probes" not in doc:
118
+ raise _error(doc, code)
119
+ return doc["probes"]
120
+
121
+ def watch(self, every: float | None = None) -> Generator[WatchEvent]:
122
+ """Events until nvq exits or the iterator is closed (which kills nvq). `every` (seconds) adds a
123
+ sample per card at that interval. nvq ending on its own raises: watch runs until killed."""
124
+ args = ["watch", *(["--every-ms", str(round(every * 1000))] if every else [])]
125
+ with (
126
+ tempfile.TemporaryFile() as stderr,
127
+ subprocess.Popen([str(self._binary()), *args], stdout=subprocess.PIPE, stderr=stderr) as p,
128
+ ):
129
+ assert p.stdout is not None
130
+ try:
131
+ for line in p.stdout:
132
+ if not line.strip():
133
+ continue
134
+ doc = _document(line, args)
135
+ if "event" not in doc:
136
+ raise _error(doc, p.wait())
137
+ yield doc
138
+ code = p.wait()
139
+ stderr.seek(0)
140
+ raise RuntimeError(f"nvq watch ended: exit {code}: {stderr.read().decode().strip()}")
141
+ finally:
142
+ p.kill() # a no-op once nvq exited; leaving the with block reaps it
143
+
144
+ def _binary(self) -> Path:
145
+ return Path(self.path) if self.path is not None else _own_binary()
146
+
147
+ def _run(self, args: list[str], timeout: float | None) -> tuple[Any, int]:
148
+ """The document nvq printed and its exit code. A process that printed none (not found, killed, a
149
+ usage error) raises here; any other exit is the document's to explain."""
150
+ deadline = ["--deadline-ms", str(round(self.deadline * 1000))] if self.deadline is not None else []
151
+ p = subprocess.run(
152
+ [str(self._binary()), *deadline, *args], capture_output=True, timeout=timeout, check=False
153
+ )
154
+ if not p.stdout.strip():
155
+ stderr = p.stderr.decode(errors="replace").strip()
156
+ raise RuntimeError(f"nvq {' '.join(args)}: exit {p.returncode}, no document: {stderr}")
157
+ return _document(p.stdout, args), p.returncode
158
+
159
+
160
+ def _own_binary() -> Path:
161
+ if not BINARY.exists():
162
+ raise FileNotFoundError(
163
+ f"nvq: this wheel carries no binary for {sys.platform} (only the linux x86_64 and aarch64 "
164
+ f"wheels do); pass NVQ(path=...)"
165
+ )
166
+ return BINARY
167
+
168
+
169
+ def _document(raw: bytes, args: list[str]) -> Any:
170
+ """A parsed document, typed by the caller: the schema, not this function, vouches for its shape."""
171
+ doc = json.loads(raw)
172
+ if not isinstance(doc, dict) or doc.get("nvq") != SCHEMA:
173
+ raise ValueError(f"nvq {' '.join(args)}: not a version {SCHEMA} document: {raw[:200]!r}")
174
+ return doc
175
+
176
+
177
+ def _error(doc: Any, code: int) -> Error:
178
+ if not isinstance(doc.get("error"), dict):
179
+ raise TypeError(f"nvq: neither the expected document nor an error: {json.dumps(doc)[:200]}")
180
+ return Error(doc["error"]["code"], doc["error"]["detail"], code)
181
+
182
+
183
+ def main() -> None:
184
+ """`nvq` on PATH: becomes the carried binary, arguments and exit code untouched."""
185
+ binary = _own_binary()
186
+ os.execv(binary, [str(binary), *sys.argv[1:]])
nvq/documents.py ADDED
@@ -0,0 +1,490 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: nvq.schema.json
3
+
4
+ from __future__ import annotations
5
+ from typing import Any, Literal, NotRequired, TypedDict
6
+
7
+
8
+ type Nvq = Any
9
+ """
10
+ NVIDIA cards as JSON. Every document carries "nvq": 1, the version of this schema; additive fields keep it, a removal or a change of meaning bumps it. Readers ignore keys they do not know. A value the card cannot report is absent, never zero, and named under "unsupported".
11
+ """
12
+
13
+
14
+ type SchemaVersion = Literal[1]
15
+
16
+
17
+ type Uuid = str
18
+
19
+
20
+ type Bus = str
21
+ """
22
+ PCI location as the kernel prints it
23
+ """
24
+
25
+
26
+ type UnixTime = float
27
+ """
28
+ seconds since the epoch, millisecond precision
29
+ """
30
+
31
+
32
+ type Code = Literal[
33
+ "uninitialized",
34
+ "invalid_argument",
35
+ "not_supported",
36
+ "no_permission",
37
+ "already_initialized",
38
+ "not_found",
39
+ "insufficient_size",
40
+ "insufficient_power",
41
+ "driver_not_loaded",
42
+ "timeout",
43
+ "irq_issue",
44
+ "library_not_found",
45
+ "function_not_found",
46
+ "corrupted_inforom",
47
+ "gpu_is_lost",
48
+ "reset_required",
49
+ "operating_system",
50
+ "lib_rm_version_mismatch",
51
+ "in_use",
52
+ "memory",
53
+ "no_data",
54
+ "vgpu_ecc_not_supported",
55
+ "insufficient_resources",
56
+ "freq_not_supported",
57
+ "argument_version_mismatch",
58
+ "deprecated",
59
+ "not_ready",
60
+ "gpu_not_found",
61
+ "invalid_state",
62
+ "unknown",
63
+ "too_many_cards",
64
+ "deadline",
65
+ ]
66
+ """
67
+ Why something failed: NVML's return codes by name, plus nvq's own.
68
+ """
69
+
70
+
71
+ class ErrorBody(TypedDict):
72
+ code: Code
73
+ detail: str
74
+
75
+
76
+ type Limit = Literal[
77
+ "gpu_idle",
78
+ "applications_clocks",
79
+ "sw_power_cap",
80
+ "hw_slowdown",
81
+ "sync_boost",
82
+ "sw_thermal",
83
+ "hw_thermal",
84
+ "hw_power_brake",
85
+ "display_clocks",
86
+ ]
87
+ """
88
+ Why the card's clocks are below their maximum right now (NVML clock event reasons).
89
+ """
90
+
91
+
92
+ class MemoryMiB(TypedDict):
93
+ total: int
94
+ used: int
95
+
96
+
97
+ class PowerW(TypedDict):
98
+ limit: NotRequired[float]
99
+ """
100
+ enforced limit
101
+ """
102
+ draw: NotRequired[float]
103
+
104
+
105
+ class ClocksMHz(TypedDict):
106
+ sm: int
107
+ mem: int
108
+
109
+
110
+ class UtilPct(TypedDict):
111
+ gpu: int
112
+ mem: int
113
+
114
+
115
+ class Pcie(TypedDict):
116
+ """
117
+ the link now and at best; a riser shows here (gen 1, width 1)
118
+ """
119
+
120
+ gen: int
121
+ width: int
122
+ maxGen: int
123
+ maxWidth: int
124
+ replays: NotRequired[int]
125
+ """
126
+ link-level retransmissions since the driver loaded; one that keeps rising is a failing riser or slot
127
+ """
128
+
129
+
130
+ class Process(TypedDict):
131
+ pid: int
132
+ usedMiB: NotRequired[int]
133
+ """
134
+ absent when NVML cannot see it (another container)
135
+ """
136
+
137
+
138
+ type Ms = dict[str, float]
139
+ """
140
+ milliseconds per step, in the order run
141
+ """
142
+
143
+
144
+ class ProbeFailed(TypedDict):
145
+ nvq: SchemaVersion
146
+ uuid: Uuid
147
+ ok: Literal[False]
148
+ step: str
149
+ """
150
+ load, init, device, uuid, name, memory, driver, attributes, context, alloc, memset, jit, function, launch, sync, copy, verify, free, unload, teardown; in probe all also hang (killed or stuck) and crash (died without a document)
151
+ """
152
+ error: str
153
+ """
154
+ a CUDA_ERROR_* name, or not_visible, more_than_one_visible, uuid_mismatch, no_compute_capability, wrong_result, library_not_found, function_not_found: <name>, killed_at_deadline, stuck_in_driver, no_output
155
+ """
156
+ ms: NotRequired[Ms]
157
+ signal: NotRequired[int]
158
+ """
159
+ crash: the signal that ended the child
160
+ """
161
+ exit: NotRequired[int]
162
+ """
163
+ crash: the child's exit code
164
+ """
165
+ pid: NotRequired[int]
166
+ """
167
+ stuck_in_driver: the child that could not be reaped
168
+ """
169
+
170
+
171
+ class Arch(TypedDict):
172
+ compute: str
173
+ """
174
+ "8.6"
175
+ """
176
+ sm: str
177
+ """
178
+ the compile target, "sm_86"
179
+ """
180
+ family: Literal[
181
+ "kepler", "maxwell", "pascal", "volta", "turing", "ampere", "ada", "hopper", "blackwell", "unknown"
182
+ ]
183
+
184
+
185
+ class Layout(TypedDict):
186
+ sms: NotRequired[int]
187
+ """
188
+ streaming multiprocessors
189
+ """
190
+ warpSize: NotRequired[int]
191
+ maxThreadsPerBlock: NotRequired[int]
192
+ maxThreadsPerSM: NotRequired[int]
193
+ maxBlocksPerSM: NotRequired[int]
194
+ maxWarpsPerSM: NotRequired[int]
195
+ """
196
+ maxThreadsPerSM / warpSize
197
+ """
198
+ maxBlockDim: NotRequired[list[int]]
199
+ maxGridDim: NotRequired[list[int]]
200
+
201
+
202
+ class Registers(TypedDict):
203
+ perBlock: NotRequired[int]
204
+ perSM: NotRequired[int]
205
+
206
+
207
+ class SharedBytes(TypedDict):
208
+ perBlock: NotRequired[int]
209
+ """
210
+ without opting in
211
+ """
212
+ perBlockOptin: NotRequired[int]
213
+ """
214
+ the most a kernel may request with cuFuncSetAttribute
215
+ """
216
+ perSM: NotRequired[int]
217
+
218
+
219
+ class Memory(TypedDict):
220
+ totalMiB: int
221
+ busWidthBits: NotRequired[int]
222
+ clockMHz: NotRequired[int]
223
+ peakGBs: NotRequired[float]
224
+ """
225
+ clockMHz × 2 (double data rate) × busWidthBits / 8, in GB/s
226
+ """
227
+ l2Bytes: NotRequired[int]
228
+ l2PersistingBytes: NotRequired[int]
229
+ constantBytes: NotRequired[int]
230
+
231
+
232
+ class ProbeOk(TypedDict):
233
+ nvq: SchemaVersion
234
+ uuid: Uuid
235
+ ok: Literal[True]
236
+ name: str
237
+ cuda: str
238
+ """
239
+ CUDA driver API version, "13.1"
240
+ """
241
+ arch: Arch
242
+ layout: Layout
243
+ registers: Registers
244
+ sharedBytes: SharedBytes
245
+ memory: Memory
246
+ smClockMHz: NotRequired[int]
247
+ """
248
+ the SM's peak clock
249
+ """
250
+ copyEngines: NotRequired[int]
251
+ """
252
+ copies that can overlap a kernel
253
+ """
254
+ concurrentKernels: NotRequired[bool]
255
+ computeMode: NotRequired[Literal["default", "prohibited", "exclusive_process", "unknown"]]
256
+ ecc: NotRequired[bool]
257
+ integrated: NotRequired[bool]
258
+ multiGpuBoard: NotRequired[bool]
259
+ unsupported: NotRequired[list[str]]
260
+ """
261
+ attributes the driver did not report
262
+ """
263
+ ms: Ms
264
+
265
+
266
+ class WatchCard(TypedDict):
267
+ uuid: Uuid
268
+ bus: Bus
269
+ state: Literal["ok", "lost", "error"]
270
+ error: NotRequired[Code]
271
+ events: NotRequired[list[Literal["xid", "ecc_double"]]]
272
+ """
273
+ event kinds registered for this card
274
+ """
275
+ eventsError: NotRequired[Code]
276
+
277
+
278
+ class EventXid(TypedDict):
279
+ """
280
+ An Xid error the driver raised; uuid and bus absent when NVML named a device nvq does not watch.
281
+ """
282
+
283
+ nvq: SchemaVersion
284
+ event: Literal["xid"]
285
+ at: UnixTime
286
+ uuid: NotRequired[Uuid]
287
+ bus: NotRequired[Bus]
288
+ xid: int
289
+ meaning: str
290
+ """
291
+ NVIDIA's name for it; empty for an Xid nvq has no name for
292
+ """
293
+
294
+
295
+ class EventEccDouble(TypedDict):
296
+ """
297
+ A double-bit ECC error; uuid and bus absent when NVML named a device nvq does not watch.
298
+ """
299
+
300
+ nvq: SchemaVersion
301
+ event: Literal["ecc_double"]
302
+ at: UnixTime
303
+ uuid: NotRequired[Uuid]
304
+ bus: NotRequired[Bus]
305
+ data: int
306
+
307
+
308
+ class EventLost(TypedDict):
309
+ """
310
+ A card NVML stopped reaching, reported once.
311
+ """
312
+
313
+ nvq: SchemaVersion
314
+ event: Literal["lost"]
315
+ at: UnixTime
316
+ uuid: Uuid
317
+ bus: Bus
318
+ error: Code
319
+
320
+
321
+ class EventWaitError(TypedDict):
322
+ """
323
+ Waiting for driver events failed; watch retries after a second.
324
+ """
325
+
326
+ nvq: SchemaVersion
327
+ event: Literal["wait_error"]
328
+ at: UnixTime
329
+ error: Code
330
+
331
+
332
+ class EventSample(TypedDict):
333
+ """
334
+ One card's readings, every --every-ms; a reading the card cannot give is absent.
335
+ """
336
+
337
+ nvq: SchemaVersion
338
+ event: Literal["sample"]
339
+ at: UnixTime
340
+ uuid: Uuid
341
+ bus: Bus
342
+ tempC: NotRequired[int]
343
+ powerW: NotRequired[float]
344
+ smMHz: NotRequired[int]
345
+ utilPct: NotRequired[int]
346
+ fanPct: NotRequired[int]
347
+ energyJ: NotRequired[float]
348
+ """
349
+ cumulative, as in list
350
+ """
351
+ pcieReplays: NotRequired[int]
352
+ """
353
+ cumulative, as in list
354
+ """
355
+ limits: NotRequired[list[Limit]]
356
+
357
+
358
+ class Version(TypedDict):
359
+ nvq: SchemaVersion
360
+ version: str
361
+
362
+
363
+ class ErrorDocument(TypedDict):
364
+ """
365
+ The whole command failed, or (in list) NVML could not be used.
366
+ """
367
+
368
+ nvq: SchemaVersion
369
+ error: ErrorBody
370
+
371
+
372
+ class Card(TypedDict):
373
+ index: int
374
+ """
375
+ position in bus order; not CUDA's or nvidia-smi's index
376
+ """
377
+ uuid: Uuid
378
+ bus: Bus
379
+ minor: int
380
+ """
381
+ /dev/nvidia<minor>; -1 when the kernel did not say
382
+ """
383
+ model: str
384
+ state: Literal["ok", "lost", "error", "unknown"]
385
+ """
386
+ ok: NVML reaches it. lost: fell off the bus (gpu_is_lost). error: another NVML failure. unknown: NVML unusable
387
+ """
388
+ error: NotRequired[Code]
389
+ """
390
+ with state lost or error
391
+ """
392
+ compute: NotRequired[str]
393
+ """
394
+ CUDA compute capability, "8.6"
395
+ """
396
+ memoryMiB: NotRequired[MemoryMiB]
397
+ powerW: NotRequired[PowerW]
398
+ tempC: NotRequired[int]
399
+ fanPct: NotRequired[int]
400
+ """
401
+ target fan speed; consumer cards with fan control off report not_supported
402
+ """
403
+ clocksMHz: NotRequired[ClocksMHz]
404
+ utilPct: NotRequired[UtilPct]
405
+ pstate: NotRequired[int]
406
+ """
407
+ performance state, 0 fastest
408
+ """
409
+ pcie: NotRequired[Pcie]
410
+ """
411
+ the link now and at best; a riser shows here (gen 1, width 1)
412
+ """
413
+ energyJ: NotRequired[float]
414
+ """
415
+ energy the card used since the driver loaded, joules; the difference of two readings over their interval is average power
416
+ """
417
+ persistence: NotRequired[bool]
418
+ limits: NotRequired[list[Limit]]
419
+ processes: NotRequired[list[Process]]
420
+ """
421
+ compute processes holding the card
422
+ """
423
+ unsupported: NotRequired[list[str]]
424
+ """
425
+ fields this card does not report
426
+ """
427
+ errors: NotRequired[dict[str, Code]]
428
+ """
429
+ fields whose query failed, by field
430
+ """
431
+
432
+
433
+ type Probe = ErrorDocument | ProbeOk | ProbeFailed
434
+
435
+
436
+ class ProbeAllOk(TypedDict):
437
+ """
438
+ One entry per card; a card that failed is an entry that says how, not a failure.
439
+ """
440
+
441
+ nvq: SchemaVersion
442
+ probes: list[ErrorDocument | ProbeOk | ProbeFailed]
443
+
444
+
445
+ class EventStart(TypedDict):
446
+ """
447
+ First line: the cards watched and the event kinds registered on each.
448
+ """
449
+
450
+ nvq: SchemaVersion
451
+ event: Literal["start"]
452
+ at: UnixTime
453
+ cards: list[WatchCard]
454
+
455
+
456
+ class ListOk(TypedDict):
457
+ """
458
+ Every card the kernel bound; lost ones are cards with state lost, not a failure.
459
+ """
460
+
461
+ nvq: SchemaVersion
462
+ driver: str
463
+ """
464
+ kernel module version from /proc, "590.48.01"; empty when unreadable
465
+ """
466
+ nvml: NotRequired[str]
467
+ """
468
+ NVML library version
469
+ """
470
+ cuda: NotRequired[str]
471
+ """
472
+ highest CUDA version the driver supports, "13.1"
473
+ """
474
+ error: NotRequired[ErrorBody]
475
+ """
476
+ NVML could not be used; every card's state is unknown
477
+ """
478
+ cards: list[Card]
479
+
480
+
481
+ type ProbeAll = ErrorDocument | ProbeAllOk
482
+
483
+
484
+ type Event = ErrorDocument | EventStart | EventXid | EventEccDouble | EventLost | EventWaitError | EventSample
485
+ """
486
+ One line of watch, told apart by "event".
487
+ """
488
+
489
+
490
+ type List = ErrorDocument | ListOk
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.5
2
+ Name: nvq
3
+ Version: 0.4.0
4
+ Summary: NVIDIA cards as JSON, for programs: list, probe, watch. Carries the nvq binary.
5
+ Project-URL: Source, https://github.com/adriangalilea/nvq
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+
9
+ # nvq
10
+
11
+ NVIDIA cards as JSON, for programs. This package carries the [nvq](https://github.com/adriangalilea/nvq) binary for linux x86_64 and aarch64 (glibc 2.17 or newer) and runs it: `pip install nvq` is the whole install, and `nvq` lands on PATH too.
12
+
13
+ ```python
14
+ import nvq
15
+
16
+ n = nvq.NVQ()
17
+ for card in n.list()["cards"]:
18
+ print(card["model"], card["state"], card.get("tempC"))
19
+
20
+ for ev in n.watch(every=1.0):
21
+ if ev["event"] == "xid":
22
+ print(ev["xid"], ev["meaning"])
23
+ ```
24
+
25
+ Documents are the parsed JSON, keys as the [contract](https://github.com/adriangalilea/nvq/blob/main/schema/nvq.schema.json) names them; a value the card cannot report is an absent key. Their types (`nvq.documents`) are TypedDicts generated from that schema. A command that failed as a whole raises `nvq.Error` with its `code` and `exit`; a lost card or a failed probe step is data, not an exception. Each call takes a `timeout`: past it nvq is killed, so a card hung in the driver never hangs your program.
@@ -0,0 +1,6 @@
1
+ nvq/__init__.py,sha256=7I8wXhYMzImZFKZ_bKAD8U1QLWsLS2UeMDr3-s_QS9A,7579
2
+ nvq/documents.py,sha256=rDkiC6EZTzoE83DjklT6FUP9SAU37ZGUROvBR2CP7aE,10661
3
+ nvq-0.4.0.dist-info/METADATA,sha256=GkjO0jzWKsck7M6-JqwtkQSGigV0K_xXdMIZ7un9jbo,1251
4
+ nvq-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ nvq-0.4.0.dist-info/entry_points.txt,sha256=xREF6LumDfLbWCmIbVcbPvHCvvFNz9tPcdcfNuRRNXQ,33
6
+ nvq-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nvq = nvq:main