lr-qrm 0.1.1__py3-none-any.whl → 0.1.3__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.
- {lr_qrm-0.1.1.dist-info → lr_qrm-0.1.3.dist-info}/METADATA +23 -8
- lr_qrm-0.1.3.dist-info/RECORD +10 -0
- qrm/cli.py +146 -17
- qrm/client.py +190 -7
- lr_qrm-0.1.1.dist-info/RECORD +0 -10
- {lr_qrm-0.1.1.dist-info → lr_qrm-0.1.3.dist-info}/WHEEL +0 -0
- {lr_qrm-0.1.1.dist-info → lr_qrm-0.1.3.dist-info}/entry_points.txt +0 -0
- {lr_qrm-0.1.1.dist-info → lr_qrm-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {lr_qrm-0.1.1.dist-info → lr_qrm-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lr-qrm
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CLI client for interacting with Qestit QRM data from LumenRadio tooling.
|
|
5
5
|
Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
|
|
6
6
|
License: MIT License
|
|
@@ -66,10 +66,18 @@ qrm status
|
|
|
66
66
|
#### Inspect UUT history
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
qrm uut status 326115020010F2F1 --start "2026-02-
|
|
69
|
+
qrm uut status 326115020010F2F1 --start "2026-02-01T00:00:00Z" --stop "2026-02-06T23:59:00Z"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
By default the command
|
|
72
|
+
By default the command uses `1970-01-01T00:00:00Z` as the start timestamp and lets QRM treat the stop as "now" (omit `--stop`). Pass `--base-url` and `--insecure` to override the stored settings when needed.
|
|
73
|
+
|
|
74
|
+
#### List production boxes
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
qrm box list --stop "2026-02-06T23:59:00Z"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If you omit `--start`, the command defaults to the Unix epoch (`1970-01-01T00:00:00Z`). Leave `--stop` out to let QRM use its current time.
|
|
73
81
|
|
|
74
82
|
#### JSON output
|
|
75
83
|
|
|
@@ -87,12 +95,19 @@ from qrm.client import QrmClient
|
|
|
87
95
|
|
|
88
96
|
state = load_login_state()
|
|
89
97
|
client = QrmClient(base_url=str(state.base_url), verify_tls=state.verify_tls)
|
|
98
|
+
|
|
90
99
|
uut_runs = client.uut_status(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
token=state.token,
|
|
101
|
+
serial_number="326115020010F2F1",
|
|
102
|
+
start_datetime="2026-02-01T00:00:00Z",
|
|
103
|
+
stop_datetime="2026-02-06T23:59:00Z",
|
|
104
|
+
max_results=1000,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
boxes = client.box_list(
|
|
108
|
+
token=state.token,
|
|
109
|
+
start_datetime="1970-01-01T00:00:00Z",
|
|
110
|
+
stop_datetime="2026-02-06T23:59:00Z",
|
|
96
111
|
)
|
|
97
112
|
```
|
|
98
113
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
lr_qrm-0.1.3.dist-info/licenses/LICENSE,sha256=cEnG3HeMPa3GKtuENkytOQaNJS9rwMMU_KWHObdWbOY,1070
|
|
2
|
+
qrm/__init__.py,sha256=vpsqqB9AKEhCcTdlSNeMF74vKSUG2NuUPEd5uE6u-ws,1069
|
|
3
|
+
qrm/cli.py,sha256=djqm4xNwPKk82mZPXHsuELe7gQDRzLvEePHh83L2GUs,14716
|
|
4
|
+
qrm/client.py,sha256=8i2IJx1D8S2g1BBReH_7rr0Lq4YKbpKtuVljm_4rqig,11431
|
|
5
|
+
qrm/config.py,sha256=TewatdLxqbCa7hBLbqj8hrKbposvbD-Wc1TSiUipzq4,623
|
|
6
|
+
lr_qrm-0.1.3.dist-info/METADATA,sha256=8jM8UVe4lYKuxsBV6Z1WHu-iNKRvoNOXk57gU2IALYk,2753
|
|
7
|
+
lr_qrm-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
lr_qrm-0.1.3.dist-info/entry_points.txt,sha256=9CaqYoIqOC9O40WoJ6SM1qYk9gP5VVShmMc51zBqas4,36
|
|
9
|
+
lr_qrm-0.1.3.dist-info/top_level.txt,sha256=URCUvNbN-7KWypbbmrCaie8JjKDowz0p4GNhBqnN99U,4
|
|
10
|
+
lr_qrm-0.1.3.dist-info/RECORD,,
|
qrm/cli.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from datetime import datetime,
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
@@ -28,15 +28,18 @@ class OutputFormat(str, Enum):
|
|
|
28
28
|
app = typer.Typer(help="Interact with the Qestit QRM service.")
|
|
29
29
|
console = Console()
|
|
30
30
|
uut_app = typer.Typer(help="Inspect Units Under Test (UUTs).")
|
|
31
|
+
box_app = typer.Typer(help="Inspect production boxes.")
|
|
31
32
|
app.add_typer(uut_app, name="uut")
|
|
33
|
+
app.add_typer(box_app, name="box")
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
API_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
36
|
+
CLI_DATE_FORMATS = [
|
|
37
|
+
API_DATE_FORMAT,
|
|
38
|
+
"%Y-%m-%d %H:%M:%S",
|
|
36
39
|
"%Y-%m-%dT%H:%M:%S",
|
|
37
40
|
"%Y-%m-%dT%H:%M:%S%z",
|
|
38
41
|
]
|
|
39
|
-
|
|
42
|
+
EPOCH_START = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
@app.callback(invoke_without_command=True)
|
|
@@ -166,14 +169,20 @@ def uut_status(
|
|
|
166
169
|
start: datetime | None = typer.Option(
|
|
167
170
|
None,
|
|
168
171
|
"--start",
|
|
169
|
-
formats=
|
|
170
|
-
help=
|
|
172
|
+
formats=CLI_DATE_FORMATS,
|
|
173
|
+
help=(
|
|
174
|
+
"Filter from this timestamp (UTC). Accepts ISO-8601 (e.g. 2020-01-01T00:00:00Z) "
|
|
175
|
+
"or YYYY-MM-DD HH:MM:SS. Default: 1970-01-01T00:00:00Z."
|
|
176
|
+
),
|
|
171
177
|
),
|
|
172
178
|
stop: datetime | None = typer.Option(
|
|
173
179
|
None,
|
|
174
180
|
"--stop",
|
|
175
|
-
formats=
|
|
176
|
-
help=
|
|
181
|
+
formats=CLI_DATE_FORMATS,
|
|
182
|
+
help=(
|
|
183
|
+
"Filter until this timestamp (UTC). Accepts ISO-8601 (e.g. 2026-02-06T23:59:00Z) "
|
|
184
|
+
"or YYYY-MM-DD HH:MM:SS. Default: QRM current time."
|
|
185
|
+
),
|
|
177
186
|
),
|
|
178
187
|
max_results: int = typer.Option(
|
|
179
188
|
1000,
|
|
@@ -209,7 +218,7 @@ def uut_status(
|
|
|
209
218
|
"""Fetch UUT execution history using the `/Uut/Status` endpoint."""
|
|
210
219
|
|
|
211
220
|
state = _load_state_or_exit(config_path)
|
|
212
|
-
|
|
221
|
+
resolved_start, resolved_stop = _resolve_uut_window(start, stop)
|
|
213
222
|
|
|
214
223
|
client = QrmClient(
|
|
215
224
|
base_url=base_url or str(state.base_url),
|
|
@@ -220,8 +229,8 @@ def uut_status(
|
|
|
220
229
|
payload = client.uut_status(
|
|
221
230
|
token=state.token,
|
|
222
231
|
serial_number=serial_number,
|
|
223
|
-
start_datetime=
|
|
224
|
-
stop_datetime=
|
|
232
|
+
start_datetime=_format_api_datetime(resolved_start),
|
|
233
|
+
stop_datetime=_format_api_datetime(resolved_stop) if resolved_stop is not None else None,
|
|
225
234
|
max_results=max_results,
|
|
226
235
|
)
|
|
227
236
|
except QrmClientError as exc:
|
|
@@ -235,6 +244,76 @@ def uut_status(
|
|
|
235
244
|
console.print(_render_uut_status(payload))
|
|
236
245
|
|
|
237
246
|
|
|
247
|
+
@box_app.command("list")
|
|
248
|
+
def box_list(
|
|
249
|
+
start: datetime | None = typer.Option(
|
|
250
|
+
None,
|
|
251
|
+
"--start",
|
|
252
|
+
formats=CLI_DATE_FORMATS,
|
|
253
|
+
help=(
|
|
254
|
+
"Filter from this timestamp (UTC). Accepts ISO-8601 (e.g. 2020-01-01T00:00:00Z) "
|
|
255
|
+
"or YYYY-MM-DD HH:MM:SS. Default: 1970-01-01T00:00:00Z."
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
stop: datetime | None = typer.Option(
|
|
259
|
+
None,
|
|
260
|
+
"--stop",
|
|
261
|
+
formats=CLI_DATE_FORMATS,
|
|
262
|
+
help=(
|
|
263
|
+
"Filter until this timestamp (UTC). Accepts ISO-8601 (e.g. 2026-02-06T23:59:00Z) "
|
|
264
|
+
"or YYYY-MM-DD HH:MM:SS. Default: QRM current time."
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
config_path: Path = typer.Option(
|
|
268
|
+
None,
|
|
269
|
+
"--config-path",
|
|
270
|
+
help="Override the location of the login.json file.",
|
|
271
|
+
),
|
|
272
|
+
base_url: str = typer.Option(
|
|
273
|
+
None,
|
|
274
|
+
"--base-url",
|
|
275
|
+
help="Override the base URL stored in the config (useful for testing).",
|
|
276
|
+
),
|
|
277
|
+
insecure: bool = typer.Option(
|
|
278
|
+
False,
|
|
279
|
+
"--insecure",
|
|
280
|
+
help="Disable TLS verification when contacting QRM.",
|
|
281
|
+
),
|
|
282
|
+
output: OutputFormat = typer.Option(
|
|
283
|
+
OutputFormat.rich,
|
|
284
|
+
"--output",
|
|
285
|
+
"-o",
|
|
286
|
+
case_sensitive=False,
|
|
287
|
+
help="Choose output format: rich (default) or json.",
|
|
288
|
+
),
|
|
289
|
+
) -> None:
|
|
290
|
+
"""List production boxes via the `/Box/List` endpoint."""
|
|
291
|
+
|
|
292
|
+
state = _load_state_or_exit(config_path)
|
|
293
|
+
resolved_start, resolved_stop = _resolve_box_window(start, stop)
|
|
294
|
+
|
|
295
|
+
client = QrmClient(
|
|
296
|
+
base_url=base_url or str(state.base_url),
|
|
297
|
+
verify_tls=not insecure and state.verify_tls,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
payload = client.box_list(
|
|
302
|
+
token=state.token,
|
|
303
|
+
start_datetime=_format_api_datetime(resolved_start),
|
|
304
|
+
stop_datetime=_format_api_datetime(resolved_stop) if resolved_stop is not None else None,
|
|
305
|
+
)
|
|
306
|
+
except QrmClientError as exc:
|
|
307
|
+
typer.secho(f"Box list failed: {exc}", fg=typer.colors.RED)
|
|
308
|
+
raise typer.Exit(code=1) from exc
|
|
309
|
+
|
|
310
|
+
if output is OutputFormat.json:
|
|
311
|
+
_emit(payload, output)
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
console.print(_render_box_list(payload))
|
|
315
|
+
|
|
316
|
+
|
|
238
317
|
def _resolve_config_path(config_path: Path | None) -> Path:
|
|
239
318
|
return config_path.expanduser() if config_path else DEFAULT_CONFIG_PATH
|
|
240
319
|
|
|
@@ -278,17 +357,35 @@ def _resolve_uut_window(
|
|
|
278
357
|
start: datetime | None,
|
|
279
358
|
stop: datetime | None,
|
|
280
359
|
) -> tuple[datetime, datetime]:
|
|
281
|
-
|
|
282
|
-
|
|
360
|
+
return _resolve_window(start, stop, default_start=EPOCH_START)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _resolve_box_window(
|
|
364
|
+
start: datetime | None,
|
|
365
|
+
stop: datetime | None,
|
|
366
|
+
) -> tuple[datetime, datetime]:
|
|
367
|
+
return _resolve_window(start, stop, default_start=EPOCH_START)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _resolve_window(
|
|
371
|
+
start: datetime | None,
|
|
372
|
+
stop: datetime | None,
|
|
373
|
+
*,
|
|
374
|
+
default_start: datetime,
|
|
375
|
+
) -> tuple[datetime, datetime | None]:
|
|
376
|
+
stop_value = stop
|
|
377
|
+
start_value = start or default_start
|
|
378
|
+
if stop_value and start_value > stop_value:
|
|
379
|
+
raise typer.BadParameter("--start must be earlier than --stop")
|
|
283
380
|
return start_value, stop_value
|
|
284
381
|
|
|
285
382
|
|
|
286
|
-
def
|
|
383
|
+
def _format_api_datetime(value: datetime | None) -> str | None:
|
|
287
384
|
if value is None:
|
|
288
385
|
return None
|
|
289
386
|
if value.tzinfo is None:
|
|
290
|
-
|
|
291
|
-
return value.astimezone(timezone.utc).strftime(
|
|
387
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
388
|
+
return value.astimezone(timezone.utc).strftime(API_DATE_FORMAT)
|
|
292
389
|
|
|
293
390
|
|
|
294
391
|
def _render_uut_status(payload: Any) -> Table:
|
|
@@ -326,6 +423,38 @@ def _render_uut_status(payload: Any) -> Table:
|
|
|
326
423
|
return table
|
|
327
424
|
|
|
328
425
|
|
|
426
|
+
def _render_box_list(payload: Any) -> Table:
|
|
427
|
+
table = Table(show_header=True, header_style="bold")
|
|
428
|
+
table.add_column("Box", style="cyan")
|
|
429
|
+
table.add_column("Name", style="cyan")
|
|
430
|
+
table.add_column("Status", style="green")
|
|
431
|
+
table.add_column("Start", style="magenta")
|
|
432
|
+
table.add_column("Stop", style="magenta")
|
|
433
|
+
table.add_column("Location", style="yellow")
|
|
434
|
+
|
|
435
|
+
rows = payload if isinstance(payload, list) else ([payload] if payload else [])
|
|
436
|
+
|
|
437
|
+
if not rows:
|
|
438
|
+
table.add_row("No results", "-", "-", "-", "-", "-")
|
|
439
|
+
return table
|
|
440
|
+
|
|
441
|
+
for entry in rows:
|
|
442
|
+
if not isinstance(entry, dict):
|
|
443
|
+
table.add_row(str(entry), "-", "-", "-", "-", "-")
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
table.add_row(
|
|
447
|
+
str(entry.get("BoxId") or entry.get("Id", "-")),
|
|
448
|
+
str(entry.get("BoxName") or entry.get("Name", "-")),
|
|
449
|
+
str(entry.get("Status") or entry.get("Outcome", "-")),
|
|
450
|
+
_format_timestamp(entry.get("StartDateTime")),
|
|
451
|
+
_format_timestamp(entry.get("StopDateTime")),
|
|
452
|
+
str(entry.get("Location") or entry.get("StationName") or "-"),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return table
|
|
456
|
+
|
|
457
|
+
|
|
329
458
|
def _resolve_serial(entry: dict[str, Any]) -> str:
|
|
330
459
|
value = entry.get("serialNumber") or entry.get("SerialNumber")
|
|
331
460
|
return str(value) if value else "-"
|
qrm/client.py
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
7
|
+
import pprint
|
|
8
|
+
import sys
|
|
6
9
|
from dataclasses import dataclass
|
|
7
10
|
from datetime import datetime, timezone
|
|
8
11
|
from typing import Any, Dict, Optional
|
|
@@ -42,8 +45,10 @@ class QrmClient:
|
|
|
42
45
|
verify_tls: bool = True
|
|
43
46
|
timeout: float = 10.0
|
|
44
47
|
session: Optional[requests.Session] = None
|
|
48
|
+
debug: bool = False
|
|
45
49
|
|
|
46
50
|
def __post_init__(self) -> None:
|
|
51
|
+
self.debug = self.debug or os.getenv("QRM_DEBUG") == "1"
|
|
47
52
|
if not self.base_url:
|
|
48
53
|
raise ValueError("base_url must be provided")
|
|
49
54
|
self.base_url = self.base_url.rstrip("/")
|
|
@@ -56,16 +61,32 @@ class QrmClient:
|
|
|
56
61
|
"""Authenticate against `/User/Login` and return the bearer token."""
|
|
57
62
|
|
|
58
63
|
credentials = LoginCredentials(username=username, password=password)
|
|
64
|
+
url = self._build_url("/User/Login")
|
|
65
|
+
body = credentials.model_dump()
|
|
66
|
+
self._debug_request(
|
|
67
|
+
method="POST",
|
|
68
|
+
url=url,
|
|
69
|
+
headers=None,
|
|
70
|
+
payload=body,
|
|
71
|
+
)
|
|
59
72
|
try:
|
|
60
73
|
response = self.session.post(
|
|
61
|
-
|
|
62
|
-
json=
|
|
74
|
+
url,
|
|
75
|
+
json=body,
|
|
63
76
|
timeout=self.timeout,
|
|
64
77
|
)
|
|
65
|
-
|
|
78
|
+
self._debug_response(method="POST", url=url, response=response)
|
|
66
79
|
except requests.RequestException as exc: # pragma: no cover - network failure
|
|
80
|
+
self._debug_error(method="POST", url=url, error=exc)
|
|
67
81
|
raise QrmClientError("Failed to contact QRM login endpoint") from exc
|
|
68
82
|
|
|
83
|
+
self._ensure_success(
|
|
84
|
+
response=response,
|
|
85
|
+
method="POST",
|
|
86
|
+
url=url,
|
|
87
|
+
error_message="Failed to contact QRM login endpoint",
|
|
88
|
+
)
|
|
89
|
+
|
|
69
90
|
token = self._extract_token(response)
|
|
70
91
|
if not token:
|
|
71
92
|
raise QrmClientError("QRM login response did not contain a token")
|
|
@@ -75,16 +96,31 @@ class QrmClient:
|
|
|
75
96
|
"""Query the `/Result/Status` endpoint to verify service health."""
|
|
76
97
|
|
|
77
98
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
99
|
+
url = self._build_url("/Result/Status")
|
|
100
|
+
self._debug_request(
|
|
101
|
+
method="GET",
|
|
102
|
+
url=url,
|
|
103
|
+
headers=headers,
|
|
104
|
+
payload=None,
|
|
105
|
+
)
|
|
78
106
|
try:
|
|
79
107
|
response = self.session.get(
|
|
80
|
-
|
|
108
|
+
url,
|
|
81
109
|
headers=headers,
|
|
82
110
|
timeout=self.timeout,
|
|
83
111
|
)
|
|
84
|
-
|
|
112
|
+
self._debug_response(method="GET", url=url, response=response)
|
|
85
113
|
except requests.RequestException as exc: # pragma: no cover - network failure
|
|
114
|
+
self._debug_error(method="GET", url=url, error=exc)
|
|
86
115
|
raise QrmClientError("Failed to contact QRM status endpoint") from exc
|
|
87
116
|
|
|
117
|
+
self._ensure_success(
|
|
118
|
+
response=response,
|
|
119
|
+
method="GET",
|
|
120
|
+
url=url,
|
|
121
|
+
error_message="Failed to contact QRM status endpoint",
|
|
122
|
+
)
|
|
123
|
+
|
|
88
124
|
try:
|
|
89
125
|
return response.json()
|
|
90
126
|
except ValueError:
|
|
@@ -114,22 +150,114 @@ class QrmClient:
|
|
|
114
150
|
if max_results is not None:
|
|
115
151
|
payload["maxNumberOfResults"] = max_results
|
|
116
152
|
|
|
153
|
+
url = self._build_url("/Uut/Status")
|
|
154
|
+
self._debug_request(
|
|
155
|
+
method="POST",
|
|
156
|
+
url=url,
|
|
157
|
+
headers=headers,
|
|
158
|
+
payload=payload,
|
|
159
|
+
)
|
|
117
160
|
try:
|
|
118
161
|
response = self.session.post(
|
|
119
|
-
|
|
162
|
+
url,
|
|
120
163
|
headers=headers,
|
|
121
164
|
json=payload,
|
|
122
165
|
timeout=self.timeout,
|
|
123
166
|
)
|
|
124
|
-
|
|
167
|
+
self._debug_response(method="POST", url=url, response=response)
|
|
125
168
|
except requests.RequestException as exc: # pragma: no cover - network failure
|
|
169
|
+
self._debug_error(method="POST", url=url, error=exc)
|
|
126
170
|
raise QrmClientError("Failed to contact QRM UUT status endpoint") from exc
|
|
127
171
|
|
|
172
|
+
self._ensure_success(
|
|
173
|
+
response=response,
|
|
174
|
+
method="POST",
|
|
175
|
+
url=url,
|
|
176
|
+
error_message="Failed to contact QRM UUT status endpoint",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
return response.json()
|
|
181
|
+
except ValueError:
|
|
182
|
+
return response.text.strip()
|
|
183
|
+
|
|
184
|
+
def box_list(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
token: str,
|
|
188
|
+
start_datetime: Optional[str] = None,
|
|
189
|
+
stop_datetime: Optional[str] = None,
|
|
190
|
+
) -> Any:
|
|
191
|
+
"""Query the `/Box/List` endpoint for production boxes."""
|
|
192
|
+
|
|
193
|
+
if not token:
|
|
194
|
+
raise ValueError("token must be provided")
|
|
195
|
+
|
|
196
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
197
|
+
payload: Dict[str, Any] = {}
|
|
198
|
+
if start_datetime:
|
|
199
|
+
payload["startDateTime"] = start_datetime
|
|
200
|
+
if stop_datetime:
|
|
201
|
+
payload["stopDateTime"] = stop_datetime
|
|
202
|
+
|
|
203
|
+
body = payload or None
|
|
204
|
+
url = self._build_url("/Box/List")
|
|
205
|
+
self._debug_request(
|
|
206
|
+
method="POST",
|
|
207
|
+
url=url,
|
|
208
|
+
headers=headers,
|
|
209
|
+
payload=body,
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
response = self.session.post(
|
|
213
|
+
url,
|
|
214
|
+
headers=headers,
|
|
215
|
+
json=body,
|
|
216
|
+
timeout=self.timeout,
|
|
217
|
+
)
|
|
218
|
+
self._debug_response(method="POST", url=url, response=response)
|
|
219
|
+
except requests.RequestException as exc: # pragma: no cover - network failure
|
|
220
|
+
self._debug_error(method="POST", url=url, error=exc)
|
|
221
|
+
raise QrmClientError("Failed to contact QRM box list endpoint") from exc
|
|
222
|
+
|
|
223
|
+
self._ensure_success(
|
|
224
|
+
response=response,
|
|
225
|
+
method="POST",
|
|
226
|
+
url=url,
|
|
227
|
+
error_message="Failed to contact QRM box list endpoint",
|
|
228
|
+
)
|
|
229
|
+
|
|
128
230
|
try:
|
|
129
231
|
return response.json()
|
|
130
232
|
except ValueError:
|
|
131
233
|
return response.text.strip()
|
|
132
234
|
|
|
235
|
+
def _ensure_success(
|
|
236
|
+
self,
|
|
237
|
+
*,
|
|
238
|
+
response: requests.Response,
|
|
239
|
+
method: str,
|
|
240
|
+
url: str,
|
|
241
|
+
error_message: str,
|
|
242
|
+
) -> None:
|
|
243
|
+
if response.status_code == 401:
|
|
244
|
+
message = (
|
|
245
|
+
"Authentication failed (HTTP 401). Your credentials may be invalid or expired. "
|
|
246
|
+
"Please run `qrm login` and try again."
|
|
247
|
+
)
|
|
248
|
+
self._debug_error(
|
|
249
|
+
method=method,
|
|
250
|
+
url=url,
|
|
251
|
+
error=RuntimeError(message),
|
|
252
|
+
)
|
|
253
|
+
raise QrmClientError(message)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
except requests.HTTPError as exc:
|
|
258
|
+
self._debug_error(method=method, url=url, error=exc)
|
|
259
|
+
raise QrmClientError(error_message) from exc
|
|
260
|
+
|
|
133
261
|
def _build_url(self, path: str) -> str:
|
|
134
262
|
return urljoin(f"{self.base_url}/", path.lstrip("/"))
|
|
135
263
|
|
|
@@ -179,5 +307,60 @@ class QrmClient:
|
|
|
179
307
|
return trimmed
|
|
180
308
|
return None
|
|
181
309
|
|
|
310
|
+
def _debug_request(
|
|
311
|
+
self,
|
|
312
|
+
*,
|
|
313
|
+
method: str,
|
|
314
|
+
url: str,
|
|
315
|
+
headers: Optional[Dict[str, Any]],
|
|
316
|
+
payload: Any,
|
|
317
|
+
) -> None:
|
|
318
|
+
if not self.debug:
|
|
319
|
+
return
|
|
320
|
+
data: Dict[str, Any] = {"method": method, "url": url}
|
|
321
|
+
if headers is not None:
|
|
322
|
+
data["headers"] = headers
|
|
323
|
+
if payload is not None:
|
|
324
|
+
data["body"] = payload
|
|
325
|
+
self._debug_http("REQUEST", data)
|
|
326
|
+
|
|
327
|
+
def _debug_response(
|
|
328
|
+
self,
|
|
329
|
+
*,
|
|
330
|
+
method: str,
|
|
331
|
+
url: str,
|
|
332
|
+
response: requests.Response,
|
|
333
|
+
) -> None:
|
|
334
|
+
if not self.debug:
|
|
335
|
+
return
|
|
336
|
+
headers = getattr(response, "headers", None)
|
|
337
|
+
data: Dict[str, Any] = {
|
|
338
|
+
"method": method,
|
|
339
|
+
"url": url,
|
|
340
|
+
"status_code": response.status_code,
|
|
341
|
+
"body": response.text,
|
|
342
|
+
}
|
|
343
|
+
if headers is not None:
|
|
344
|
+
data["headers"] = dict(headers)
|
|
345
|
+
self._debug_http("RESPONSE", data)
|
|
346
|
+
|
|
347
|
+
def _debug_error(self, *, method: str, url: str, error: Exception) -> None:
|
|
348
|
+
if not self.debug:
|
|
349
|
+
return
|
|
350
|
+
self._debug_http(
|
|
351
|
+
"ERROR",
|
|
352
|
+
{
|
|
353
|
+
"method": method,
|
|
354
|
+
"url": url,
|
|
355
|
+
"error": f"{error.__class__.__name__}: {error}",
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _debug_http(self, stage: str, payload: Dict[str, Any]) -> None:
|
|
360
|
+
if not self.debug:
|
|
361
|
+
return
|
|
362
|
+
formatted = pprint.pformat(payload)
|
|
363
|
+
print(f"[QRM DEBUG] {stage}: {formatted}", file=sys.stderr)
|
|
364
|
+
|
|
182
365
|
|
|
183
366
|
__all__ = ["LoginCredentials", "LoginState", "QrmClient", "QrmClientError"]
|
lr_qrm-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
lr_qrm-0.1.1.dist-info/licenses/LICENSE,sha256=cEnG3HeMPa3GKtuENkytOQaNJS9rwMMU_KWHObdWbOY,1070
|
|
2
|
-
qrm/__init__.py,sha256=vpsqqB9AKEhCcTdlSNeMF74vKSUG2NuUPEd5uE6u-ws,1069
|
|
3
|
-
qrm/cli.py,sha256=M3f6D5_ROssysCWsWhbdMgsNJOaDWQZux5u5D0LE6II,10602
|
|
4
|
-
qrm/client.py,sha256=-wA06kxdOx1y4xlPASfwFzltUZaFt4KeDqbgMBY8QCY,5979
|
|
5
|
-
qrm/config.py,sha256=TewatdLxqbCa7hBLbqj8hrKbposvbD-Wc1TSiUipzq4,623
|
|
6
|
-
lr_qrm-0.1.1.dist-info/METADATA,sha256=DaUZm9XUDnxxWReoj9GzpoUcxxYDY-1Zrbln0bGc1xQ,2310
|
|
7
|
-
lr_qrm-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
lr_qrm-0.1.1.dist-info/entry_points.txt,sha256=9CaqYoIqOC9O40WoJ6SM1qYk9gP5VVShmMc51zBqas4,36
|
|
9
|
-
lr_qrm-0.1.1.dist-info/top_level.txt,sha256=URCUvNbN-7KWypbbmrCaie8JjKDowz0p4GNhBqnN99U,4
|
|
10
|
-
lr_qrm-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|