qapu-cli 0.74.0__tar.gz → 0.76.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.
Files changed (46) hide show
  1. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/PKG-INFO +1 -1
  2. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/pyproject.toml +1 -1
  3. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/command_packages.py +12 -0
  4. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/main.py +287 -3
  5. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/PKG-INFO +1 -1
  6. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/README.md +0 -0
  7. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/__init__.py +0 -0
  8. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/client.py +0 -0
  9. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/__init__.py +0 -0
  10. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/app.py +0 -0
  11. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/app.tcss +0 -0
  12. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/data.py +0 -0
  13. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/rendering.py +0 -0
  14. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/__init__.py +0 -0
  15. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/catalog.py +0 -0
  16. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/catalog_form.py +0 -0
  17. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/catalog_picker.py +0 -0
  18. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/confirm.py +0 -0
  19. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/dashboard.py +0 -0
  20. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/device_authorization.py +0 -0
  21. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/device_edit.py +0 -0
  22. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/device_list.py +0 -0
  23. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/equipment_form.py +0 -0
  24. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/group_detail.py +0 -0
  25. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/group_form.py +0 -0
  26. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/group_list.py +0 -0
  27. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/screens/infrastructure.py +0 -0
  28. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/widgets/__init__.py +0 -0
  29. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/tui/widgets/device_table.py +0 -0
  30. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli/visibility.py +0 -0
  31. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/SOURCES.txt +0 -0
  32. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/dependency_links.txt +0 -0
  33. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/entry_points.txt +0 -0
  34. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/requires.txt +0 -0
  35. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/qapu_cli.egg-info/top_level.txt +0 -0
  36. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/setup.cfg +0 -0
  37. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_command_packages_complete.py +0 -0
  38. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_login.py +0 -0
  39. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_logout.py +0 -0
  40. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_authorization.py +0 -0
  41. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_catalog.py +0 -0
  42. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_device_edit.py +0 -0
  43. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_groups.py +0 -0
  44. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_infrastructure.py +0 -0
  45. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_tui_smoke.py +0 -0
  46. {qapu_cli-0.74.0 → qapu_cli-0.76.0}/tests/test_visibility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qapu-cli
3
- Version: 0.74.0
3
+ Version: 0.76.0
4
4
  Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
5
5
  Author: OVOO Technology
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qapu-cli"
3
- version = "0.74.0"
3
+ version = "0.76.0"
4
4
  description = "CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -63,6 +63,17 @@ COMMAND_REQUIRED_PACKAGES: dict[str, frozenset[str]] = {
63
63
  "calibration_get": frozenset({"calibration:read"}),
64
64
  "calibration_list": frozenset({"calibration:read"}),
65
65
  "calibration_update": frozenset({"calibration:write"}),
66
+ "calibration_assignment_add": frozenset({"calibration:write"}),
67
+ "calibration_assignment_delete": frozenset({"calibration:write"}),
68
+ "calibration_assignment_get": frozenset({"calibration:read"}),
69
+ "calibration_assignment_list": frozenset({"calibration:read"}),
70
+ "calibration_assignment_retire": frozenset({"calibration:write"}),
71
+ "calibration_assignment_update": frozenset({"calibration:write"}),
72
+ "calibration_profile_add": frozenset({"calibration:write"}),
73
+ "calibration_profile_delete": frozenset({"calibration:write"}),
74
+ "calibration_profile_get": frozenset({"calibration:read"}),
75
+ "calibration_profile_list": frozenset({"calibration:read"}),
76
+ "calibration_profile_update": frozenset({"calibration:write"}),
66
77
  "cinga_ai_list": frozenset({"mcp:read-only"}),
67
78
  "cinga_ai_log": frozenset({"observability:write"}),
68
79
  "crop_type_add": frozenset({"operations-catalog:write"}),
