protocol-validation-suite-framework-apis 26.7.1__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.
@@ -0,0 +1,616 @@
1
+ # Code Review Report — Protocol_Validation_Suite_APIs
2
+
3
+ **Reviewer disposition:** by-the-book, strict.
4
+ **Scope:** all hand-written Python in `Protocol_Validation_Suite_APIs/`.
5
+ **Excluded:** auto-generated `stubs/GRPC_pb2.py` and `stubs/GRPC_pb2_grpc.py` (generated, "DO NOT EDIT"); docstring/inline-comment prose (except where a docstring asserts a wrong *type* or a contract the code contradicts).
6
+
7
+ **Files reviewed:**
8
+ - `__init__.py`
9
+ - `models.py`
10
+ - `protocol_validation_suite_apis.py`
11
+ - `_client.py`
12
+ - `_decorators.py`
13
+ - `_utils.py`
14
+
15
+ ### Summary
16
+
17
+ | Severity | Count | IDs |
18
+ |---|---|---|
19
+ | 🔴 Critical | 5 | C1–C5 |
20
+ | 🟠 High | 8 | C6–C13 |
21
+ | 🟡 Medium | 17 | S1–S17 |
22
+
23
+ **Most urgent:** C1 and C2 are outright functional bugs that hit real callers. C6 is the systemic issue — the catch-all decorator masks C1, C7, C13 and every other failure as a silent `None`. Fix C6's swallowing behavior first, because it currently hides the rest.
24
+
25
+ ---
26
+
27
+ ## 🔴 Critical — Functional defects
28
+
29
+ ---
30
+
31
+ ### C1. `Selected_Step_Numbers` type mismatch — `list[int]` sent to a `repeated string` field
32
+
33
+ **Location:** `_client.py:394` (definition of type at `models.py:51`)
34
+
35
+ **What is wrong:**
36
+ The proto defines `Compile_Details.Selected_Step_Numbers` as **`repeated string`**. `compile_sequence` passes `compile_sequence_details.selected_step_numbers` straight through, and `CompileSequenceRequest.selected_step_numbers` is typed `list[int]`.
37
+
38
+ ```python
39
+ # _client.py:391-397
40
+ request = GRPC_pb2.Compile_Details(
41
+ Sequence_Name=compile_sequence_details.sequence_name,
42
+ Variable_Name=compile_sequence_details.variable_name,
43
+ Selected_Step_Numbers=compile_sequence_details.selected_step_numbers, # list[int] -> repeated string
44
+ Skip_Migrate=compile_sequence_details.skip_migrate,
45
+ Run_Sequence_Manager=compile_sequence_details.run_sequence_manager,
46
+ )
47
+ ```
48
+
49
+ **Why it is critical:**
50
+ protobuf raises `TypeError` when serializing `int` into a `string` field. Any caller passing a non-empty `selected_step_numbers` list gets a hard failure. Because of the catch-all decorator (C6), that failure is swallowed and the caller silently receives `None` instead of a compile result — the feature simply does not work and gives no signal that it broke.
51
+
52
+ **Suggested fix:**
53
+ Convert to strings before building the message:
54
+
55
+ ```python
56
+ Selected_Step_Numbers=[str(step) for step in compile_sequence_details.selected_step_numbers],
57
+ ```
58
+
59
+ ---
60
+
61
+ ### C2. `run_sequence` returns a protobuf message but is typed and documented as `str`
62
+
63
+ **Location:** `_client.py:503-504`
64
+
65
+ **What is wrong:**
66
+ `Get_Report_Details` returns a `Report_Details` message whose payload is the `Report_Folder_Path` field. The code returns the whole message object, while the signature says `-> str` and the docstring promises "Absolute path to the generated report directory."
67
+
68
+ ```python
69
+ # _client.py:503-504
70
+ report_path = self.stub.Get_Report_Details(GRPC_pb2.Trigger(Trigger=True))
71
+ return report_path # <-- returns a Report_Details message, not a str
72
+ ```
73
+
74
+ **Why it is critical:**
75
+ The documented and annotated return type is violated. Callers that do `os.path.exists(result)`, string concatenation, or logging on the return value get a protobuf object and either crash or behave incorrectly. This breaks the primary success-path contract of the method.
76
+
77
+ **Suggested fix:**
78
+
79
+ ```python
80
+ report_details = self.stub.Get_Report_Details(GRPC_pb2.Trigger(Trigger=True))
81
+ return report_details.Report_Folder_Path
82
+ ```
83
+
84
+ ---
85
+
86
+ ### C3. Inconsistent return contract in `run_sequence` — error path returns a human sentence
87
+
88
+ **Location:** `_client.py:499` and `_client.py:506`
89
+
90
+ **What is wrong:**
91
+ The same function returns three structurally different things:
92
+ - success → a report path
93
+ - runtime errors → `"Sequence run encountered errors: ..."` (line 499)
94
+ - compile failure → `"Sequence compilation failed, cannot run sequence ..."` (line 506)
95
+
96
+ ```python
97
+ # _client.py:499
98
+ return "Sequence run encountered errors: " + ";\n ".join(parts)
99
+ ...
100
+ # _client.py:506
101
+ return f"Sequence compilation failed, cannot run sequence {run_sequence_details.sequence_name} - {response.Message}."
102
+ ```
103
+
104
+ **Why it is critical:**
105
+ A caller cannot programmatically distinguish success from failure without string-sniffing the return value. Automation code built on this API cannot reliably branch on outcome, which defeats the purpose of an automation SDK.
106
+
107
+ **Suggested fix:**
108
+ Return a structured result instead of overloaded prose, e.g. a dataclass:
109
+
110
+ ```python
111
+ @dataclass
112
+ class RunSequenceResult:
113
+ success: bool
114
+ report_path: str | None = None
115
+ errors: list[tuple[int, str]] | None = None
116
+ message: str | None = None
117
+ ```
118
+
119
+ …and populate `success`/`report_path`/`errors`/`message` on each path so callers branch on fields, not strings.
120
+
121
+ ---
122
+
123
+ ### C4. Dead branch: `step is None` can never be true
124
+
125
+ **Location:** `_client.py:489-490`
126
+
127
+ **What is wrong:**
128
+ `errors` only ever receives tuples where `step_number_int is not None` (guarded at `_client.py:465`). So in the reporting loop, `step` is never `None`.
129
+
130
+ ```python
131
+ # _client.py:465-466 -- guard ensures step is never None when added
132
+ if step_number_int is not None:
133
+ errors.add((step_number_int, info))
134
+ ...
135
+ # _client.py:489-490 -- therefore unreachable
136
+ if step is None:
137
+ step_label = "Step Unknown"
138
+ ```
139
+
140
+ **Why it is critical (correctness):**
141
+ Unreachable code signals a logic gap: either the guard at line 465 is wrong (some errors with unknown step should be captured but are being dropped), or the `None` branch is stale. Either way the intent and the code disagree, which is exactly the kind of latent defect a strict review must flag.
142
+
143
+ **Suggested fix:**
144
+ Decide the intent. If unknown-step errors should be reported, remove the `is not None` guard at line 465 and keep the `None` branch. If not, remove the dead `if step is None` branch entirely.
145
+
146
+ ---
147
+
148
+ ### C5. Comment/logic contradiction on end-of-sequence detection
149
+
150
+ **Location:** `_client.py:468-472` vs `_client.py:491-492`
151
+
152
+ **What is wrong:**
153
+ The loop comment says `step_number_int == 0` is "Completion of Sequence" and `-1` is "Error before the start of sequence." But the label logic only special-cases `-1` as `"End of Sequence"` and formats `0` as `"Step 0"`.
154
+
155
+ ```python
156
+ # _client.py:468-472
157
+ # -- Completion of Sequence (step_number_int == 0)
158
+ # -- Error before the start of sequence (step_number_int == -1)
159
+ if step_number_int in [0, -1]:
160
+ break
161
+ ...
162
+ # _client.py:491-494
163
+ elif step == -1:
164
+ step_label = "End of Sequence"
165
+ else:
166
+ step_label = f"Step {step}" # step == 0 falls here -> "Step 0"
167
+ ```
168
+
169
+ **Why it is critical:**
170
+ The two pieces of logic disagree about what `0` and `-1` mean. `-1` is documented as "error before start" but labeled "End of Sequence"; `0` is documented as "completion" but rendered as an ordinary "Step 0". Error reports emitted to users will be mislabeled, and the mismatch strongly suggests the sentinel handling is wrong.
171
+
172
+ **Suggested fix:**
173
+ Make labels match the documented semantics, e.g.:
174
+
175
+ ```python
176
+ if step == -1:
177
+ step_label = "Before Sequence Start"
178
+ elif step == 0:
179
+ step_label = "End of Sequence"
180
+ else:
181
+ step_label = f"Step {step}"
182
+ ```
183
+
184
+ (Confirm the true sentinel meaning with the server contract before finalizing.)
185
+
186
+ ---
187
+
188
+ ## 🟠 High — Robustness / correctness
189
+
190
+ ---
191
+
192
+ ### C6. Blanket `@handle_exceptions` swallows everything and returns `None`
193
+
194
+ **Location:** `_decorators.py:34-41`
195
+
196
+ **What is wrong:**
197
+ Every public method is wrapped so that **any** exception is printed and converted to `None`.
198
+
199
+ ```python
200
+ # _decorators.py:35-41
201
+ def wrapper(*args, **kwargs):
202
+ try:
203
+ return func(*args, **kwargs)
204
+ except Exception as e:
205
+ print(f"[ERROR] Exception occurred in '{func_name}': {e}")
206
+ traceback.print_exc()
207
+ return None
208
+ ```
209
+
210
+ **Why it is critical:**
211
+ - Functions documented `-> bool` / `-> str` / `-> dict` can silently return `None`, violating their type contract on every error path.
212
+ - It masks real bugs (C1, C7, C13) — failures become silent `None`s with no exception to catch.
213
+ - `launch_server` documents `Raises: FileNotFoundError` (`_client.py:71`), `_wait_until_server_is_ready` documents `Raises: TimeoutError`, and `get_paths` documents `Raises: FileNotFoundError, JSONDecodeError` — but the decorator guarantees none of these can ever propagate. Docstring and behavior are in direct conflict.
214
+ - `print` for error reporting is inappropriate for a library; use `logging`.
215
+
216
+ **Suggested fix:**
217
+ - Do not blanket-swallow. Catch specific, expected exceptions where recovery is meaningful and let the rest propagate.
218
+ - Replace `print` with the `logging` module.
219
+ - If a "graceful" mode is genuinely required, make it opt-in and return a typed result (never `None` masquerading as `bool`/`str`).
220
+
221
+ ---
222
+
223
+ ### C7. `close_server` crashes / no-ops if the server was never launched
224
+
225
+ **Location:** `_client.py:205`
226
+
227
+ **What is wrong:**
228
+ `self.stub` is `None` until `_init_grpc_channel` runs. `close_server` dereferences it unconditionally.
229
+
230
+ ```python
231
+ # _client.py:204-205
232
+ trigger = Trigger(Trigger=False)
233
+ response = self.stub.Close_Server(trigger) # AttributeError if stub is None
234
+ ```
235
+
236
+ **Why it is critical:**
237
+ If `launch_server` failed or was never called, this raises `AttributeError`, which C6 converts to `None`. The caller believes shutdown happened when it did not — resource/process cleanup is skipped silently.
238
+
239
+ **Suggested fix:**
240
+
241
+ ```python
242
+ if self.stub is not None:
243
+ self.stub.Close_Server(Trigger(Trigger=False))
244
+ ```
245
+
246
+ …and still run the process/thread teardown below it regardless.
247
+
248
+ ---
249
+
250
+ ### C8. `_wait_until_server_is_ready` leaks a channel and does not reuse the real one
251
+
252
+ **Location:** `_client.py:172-174`
253
+
254
+ **What is wrong:**
255
+ A fresh channel is created only for the readiness probe and never closed; `_init_grpc_channel` then creates a second channel.
256
+
257
+ ```python
258
+ # _client.py:172-174
259
+ grpc.channel_ready_future(
260
+ grpc.insecure_channel(self.__grpc_target_host) # created, never closed
261
+ ).result(timeout=self.__grpc_server_timeout)
262
+ ```
263
+
264
+ **Why it is critical (resource leak):**
265
+ Every launch leaks one gRPC channel. Two channels exist for the same target. Over repeated launch cycles this accumulates.
266
+
267
+ **Suggested fix:**
268
+ Create the channel once, store it, probe readiness on it, and reuse it in `_init_grpc_channel`:
269
+
270
+ ```python
271
+ self.channel = grpc.insecure_channel(self.__grpc_target_host)
272
+ grpc.channel_ready_future(self.channel).result(timeout=self.__grpc_server_timeout)
273
+ # _init_grpc_channel then just binds the stub to self.channel
274
+ ```
275
+
276
+ ---
277
+
278
+ ### C9. Error-watcher exception "signalling" is effectively useless + thread race
279
+
280
+ **Location:** `_client.py:138-152` (and read side at `_client.py:177`)
281
+
282
+ **What is wrong:**
283
+ Fatal stderr lines are appended to `_server_launch_exceptions`, but this list is only ever read inside `_wait_until_server_is_ready` on a `FutureTimeoutError`.
284
+
285
+ ```python
286
+ # _client.py:177-178
287
+ if len(self._server_launch_exceptions) > 0:
288
+ raise self._server_launch_exceptions[0]
289
+ ```
290
+
291
+ **Why it is critical:**
292
+ - If the server becomes reachable but the watcher later captures a fatal error, nothing surfaces it — the error is collected and ignored.
293
+ - The list is written on a daemon thread and read on the main thread with no lock → data race.
294
+
295
+ **Suggested fix:**
296
+ - Guard `_server_launch_exceptions` with a `threading.Lock` (or use a `queue.Queue`).
297
+ - Check for captured fatal errors after RPC calls, or fail fast when the watcher records one, rather than only on the readiness timeout.
298
+
299
+ ---
300
+
301
+ ### C10. Substring error-classification is fragile
302
+
303
+ **Location:** `_client.py:124-133`
304
+
305
+ **What is wrong:**
306
+ Free-form stderr is lowercased and matched against substrings like `"unavailable"` and `"connection refused"` to decide "non-fatal."
307
+
308
+ ```python
309
+ # _client.py:124-133
310
+ conn_err_markers = ["failed to connect to all addresses", "connectex",
311
+ "connection refused", "10061", "unavailable"]
312
+ low = stripped_line.lower()
313
+ if any(m in low for m in conn_err_markers):
314
+ continue
315
+ ```
316
+
317
+ **Why it is critical:**
318
+ Any legitimate fatal error whose message merely *contains* one of these words (e.g. a real failure mentioning "service unavailable") is misclassified as non-fatal and dropped, hiding genuine startup failures.
319
+
320
+ **Suggested fix:**
321
+ Classify on structured signals (gRPC status codes / typed exceptions) rather than substring matching of human-readable text. If substring matching is unavoidable, anchor it tightly and document the exact source strings.
322
+
323
+ ---
324
+
325
+ ### C11. Fragile magic-string parsing of the setup payload
326
+
327
+ **Location:** `_client.py:305-308`
328
+
329
+ **What is wrong:**
330
+ `setup_interposer_board` strips a literal `"Data: "` prefix then `json.loads`.
331
+
332
+ ```python
333
+ # _client.py:305-308
334
+ if raw_data.startswith("Data: "):
335
+ raw_data = raw_data[len("Data: ") :]
336
+ data = json.loads(raw_data)
337
+ ```
338
+
339
+ **Why it is critical:**
340
+ If the server omits/renames the prefix or returns non-JSON on failure, `json.loads` throws, which C6 swallows → `None`. The undocumented magic-string protocol makes the parse brittle and the failure invisible.
341
+
342
+ **Suggested fix:**
343
+ Wrap the parse defensively and handle malformed payloads explicitly:
344
+
345
+ ```python
346
+ try:
347
+ data = json.loads(raw_data)
348
+ except json.JSONDecodeError:
349
+ return "Setup InterposerBoard Failed: unrecognized server response."
350
+ ```
351
+
352
+ Better: agree a structured response field with the server rather than a prefixed string.
353
+
354
+ ---
355
+
356
+ ### C12. `get_paths` opens config without an explicit encoding
357
+
358
+ **Location:** `_utils.py:58`
359
+
360
+ **What is wrong:**
361
+
362
+ ```python
363
+ # _utils.py:58
364
+ with open(json_path, "r") as f: # no encoding
365
+ data = json.load(f)
366
+ ```
367
+
368
+ **Why it is critical (portability):**
369
+ Uses the platform default encoding, which differs across OSes/locales and can corrupt non-ASCII paths. `_client.py:45` correctly uses `encoding="utf-8"`; this one is inconsistent and non-portable.
370
+
371
+ **Suggested fix:**
372
+
373
+ ```python
374
+ with open(json_path, "r", encoding="utf-8") as f:
375
+ data = json.load(f)
376
+ ```
377
+
378
+ ---
379
+
380
+ ### C13. `Utils.get_paths` documents `Raises` but is decorated to never raise → constructor `TypeError`
381
+
382
+ **Location:** `_utils.py:47-49` (fallout at `_client.py:44-45`)
383
+
384
+ **What is wrong:**
385
+ The docstring promises `FileNotFoundError` / `JSONDecodeError`, but `@handle_exceptions` converts both to `None`. That `None` then flows into the constructor:
386
+
387
+ ```python
388
+ # _client.py:44-45
389
+ self.__paths = Utils.get_paths() # may be None (C6)
390
+ with open(self.__paths["ApplicationSettingsPath"], ...) # None[...] -> TypeError
391
+ ```
392
+
393
+ **Why it is critical:**
394
+ On any config problem, `get_paths` returns `None` instead of raising, and `self.__paths["..."]` raises `TypeError: 'NoneType' object is not subscriptable`. The constructor has no defined failure mode, and the actual error (missing/invalid config) is obscured behind an unrelated `TypeError`.
395
+
396
+ **Suggested fix:**
397
+ Remove `@handle_exceptions` from `get_paths` (let config errors propagate as documented), or validate the result in `__init__`:
398
+
399
+ ```python
400
+ self.__paths = Utils.get_paths()
401
+ if not self.__paths:
402
+ raise RuntimeError("Failed to load paths.json configuration.")
403
+ ```
404
+
405
+ ---
406
+
407
+ ## 🟡 Medium — Coding standards / consistency
408
+
409
+ ---
410
+
411
+ ### S1. Inconsistent parameter naming between facade and client
412
+
413
+ **Location:** `protocol_validation_suite_apis.py:66` vs `_client.py:265`, `_client.py:381`, `_client.py:402`
414
+
415
+ The public facade uses `request`, while the client uses `setup_interposer_board_request` / `compile_sequence_details` / `run_sequence_details`.
416
+
417
+ **Why it matters:** Inconsistent naming for the same concept across the call chain hurts readability and maintainability.
418
+
419
+ **Suggested fix:** Pick one convention (e.g. `request`) and apply it in both layers.
420
+
421
+ ---
422
+
423
+ ### S2. Missing / inconsistent return type hints
424
+
425
+ **Location:** `protocol_validation_suite_apis.py` (`launch_server`, `set_profile`, `setup_interposer_board`, `compile_sequence`, `run_sequence`, `i3s_monitor_setup`); `_client.py:381` (`compile_sequence` documented `tuple[bool, str]` but unannotated); `set_profile` unannotated while `load_project` is annotated.
426
+
427
+ **Why it matters:** Documented return types without annotations defeat static analysis and IDE support; the codebase is uneven.
428
+
429
+ **Suggested fix:** Add explicit return annotations matching the docstrings (e.g. `-> tuple[bool, str]`, `-> bool`, `-> str`).
430
+
431
+ ---
432
+
433
+ ### S3. `__init__.py` public surface is incomplete
434
+
435
+ **Location:** `__init__.py:2-8`
436
+
437
+ `SetupInterposerBoardRequest` and `ResetInterposerBoardRequest` are required arguments of public methods but are **not** exported. `ProtocolDebugResponse` is also unexported.
438
+
439
+ **Why it matters:** Users must import from `.models` (a private-looking module) to call public methods — the advertised API is not self-contained.
440
+
441
+ **Suggested fix:** Add the request models to the imports and `__all__`:
442
+
443
+ ```python
444
+ from .models import (
445
+ CompileSequenceRequest, RunSequenceRequest, I3SMonitorSetupRequest,
446
+ SetupInterposerBoardRequest, ResetInterposerBoardRequest,
447
+ )
448
+ __all__ = [
449
+ "ProtocolValidationSuiteAPIs",
450
+ "CompileSequenceRequest", "RunSequenceRequest", "I3SMonitorSetupRequest",
451
+ "SetupInterposerBoardRequest", "ResetInterposerBoardRequest",
452
+ ]
453
+ ```
454
+
455
+ ---
456
+
457
+ ### S4. Dead / unreachable capability: `_call_protocol_debug` and `ProtocolDebugResponse`
458
+
459
+ **Location:** `_client.py:536`, `models.py:89`
460
+
461
+ `_call_protocol_debug` is private and has **no** facade method in `ProtocolValidationSuiteAPIs`, so it is unreachable through the public API. `ProtocolDebugResponse` exists only to serve it.
462
+
463
+ **Why it matters:** Either incomplete work or dead code; both should be resolved.
464
+
465
+ **Suggested fix:** Expose a public `protocol_debug(...)` method on the facade if intended, or remove `_call_protocol_debug` and `ProtocolDebugResponse` if not.
466
+
467
+ ---
468
+
469
+ ### S5. Local variable shadowing / reassigning `request` to a different message type
470
+
471
+ **Location:** `_client.py:282-300`, `_client.py:415-425`
472
+
473
+ `request` is built as one message type, used, then rebound to a different message type in the same scope (`Setup_Configurations` → `Request`; `Compile_Details` → `Run_Details`).
474
+
475
+ **Why it matters:** Reusing one name for two distinct types hurts readability and defeats type inference.
476
+
477
+ **Suggested fix:** Use distinct names, e.g. `setup_request` / `channel_request`, and `compile_request` / `run_request`.
478
+
479
+ ---
480
+
481
+ ### S6. Unused instance attribute `self._error_stream`
482
+
483
+ **Location:** `_client.py:79`
484
+
485
+ ```python
486
+ self._error_stream = subprocess.PIPE # assigned, never read
487
+ ```
488
+
489
+ `Popen` uses the literal `subprocess.PIPE` on the next line.
490
+
491
+ **Suggested fix:** Remove the attribute.
492
+
493
+ ---
494
+
495
+ ### S7. `response` assigned but unused; channel-update success ignored
496
+
497
+ **Location:** `_client.py:205`, `_client.py:300`, `_client.py:352`
498
+
499
+ `Close_Server` and both `Update_*_Channels` calls capture a return value that is never inspected. Notably the channel-update `Success` is ignored, so a failed channel update still reports `"Successful."`.
500
+
501
+ **Suggested fix:** Either check the returned `.Success` / `.Response_Data` and reflect it in the result, or drop the unused assignment.
502
+
503
+ ---
504
+
505
+ ### S8. `launch_server` returns `bool` but `close_server` returns `None` — asymmetric
506
+
507
+ **Location:** `protocol_validation_suite_apis.py:28-38`
508
+
509
+ `launch_server` forwards the client's bool (with no annotation); `close_server` drops the return.
510
+
511
+ **Suggested fix:** Make the pair symmetric — return a status from `close_server` too, and annotate both.
512
+
513
+ ---
514
+
515
+ ### S9. Imports not grouped per PEP 8 / split model import
516
+
517
+ **Location:** `_client.py:7-8` (third-party `grpc` interleaved with local `.stubs`); `protocol_validation_suite_apis.py:4-10` (`.models` imported in two statements); `_utils.py:1-3` (`os, json, re` not alphabetical).
518
+
519
+ **Suggested fix:** Group imports as stdlib / third-party / local with blank lines between groups; consolidate the split `.models` import.
520
+
521
+ ---
522
+
523
+ ### S10. Inline/local imports instead of top-of-module
524
+
525
+ **Location:** `_client.py:200-202`
526
+
527
+ `Trigger` is imported inside `close_server` "to avoid circular imports," but `GRPC_pb2` is already imported at module top and used as `GRPC_pb2.Trigger` elsewhere (e.g. `_client.py:439`). The justification is false. Similarly `Debug_Configurations` is imported at top (`_client.py:20`) while every other message is namespaced via `GRPC_pb2.` — mixed styles.
528
+
529
+ **Suggested fix:** Remove the local import and use `GRPC_pb2.Trigger`; standardize on the `GRPC_pb2.` namespace everywhere (drop the direct `Debug_Configurations` import).
530
+
531
+ ---
532
+
533
+ ### S11. `i3s_monitor_setup` unreachable `else` branch / duplicated body
534
+
535
+ **Location:** `_client.py:588-590`
536
+
537
+ `configurations` is typed `dict[str, Any] | str`; the `dict` and `str` branches are exhaustive. The "best-effort fallback" `else` is only reachable on a type-contract violation and duplicates the `dict` branch anyway.
538
+
539
+ **Suggested fix:** Reduce to the two valid branches (`isinstance(..., str)` → passthrough; else `json.dumps`), or validate input and raise on unsupported types.
540
+
541
+ ---
542
+
543
+ ### S12. `handle_exceptions` name resolution is brittle and redundant
544
+
545
+ **Location:** `_decorators.py:30-32`
546
+
547
+ ```python
548
+ func_name = getattr(func, "__name__", None) or getattr(func, "__func__", None).__name__
549
+ ```
550
+
551
+ If `func` has neither `__name__` nor `__func__`, the second `getattr` returns `None` and `.__name__` raises `AttributeError` at decoration time. Also, `functools.wraps` already provides the name.
552
+
553
+ **Suggested fix:** Use `func.__name__` after `functools.wraps`, or read the name inside `wrapper` via `wrapper.__name__`; drop the fragile fallback chain.
554
+
555
+ ---
556
+
557
+ ### S13. `set_test_condition` redundant assignment
558
+
559
+ **Location:** `_client.py:373`
560
+
561
+ ```python
562
+ file_path = test_file_path # pointless alias
563
+ ```
564
+
565
+ **Suggested fix:** Use `test_file_path` directly and remove the alias.
566
+
567
+ ---
568
+
569
+ ### S14. `set_test_condition` always returns `True`
570
+
571
+ **Location:** `_client.py:378`
572
+
573
+ Returns hard-coded `True`; the only non-`True` outcome is an exception → `None` (via C6). The documented `bool` is really `True | None`.
574
+
575
+ **Suggested fix:** Either let it raise on failure (return type becomes meaningful only if there is a real `False` path), or document that success is `True` and failure raises.
576
+
577
+ ---
578
+
579
+ ### S15. Facade docstring for `reset_interposer_board` names the wrong type
580
+
581
+ **Location:** `protocol_validation_suite_apis.py:85`
582
+
583
+ Arg documented as `SetupInterposerBoardRequest` but the parameter is `ResetInterposerBoardRequest`.
584
+
585
+ **Suggested fix:** Correct the documented type to `ResetInterposerBoardRequest`.
586
+
587
+ ---
588
+
589
+ ### S16. `mass_compile_sequence` param name mismatch with docstring
590
+
591
+ **Location:** `protocol_validation_suite_apis.py:141-147`
592
+
593
+ Parameter is `requests`; the docstring `Args` calls it `request`.
594
+
595
+ **Suggested fix:** Rename the docstring arg to `requests`.
596
+
597
+ ---
598
+
599
+ ### S17. Blank-line convention inside `models.py`
600
+
601
+ **Location:** `models.py:88-89`
602
+
603
+ Only one blank line between `I3SMonitorSetupRequest` and `ProtocolDebugResponse`; every other top-level class uses two (PEP 8).
604
+
605
+ **Suggested fix:** Add the second blank line before `ProtocolDebugResponse`.
606
+
607
+ ---
608
+
609
+ ## Recommended fix order
610
+
611
+ 1. **C6** — stop the blanket swallow; it is hiding every other bug.
612
+ 2. **C1** — fix the `list[int]` → `repeated string` serialization crash.
613
+ 3. **C2** — return `report_details.Report_Folder_Path` (honor the `str` contract).
614
+ 4. **C3** — give `run_sequence` a structured, machine-checkable result.
615
+ 5. **C7, C13** — harden the launch/close/config lifecycle against `None`/uninitialized state.
616
+ 6. Remaining High (C8–C12), then Medium (S1–S17).
@@ -0,0 +1,9 @@
1
+ from .protocol_validation_suite_apis import ProtocolValidationSuiteAPIs
2
+ from .models import CompileSequenceRequest, RunSequenceRequest, I3SMonitorSetupRequest
3
+
4
+ __all__ = [
5
+ "ProtocolValidationSuiteAPIs",
6
+ "CompileSequenceRequest",
7
+ "RunSequenceRequest",
8
+ "I3SMonitorSetupRequest",
9
+ ]