@@ -90,6 +101,7 @@ COMMAND_REQUIRED_PACKAGES: dict[str, frozenset[str]] = {
90
101
  "device_power": frozenset({"telemetry:read"}),
91
102
  "device_purge_history": frozenset({"devices:destructive"}),
92
103
  "device_reliability": frozenset({"telemetry:read"}),
104
+ "device_windows": frozenset({"telemetry:read"}),
93
105
  "device_trend": frozenset({"telemetry:read"}),
94
106
  "device_voltage": frozenset({"telemetry:read"}),
95
107
  "equipment_add": frozenset({"infrastructure:write"}),
@@ -78,7 +78,11 @@ app.add_typer(equipment_relation_app, name="equipment-relation", rich_help_panel
78
78
  blockchain_app = typer.Typer(no_args_is_help=True, help="Device blockchain commands (mined block ledger).")
79
79
  app.add_typer(blockchain_app, name="blockchain", rich_help_panel="Analiz & Skor")
80
80
  calibration_app = typer.Typer(no_args_is_help=True, help="Device calibration commands (Gain/Offset per variable).")
81
+ calibration_profile_app = typer.Typer(no_args_is_help=True, help="Reusable calibration profile commands (2026-10-15 redesign - share one Gain/Offset definition across many assignments).")
82
+ calibration_assignment_app = typer.Typer(no_args_is_help=True, help="Calibration assignment commands (2026-10-15 redesign - DEVICE/GROUP/GLOBAL scope, priority, time-windowed validity).")
81
83
  app.add_typer(calibration_app, name="calibration", rich_help_panel="Analiz & Skor")
84
+ app.add_typer(calibration_profile_app, name="calibration-profile", rich_help_panel="Analiz & Skor")
85
+ app.add_typer(calibration_assignment_app, name="calibration-assignment", rich_help_panel="Analiz & Skor")
82
86
  score_app = typer.Typer(no_args_is_help=True, help="Cinga Score commands (cinga_score_snapshot rows - list/get/bundle/revisions/calculate/analysis-request/narrative/delete).")
83
87
  app.add_typer(score_app, name="score", rich_help_panel="Analiz & Skor")
84
88
  crop_type_app = typer.Typer(no_args_is_help=True, help="Crop type catalog commands.")
@@ -1161,6 +1165,91 @@ def device_reliability(
1161
1165
  ]))
1162
1166
 
1163
1167
 
1168
+ _WINDOWS_PERIOD_TYPE_TO_CODE = {"day": 1, "week": 7, "month": 30}
1169
+ _WINDOWS_PERIOD_CODE_TO_LABEL = {1: "Günlük", 7: "Haftalık", 30: "Aylık"}
1170
+
1171
+
1172
+ @device_app.command("windows")
1173
+ def device_windows(
1174
+ device_id: str = typer.Argument(..., help="Device ID."),
1175
+ variable_id: Optional[str] = typer.Option(None, "--variable-id", help="Restrict to one variable (e.g. VRMS_A). Omit for every in-scope variable."),
1176
+ period: Optional[str] = typer.Option(None, "--period", help="day | week | month. Omit for all three."),
1177
+ days: Optional[int] = typer.Option(None, "--days", help="Window size in days, by window_start, ending now. Ignored if --start/--end are given."),
1178
+ start: Optional[str] = typer.Option(None, "--start", help="ISO date/datetime lower bound on window_start, inclusive. Takes priority over --days."),
1179
+ end: Optional[str] = typer.Option(None, "--end", help="ISO date/datetime upper bound on window_start, exclusive. Takes priority over --days."),
1180
+ limit: Optional[int] = typer.Option(None, "--limit", help="Cap the number of rows shown. Omit to page through everything interactively."),
1181
+ as_json: bool = typer.Option(False, "--json", help="Print raw JSON instead of a human-readable table."),
1182
+ ):
1183
+ """Pre-computed daily/weekly/monthly statistics for a device, voltage/current/power/energy only (GET /hermes/windows/{device_id}). Not real-time - see `qapu trend`/`qapu data` for live readings."""
1184
+
1185
+ # Translate --period's Friendly Name Into the Raw int the API Expects
1186
+ period_type: Optional[int] = None
1187
+ if period is not None:
1188
+ period_type = _WINDOWS_PERIOD_TYPE_TO_CODE.get(period.strip().lower())
1189
+ if period_type is None:
1190
+ console.print(f"[red]Geçersiz --period değeri: {period!r} (day, week veya month olmalı)[/red]")
1191
+ raise typer.Exit(code=1)
1192
+
1193
+ # Fetch the Rows
1194
+ params: dict[str, Any] = {}
1195
+ if variable_id:
1196
+ params["variable_id"] = variable_id
1197
+ if period_type is not None:
1198
+ params["period_type"] = period_type
1199
+ if start or end:
1200
+ if start:
1201
+ params["start"] = start
1202
+ if end:
1203
+ params["end"] = end
1204
+ elif days is not None:
1205
+ params["days"] = days
1206
+ rows: list[dict[str, Any]] = client.get(f"/hermes/windows/{device_id}", params=params)
1207
+
1208
+ # JSON Output - Nothing Further to Format
1209
+ if as_json:
1210
+ typer.echo(json.dumps(rows, indent=2))
1211
+ return
1212
+
1213
+ # No Rows - Say so Plainly
1214
+ if not rows:
1215
+ console.print(f"[yellow]{device_id} için kayıt bulunamadı.[/yellow]")
1216
+ return
1217
+
1218
+ # Build and Print the Table, Paginated Like Every Other Long List in
1219
+ # This CLI (device/variable/synthesis list) - a Device's own Window
1220
+ # History Can Easily Run Into the Hundreds of Rows Once Every Variable
1221
+ # and Period Type is Included.
1222
+ build_table = _simple_table_builder([
1223
+ ("DÖNEM", "period_type"),
1224
+ ("DEĞİŞKEN", "variable_id"),
1225
+ ("BAŞLANGIÇ", "window_start"),
1226
+ ("ORT", "mean"),
1227
+ ("MIN", "min"),
1228
+ ("MAKS", "max"),
1229
+ ("ÖRNEK", "sample_count"),
1230
+ ("TAMLIK", "completeness_ratio"),
1231
+ ])
1232
+
1233
+ # Pretty-Print Each Row's period_type/window_start/completeness_ratio
1234
+ # Before Handing off to the Generic Table Builder - Raw int Period
1235
+ # Codes and ISO Timestamps/Ratios Read Poorly Side by Side With Every
1236
+ # Other Column's Already-Human Values.
1237
+ display_rows = []
1238
+ for row in rows:
1239
+ display_row = dict(row)
1240
+ display_row["period_type"] = _WINDOWS_PERIOD_CODE_TO_LABEL.get(row.get("period_type"), row.get("period_type"))
1241
+ display_row["window_start"] = _format_time(row.get("window_start"))
1242
+ ratio = row.get("completeness_ratio")
1243
+ display_row["completeness_ratio"] = f"%{ratio * 100:.0f}" if ratio is not None else "-"
1244
+ for numeric_col in ("mean", "min", "max"):
1245
+ value = row.get(numeric_col)
1246
+ if isinstance(value, (int, float)):
1247
+ display_row[numeric_col] = round(value, 3)
1248
+ display_rows.append(display_row)
1249
+
1250
+ _print_paginated(display_rows, build_table, limit, "pencere")
1251
+
1252
+
1164
1253
  @device_app.command("purge-history")
1165
1254
  def device_purge_history(
1166
1255
  device_id: str = typer.Argument(..., help="Device ID."),
@@ -5419,9 +5508,14 @@ def calibration_list(device_id: str = typer.Argument(...), as_json: bool = typer
5419
5508
  typer.echo("Bu cihaz için kalibrasyon kaydı yok.")
5420
5509
  return
5421
5510
 
5422
- # Build the Table
5511
+ # Build the Table - SCOPE (2026-10-15, calibration_profiles/
5512
+ # calibration_assignments Redesign) Shows Which Scope Tier This
5513
+ # Device's Effective Value Actually Came From; ID is Blank for a
5514
+ # GROUP/GLOBAL-Inherited row (not Directly Editable at This Device's
5515
+ # Level - See Calibration.list()'s own Docstring)
5423
5516
  table = Table(show_header=True, header_style="bold")
5424
5517
  table.add_column("ID", justify="right")
5518
+ table.add_column("SCOPE")
5425
5519
  table.add_column("VARIABLE")
5426
5520
  table.add_column("GAIN", justify="right")
5427
5521
  table.add_column("OFFSET", justify="right")
@@ -5430,7 +5524,8 @@ def calibration_list(device_id: str = typer.Argument(...), as_json: bool = typer
5430
5524
  for item in items:
5431
5525
  variable = item.get("Variable") or {}
5432
5526
  table.add_row(
5433
- str(item.get("ID")),
5527
+ str(item.get("ID")) if item.get("ID") is not None else "-",
5528
+ item.get("Scope") or "-",
5434
5529
  variable.get("ID") or "-",
5435
5530
  str(item.get("Gain")) if item.get("Gain") is not None else "-",
5436
5531
  str(item.get("Offset")) if item.get("Offset") is not None else "-",
@@ -5496,6 +5591,195 @@ def calibration_delete(calibration_id: int = typer.Argument(...)):
5496
5591
  typer.echo(result.get("Message", result))
5497
5592
 
5498
5593
 
5594
+ # Calibration Profile Commands (2026-10-15) - Reusable Gain/Offset
5595
+ # Definitions, Genuinely Shareable Across Many Calibration_Assignment rows
5596
+
5597
+ @calibration_profile_app.command("list")
5598
+ def calibration_profile_list(variable_id: Optional[str] = typer.Option(None, "--variable-id"), as_json: bool = typer.Option(False, "--json")):
5599
+ """List reusable calibration profiles (GET /hermes/calibration-profiles)."""
5600
+ items: list[dict[str, Any]] = client.get("/hermes/calibration-profiles", params={"variable_id": variable_id} if variable_id else None)
5601
+ if as_json:
5602
+ typer.echo(json.dumps(items, indent=2))
5603
+ return
5604
+ if not items:
5605
+ typer.echo("Hiç kalibrasyon profili yok.")
5606
+ return
5607
+ table = Table(show_header=True, header_style="bold")
5608
+ table.add_column("ID", justify="right")
5609
+ table.add_column("VARIABLE")
5610
+ table.add_column("GAIN", justify="right")
5611
+ table.add_column("OFFSET", justify="right")
5612
+ table.add_column("ACTIVE")
5613
+ table.add_column("DESCRIPTION")
5614
+ for item in items:
5615
+ variable = item.get("Variable") or {}
5616
+ table.add_row(str(item.get("ID")), variable.get("ID") or "-", str(item.get("Gain")), str(item.get("Offset")), "✓" if item.get("Is_Active") else "-", item.get("Description") or "-")
5617
+ console.print(table)
5618
+
5619
+
5620
+ @calibration_profile_app.command("get")
5621
+ def calibration_profile_get(profile_id: int = typer.Argument(...), as_json: bool = typer.Option(False, "--json")):
5622
+ """Get one calibration profile's detail (GET /hermes/calibration-profiles/{profile_id})."""
5623
+ result = client.get(f"/hermes/calibration-profiles/{profile_id}")
5624
+ if as_json:
5625
+ typer.echo(json.dumps(result, indent=2))
5626
+ return
5627
+ _print_detail(result)
5628
+
5629
+
5630
+ @calibration_profile_app.command("add")
5631
+ def calibration_profile_add(
5632
+ variable_id: str = typer.Argument(..., help="Variable ID this profile applies to."),
5633
+ gain: float = typer.Option(..., "--gain"),
5634
+ offset: float = typer.Option(..., "--offset"),
5635
+ description: Optional[str] = typer.Option(None, "--description"),
5636
+ ):
5637
+ """Create a new, standalone calibration profile - not assigned to anything until a calibration-assignment points at it (POST /hermes/calibration-profiles)."""
5638
+ body: dict[str, Any] = {"Variable": {"ID": variable_id}, "Gain": gain, "Offset": offset}
5639
+ if description is not None:
5640
+ body["Description"] = description
5641
+ result = client.post("/hermes/calibration-profiles", json_body=body)
5642
+ typer.echo(result.get("Message", result))
5643
+
5644
+
5645
+ @calibration_profile_app.command("update")
5646
+ def calibration_profile_update(
5647
+ profile_id: int = typer.Argument(...),
5648
+ gain: Optional[float] = typer.Option(None, "--gain"),
5649
+ offset: Optional[float] = typer.Option(None, "--offset"),
5650
+ is_active: Optional[bool] = typer.Option(None, "--active/--inactive"),
5651
+ description: Optional[str] = typer.Option(None, "--description"),
5652
+ ):
5653
+ """Update a calibration profile - only the fields passed are changed. Affects every assignment pointing at it (PUT /hermes/calibration-profiles/{profile_id})."""
5654
+ body: dict[str, Any] = {}
5655
+ if gain is not None:
5656
+ body["Gain"] = gain
5657
+ if offset is not None:
5658
+ body["Offset"] = offset
5659
+ if is_active is not None:
5660
+ body["Is_Active"] = is_active
5661
+ if description is not None:
5662
+ body["Description"] = description
5663
+ result = client.put(f"/hermes/calibration-profiles/{profile_id}", json_body=body)
5664
+ typer.echo(result.get("Message", result))
5665
+
5666
+
5667
+ @calibration_profile_app.command("delete")
5668
+ def calibration_profile_delete(profile_id: int = typer.Argument(...)):
5669
+ """Delete a calibration profile - refuses if any assignment still points at it (DELETE /hermes/calibration-profiles/{profile_id})."""
5670
+ result = client.delete(f"/hermes/calibration-profiles/{profile_id}")
5671
+ typer.echo(result.get("Message", result))
5672
+
5673
+
5674
+ # Calibration Assignment Commands (2026-10-15) - DEVICE/GROUP/GLOBAL
5675
+ # Scope, Priority, Time-Windowed Validity. The Only way to Create a
5676
+ # GROUP/GLOBAL Assignment, or to Point a new Assignment at an Existing
5677
+ # Profile for Real Reuse.
5678
+
5679
+ @calibration_assignment_app.command("list")
5680
+ def calibration_assignment_list(
5681
+ variable_id: Optional[str] = typer.Option(None, "--variable-id"),
5682
+ device_id: Optional[str] = typer.Option(None, "--device-id"),
5683
+ group_id: Optional[int] = typer.Option(None, "--group-id"),
5684
+ scope_type: Optional[str] = typer.Option(None, "--scope", help="DEVICE / GROUP / GLOBAL"),
5685
+ calibration_id: Optional[int] = typer.Option(None, "--profile-id"),
5686
+ as_json: bool = typer.Option(False, "--json"),
5687
+ ):
5688
+ """List calibration assignments (GET /hermes/calibration-assignments)."""
5689
+ params = {"variable_id": variable_id, "device_id": device_id, "group_id": group_id, "scope_type": scope_type, "calibration_id": calibration_id}
5690
+ items: list[dict[str, Any]] = client.get("/hermes/calibration-assignments", params={k: v for k, v in params.items() if v is not None})
5691
+ if as_json:
5692
+ typer.echo(json.dumps(items, indent=2))
5693
+ return
5694
+ if not items:
5695
+ typer.echo("Hiç kalibrasyon ataması yok.")
5696
+ return
5697
+ table = Table(show_header=True, header_style="bold")
5698
+ table.add_column("ID", justify="right")
5699
+ table.add_column("PROFILE", justify="right")
5700
+ table.add_column("VARIABLE")
5701
+ table.add_column("SCOPE")
5702
+ table.add_column("TARGET")
5703
+ table.add_column("PRIORITY", justify="right")
5704
+ table.add_column("ACTIVE")
5705
+ table.add_column("EFFECTIVE FROM")
5706
+ table.add_column("EFFECTIVE TO")
5707
+ for item in items:
5708
+ target = item.get("Device_ID") or (f"group:{item.get('Group_ID')}" if item.get("Group_ID") else "-")
5709
+ table.add_row(
5710
+ str(item.get("ID")), str(item.get("Calibration_ID")), item.get("Variable_ID") or "-", item.get("Scope_Type") or "-",
5711
+ target, str(item.get("Priority_Override")) if item.get("Priority_Override") is not None else "-",
5712
+ "✓" if item.get("Is_Active") else "-", item.get("Effective_From") or "-", item.get("Effective_To") or "-",
5713
+ )
5714
+ console.print(table)
5715
+
5716
+
5717
+ @calibration_assignment_app.command("get")
5718
+ def calibration_assignment_get(assignment_id: int = typer.Argument(...), as_json: bool = typer.Option(False, "--json")):
5719
+ """Get one calibration assignment's detail (GET /hermes/calibration-assignments/{assignment_id})."""
5720
+ result = client.get(f"/hermes/calibration-assignments/{assignment_id}")
5721
+ if as_json:
5722
+ typer.echo(json.dumps(result, indent=2))
5723
+ return
5724
+ _print_detail(result)
5725
+
5726
+
5727
+ @calibration_assignment_app.command("add")
5728
+ def calibration_assignment_add(
5729
+ profile_id: int = typer.Argument(..., help="The calibration-profile ID this assignment points at."),
5730
+ scope: str = typer.Option(..., "--scope", help="DEVICE / GROUP / GLOBAL"),
5731
+ device_id: Optional[str] = typer.Option(None, "--device-id", help="Required for --scope DEVICE."),
5732
+ group_id: Optional[int] = typer.Option(None, "--group-id", help="Required for --scope GROUP."),
5733
+ priority: Optional[int] = typer.Option(None, "--priority", help="Lower value wins ties (e.g. a device belonging to multiple groups)."),
5734
+ ):
5735
+ """Create a new calibration assignment - the only way to create a GROUP/GLOBAL-scope assignment, or to point a new assignment at an existing profile for real reuse (POST /hermes/calibration-assignments)."""
5736
+ body: dict[str, Any] = {"Calibration_ID": profile_id, "Scope_Type": scope.upper()}
5737
+ if device_id is not None:
5738
+ body["Device_ID"] = device_id
5739
+ if group_id is not None:
5740
+ body["Group_ID"] = group_id
5741
+ if priority is not None:
5742
+ body["Priority_Override"] = priority
5743
+ result = client.post("/hermes/calibration-assignments", json_body=body)
5744
+ typer.echo(result.get("Message", result))
5745
+
5746
+
5747
+ @calibration_assignment_app.command("update")
5748
+ def calibration_assignment_update(
5749
+ assignment_id: int = typer.Argument(...),
5750
+ priority: Optional[int] = typer.Option(None, "--priority"),
5751
+ effective_from: Optional[str] = typer.Option(None, "--effective-from"),
5752
+ effective_to: Optional[str] = typer.Option(None, "--effective-to"),
5753
+ is_active: Optional[bool] = typer.Option(None, "--active/--inactive"),
5754
+ ):
5755
+ """Update an assignment's priority/validity window/active flag - scope/target/profile are not editable here (delete+add instead) (PUT /hermes/calibration-assignments/{assignment_id})."""
5756
+ body: dict[str, Any] = {}
5757
+ if priority is not None:
5758
+ body["Priority_Override"] = priority
5759
+ if effective_from is not None:
5760
+ body["Effective_From"] = effective_from
5761
+ if effective_to is not None:
5762
+ body["Effective_To"] = effective_to
5763
+ if is_active is not None:
5764
+ body["Is_Active"] = is_active
5765
+ result = client.put(f"/hermes/calibration-assignments/{assignment_id}", json_body=body)
5766
+ typer.echo(result.get("Message", result))
5767
+
5768
+
5769
+ @calibration_assignment_app.command("retire")
5770
+ def calibration_assignment_retire(assignment_id: int = typer.Argument(...)):
5771
+ """Retire an assignment (Effective_To=now(), Is_Active=false) - preserves it as real history instead of deleting it (POST /hermes/calibration-assignments/{assignment_id}/retire)."""
5772
+ result = client.post(f"/hermes/calibration-assignments/{assignment_id}/retire", json_body={})
5773
+ typer.echo(result.get("Message", result))
5774
+
5775
+
5776
+ @calibration_assignment_app.command("delete")
5777
+ def calibration_assignment_delete(assignment_id: int = typer.Argument(...)):
5778
+ """Delete a calibration assignment outright - loses history, prefer 'retire' when the point is to preserve it (DELETE /hermes/calibration-assignments/{assignment_id})."""
5779
+ result = client.delete(f"/hermes/calibration-assignments/{assignment_id}")
5780
+ typer.echo(result.get("Message", result))
5781
+
5782
+
5499
5783
  # Device Trigger-Rule Commands (2026-10-10) - This is the Real Device-
5500
5784
  # Side Rule Engine services/rule Evaluates on Every qapu.stream Message
5501
5785
  # (Low Voltage/High Current/Pump Active/etc.) - Distinct From `synthesis`
@@ -7661,7 +7945,7 @@ def _console_fetch(device_id: str) -> dict[str, Any]:
7661
7945
  @auth_token_app.command("create")
7662
7946
  def auth_token_create(
7663
7947
  label: str = typer.Argument(..., help="A human-readable label for this token, e.g. 'macbook-mcp'."),
7664
- packages: str = typer.Option(..., "--packages", help="Comma-separated permission package codes, e.g. catalog:read,telemetry:read - must be a subset of your own granted packages."),
7948
+ packages: str = typer.Option(..., "--packages", help="Comma-separated permission package codes, e.g. devices:read,telemetry:read,infrastructure:read - must be a subset of your own granted packages (see `qapu user tiers` for the current catalog; `catalog:read`/`infrastructure:*`/`telemetry:write` are the RETIRED pre-2026-10-10 names and no longer unlock any route)."),
7665
7949
  expires_in_days: int = typer.Option(90, "--expires-in-days"),
7666
7950
  no_save: bool = typer.Option(False, "--no-save", help="Skip the 'save as active Hermes token?' prompt entirely - print the raw token and nothing else."),
7667
7951
  ):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qapu-cli
3
- Version: 0.74.0
3
+ Version: 0.76.0
4
4
  Summary: CLI client for the Qapu API - built for the Hermes agent, but usable by anyone talking to api.ovoo.com.tr from outside the Swarm.
5
5
  Author: OVOO Technology
6
6
  Classifier: Programming Language :: Python :: 3
File without changes
File without changes
File without changes
File without changes
File without changes