qapu-cli 0.3.0__tar.gz → 0.4.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.
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/PKG-INFO +14 -1
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/README.md +13 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/pyproject.toml +1 -1
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli/main.py +195 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/PKG-INFO +14 -1
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli/__init__.py +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli/client.py +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/SOURCES.txt +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/dependency_links.txt +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/entry_points.txt +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/requires.txt +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/qapu_cli.egg-info/top_level.txt +0 -0
- {qapu_cli-0.3.0 → qapu_cli-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -49,6 +49,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
|
|
|
49
49
|
## Commands
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
+
qapu --version # print the installed qapu-cli version and exit
|
|
52
53
|
qapu health # GET /health - no auth, quick connectivity check
|
|
53
54
|
qapu device list # GET /hermes/devices - every device, bulk, as a table
|
|
54
55
|
qapu device list --json # same data, raw JSON
|
|
@@ -76,6 +77,18 @@ qapu data <device_id> --search vrms # substring match on variable ID, e.g. VRMS
|
|
|
76
77
|
|
|
77
78
|
`qapu data`'s family filters (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`) are a union - passing more than one shows variables matching any of them. `--search` narrows whatever they leave (or the full list, if none were given) further, by a case-insensitive substring match on the variable ID - the way to pin down an exact family like `VRMS_R`/`VRMS_S`/`VRMS_T`. Values come from the measurement cache's rolling buffer (the same source `/measurement/{device_id}/last/{variable_id}` reads from), not a fresh device poll - "latest" means the most recent packet already ingested, not real-time.
|
|
78
79
|
|
|
80
|
+
```bash
|
|
81
|
+
qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
|
|
82
|
+
qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
|
|
83
|
+
qapu trend <device_id> <variable_id> --days 2 # daily min/avg/max over the last 2 days, instead of raw readings
|
|
84
|
+
qapu trend <device_id> <variable_id> --days 30 # ...or the last month
|
|
85
|
+
qapu trend <device_id> <variable_id> --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`qapu trend` shows a per-variable history plus a simple linear-regression trend line (slope + direction). Two modes, picked by which option you pass:
|
|
89
|
+
- **Raw readings** (`--last N`, default `N=20`): the measurement cache's rolling buffer - bounded to its own depth (~50 most recent packets), *not* a time window. Good for "what's it doing right now."
|
|
90
|
+
- **Daily aggregates** (`--days N`): one row per calendar day (min/avg/max/count), from the same cache the `/measurement/{device_id}/history` endpoint reads - this is what actually covers multi-day/month windows, since the raw buffer doesn't go back that far. `--days` wins if both are given.
|
|
91
|
+
|
|
79
92
|
Filtering (`--status`/`--model`/`--limit`) happens client-side in the CLI, not on the server - fine at the current fleet size, worth moving server-side (`GET /hermes/devices?status=...`) if it ever grows large enough to matter.
|
|
80
93
|
|
|
81
94
|
## Auth (current placeholder - read this before pointing at production)
|
|
@@ -37,6 +37,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
|
|
|
37
37
|
## Commands
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
+
qapu --version # print the installed qapu-cli version and exit
|
|
40
41
|
qapu health # GET /health - no auth, quick connectivity check
|
|
41
42
|
qapu device list # GET /hermes/devices - every device, bulk, as a table
|
|
42
43
|
qapu device list --json # same data, raw JSON
|
|
@@ -64,6 +65,18 @@ qapu data <device_id> --search vrms # substring match on variable ID, e.g. VRMS
|
|
|
64
65
|
|
|
65
66
|
`qapu data`'s family filters (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`) are a union - passing more than one shows variables matching any of them. `--search` narrows whatever they leave (or the full list, if none were given) further, by a case-insensitive substring match on the variable ID - the way to pin down an exact family like `VRMS_R`/`VRMS_S`/`VRMS_T`. Values come from the measurement cache's rolling buffer (the same source `/measurement/{device_id}/last/{variable_id}` reads from), not a fresh device poll - "latest" means the most recent packet already ingested, not real-time.
|
|
66
67
|
|
|
68
|
+
```bash
|
|
69
|
+
qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
|
|
70
|
+
qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
|
|
71
|
+
qapu trend <device_id> <variable_id> --days 2 # daily min/avg/max over the last 2 days, instead of raw readings
|
|
72
|
+
qapu trend <device_id> <variable_id> --days 30 # ...or the last month
|
|
73
|
+
qapu trend <device_id> <variable_id> --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`qapu trend` shows a per-variable history plus a simple linear-regression trend line (slope + direction). Two modes, picked by which option you pass:
|
|
77
|
+
- **Raw readings** (`--last N`, default `N=20`): the measurement cache's rolling buffer - bounded to its own depth (~50 most recent packets), *not* a time window. Good for "what's it doing right now."
|
|
78
|
+
- **Daily aggregates** (`--days N`): one row per calendar day (min/avg/max/count), from the same cache the `/measurement/{device_id}/history` endpoint reads - this is what actually covers multi-day/month windows, since the raw buffer doesn't go back that far. `--days` wins if both are given.
|
|
79
|
+
|
|
67
80
|
Filtering (`--status`/`--model`/`--limit`) happens client-side in the CLI, not on the server - fine at the current fleet size, worth moving server-side (`GET /hermes/devices?status=...`) if it ever grows large enough to matter.
|
|
68
81
|
|
|
69
82
|
## Auth (current placeholder - read this before pointing at production)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qapu-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version as _package_version
|
|
8
9
|
from typing import Any, Optional
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
@@ -22,6 +23,33 @@ app.add_typer(variable_app, name="variable")
|
|
|
22
23
|
|
|
23
24
|
console = Console()
|
|
24
25
|
|
|
26
|
+
|
|
27
|
+
# --version - Reads the Installed Package Metadata Directly, so it's
|
|
28
|
+
# Always in Sync with pyproject.toml's version - Nothing to Hand-Bump Here.
|
|
29
|
+
def _version_callback(show_version: bool) -> None:
|
|
30
|
+
|
|
31
|
+
# Only Act if --version was Actually Passed
|
|
32
|
+
if not show_version:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Try to Resolve the Installed Version
|
|
36
|
+
try:
|
|
37
|
+
typer.echo(_package_version("qapu-cli"))
|
|
38
|
+
|
|
39
|
+
# Not Installed as a Package (e.g. Running Straight from Source)
|
|
40
|
+
except PackageNotFoundError:
|
|
41
|
+
typer.echo("unknown (not installed as a package)")
|
|
42
|
+
|
|
43
|
+
# Exit Before Typer Falls Through to "No Command Given" Help Text
|
|
44
|
+
raise typer.Exit()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.callback()
|
|
48
|
+
def _root(
|
|
49
|
+
version: Optional[bool] = typer.Option(None, "--version", callback=_version_callback, is_eager=True, help="Show the installed qapu-cli version and exit."),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Qapu API client CLI."""
|
|
52
|
+
|
|
25
53
|
# Segment Name -> ID, Matching Constants.Variable_Segment (common/qapu_common/constants.py)
|
|
26
54
|
SEGMENTS = {
|
|
27
55
|
"unknown": 0,
|
|
@@ -544,6 +572,173 @@ def device_data(
|
|
|
544
572
|
console.print(_build_data_table(sorted(readings, key=lambda r: r.get("Variable_ID") or "")))
|
|
545
573
|
|
|
546
574
|
|
|
575
|
+
# Simple Least-Squares Slope - No Extra Dependency for This, and n < 2
|
|
576
|
+
# (Or a Vertical/Degenerate x Range) Just Means "Not Enough Points Yet"
|
|
577
|
+
def _linear_slope(xs: list[float], ys: list[float]) -> Optional[float]:
|
|
578
|
+
|
|
579
|
+
# Not Enough Points to Fit a Line
|
|
580
|
+
n = len(xs)
|
|
581
|
+
if n < 2:
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
# Compute Means
|
|
585
|
+
mean_x = sum(xs) / n
|
|
586
|
+
mean_y = sum(ys) / n
|
|
587
|
+
|
|
588
|
+
# Compute Slope via Covariance / Variance
|
|
589
|
+
numerator = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys))
|
|
590
|
+
denominator = sum((x - mean_x) ** 2 for x in xs)
|
|
591
|
+
|
|
592
|
+
# Degenerate x Range (All Same Timestamp/Day) - No Meaningful Slope
|
|
593
|
+
if denominator == 0:
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
# Return the Slope
|
|
597
|
+
return numerator / denominator
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# Describe a Slope's Direction in Turkish, for the Trend Summary Line
|
|
601
|
+
def _slope_direction(slope: float) -> str:
|
|
602
|
+
|
|
603
|
+
# Resolve Direction Label
|
|
604
|
+
if slope > 0:
|
|
605
|
+
return "artıyor"
|
|
606
|
+
if slope < 0:
|
|
607
|
+
return "azalıyor"
|
|
608
|
+
return "sabit"
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# Get One Variable's Trend on One Device - Either the Last N Raw Buffered
|
|
612
|
+
# Readings, or Daily Min/Avg/Max Over the Last N Days (Mutually Exclusive -
|
|
613
|
+
# --days Wins if Both are Given)
|
|
614
|
+
@app.command("trend")
|
|
615
|
+
def device_trend(
|
|
616
|
+
device_id: str = typer.Argument(..., help="Device ID."),
|
|
617
|
+
variable_id: str = typer.Argument(..., help="Variable ID (e.g. VRMS_R)."),
|
|
618
|
+
last: int = typer.Option(20, "--last", help="Show the last N raw buffered readings. Bounded by the buffer's own depth (~50 most recent packets) - not a time window."),
|
|
619
|
+
days: Optional[int] = typer.Option(None, "--days", help="Show daily min/avg/max over the last N days instead of raw readings (e.g. --days 2, --days 30 for a month). Takes priority over --last if both are given."),
|
|
620
|
+
as_json: bool = typer.Option(False, "--json", help="Print raw JSON instead of a table."),
|
|
621
|
+
):
|
|
622
|
+
"""Trend for one variable on one device: recent readings or daily min/avg/max (GET /hermes/trend/{device_id}/{variable_id})."""
|
|
623
|
+
|
|
624
|
+
# Fetch the Trend - Raw Series + Full Daily History, Already Enriched
|
|
625
|
+
t: dict[str, Any] = client.get(f"/hermes/trend/{device_id}/{variable_id}")
|
|
626
|
+
|
|
627
|
+
# JSON Output - Everything at Once, no Slicing/Stats Applied
|
|
628
|
+
if as_json:
|
|
629
|
+
|
|
630
|
+
# Print Raw JSON
|
|
631
|
+
typer.echo(json.dumps(t, indent=2))
|
|
632
|
+
return
|
|
633
|
+
|
|
634
|
+
# Header Line - What Variable, on What Device
|
|
635
|
+
description = t.get("Description") or "-"
|
|
636
|
+
unit = t.get("Unit") or "-"
|
|
637
|
+
segment_name = t.get("Segment") or "-"
|
|
638
|
+
console.print(f"[bold]{t.get('Variable_ID') or variable_id}[/bold] - {description} ({unit}) - {segment_name}")
|
|
639
|
+
|
|
640
|
+
# --- Daily Mode (--days) ---
|
|
641
|
+
if days is not None:
|
|
642
|
+
|
|
643
|
+
# Resolve the Last N Days, Sorted Chronologically
|
|
644
|
+
daily: dict[str, Any] = t.get("Daily") or {}
|
|
645
|
+
sorted_days = sorted(daily.keys())[-days:]
|
|
646
|
+
|
|
647
|
+
# Nothing to Show
|
|
648
|
+
if not sorted_days:
|
|
649
|
+
typer.echo("Bu değişken için günlük veri yok.")
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
# Build the Daily Table
|
|
653
|
+
table = Table(show_header=True, header_style="bold")
|
|
654
|
+
table.add_column("DATE")
|
|
655
|
+
table.add_column("MIN", justify="right")
|
|
656
|
+
table.add_column("AVG", justify="right")
|
|
657
|
+
table.add_column("MAX", justify="right")
|
|
658
|
+
table.add_column("COUNT", justify="right")
|
|
659
|
+
|
|
660
|
+
# Populate Rows, Collecting (Day Index, Avg) Pairs for the Slope
|
|
661
|
+
xs: list[float] = []
|
|
662
|
+
ys: list[float] = []
|
|
663
|
+
for i, day in enumerate(sorted_days):
|
|
664
|
+
|
|
665
|
+
# Resolve the Day's Node
|
|
666
|
+
node = daily.get(day) or {}
|
|
667
|
+
avg = node.get("Avg")
|
|
668
|
+
|
|
669
|
+
# Add the Row
|
|
670
|
+
table.add_row(
|
|
671
|
+
day,
|
|
672
|
+
f"{node.get('Min'):g}" if isinstance(node.get("Min"), (int, float)) else "-",
|
|
673
|
+
f"{avg:g}" if isinstance(avg, (int, float)) else "-",
|
|
674
|
+
f"{node.get('Max'):g}" if isinstance(node.get("Max"), (int, float)) else "-",
|
|
675
|
+
str(node.get("Count", "-")),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Track for the Slope (Trend Over the Daily Averages)
|
|
679
|
+
if isinstance(avg, (int, float)):
|
|
680
|
+
xs.append(float(i))
|
|
681
|
+
ys.append(float(avg))
|
|
682
|
+
|
|
683
|
+
# Print the Table
|
|
684
|
+
console.print(table)
|
|
685
|
+
|
|
686
|
+
# Print the Trend Summary, if There Were Enough Points
|
|
687
|
+
slope = _linear_slope(xs, ys)
|
|
688
|
+
if slope is not None:
|
|
689
|
+
typer.echo(f"Trend: {slope:+.4f} {unit}/gün ({_slope_direction(slope)})")
|
|
690
|
+
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
# --- Raw Readings Mode (Default, --last) ---
|
|
694
|
+
series: list = t.get("Series") or []
|
|
695
|
+
series = series[-last:] if last > 0 else series
|
|
696
|
+
|
|
697
|
+
# Nothing Buffered Yet
|
|
698
|
+
if not series:
|
|
699
|
+
typer.echo("Bu değişken için henüz buffer verisi yok.")
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
# Build the Readings Table, Chronological (Oldest First)
|
|
703
|
+
table = Table(show_header=True, header_style="bold")
|
|
704
|
+
table.add_column("TIME")
|
|
705
|
+
table.add_column("VALUE", justify="right")
|
|
706
|
+
|
|
707
|
+
# Populate Rows, Collecting (Elapsed Hours, Value) Pairs for the Slope
|
|
708
|
+
xs = []
|
|
709
|
+
ys = []
|
|
710
|
+
first_time: Optional[datetime] = None
|
|
711
|
+
for entry in series:
|
|
712
|
+
|
|
713
|
+
# Skip Malformed Entries Defensively
|
|
714
|
+
if not isinstance(entry, list) or len(entry) != 2:
|
|
715
|
+
continue
|
|
716
|
+
|
|
717
|
+
# Unpack the Entry
|
|
718
|
+
raw_time, value = entry
|
|
719
|
+
table.add_row(_relative_time(raw_time), f"{value:g}" if isinstance(value, (int, float)) else "-")
|
|
720
|
+
|
|
721
|
+
# Track for the Slope (Trend Over Elapsed Hours Since the First Point)
|
|
722
|
+
try:
|
|
723
|
+
parsed_time = datetime.fromisoformat(raw_time)
|
|
724
|
+
if first_time is None:
|
|
725
|
+
first_time = parsed_time
|
|
726
|
+
xs.append((parsed_time - first_time).total_seconds() / 3600)
|
|
727
|
+
ys.append(float(value))
|
|
728
|
+
except (ValueError, TypeError):
|
|
729
|
+
continue
|
|
730
|
+
|
|
731
|
+
# Print the Table
|
|
732
|
+
console.print(table)
|
|
733
|
+
|
|
734
|
+
# Print Min/Avg/Max and the Trend Summary
|
|
735
|
+
if ys:
|
|
736
|
+
typer.echo(f"Min: {min(ys):g} Avg: {sum(ys) / len(ys):g} Max: {max(ys):g}")
|
|
737
|
+
slope = _linear_slope(xs, ys)
|
|
738
|
+
if slope is not None:
|
|
739
|
+
typer.echo(f"Trend: {slope:+.4f} {unit}/saat ({_slope_direction(slope)})")
|
|
740
|
+
|
|
741
|
+
|
|
547
742
|
if __name__ == "__main__":
|
|
548
743
|
|
|
549
744
|
# Run the Typer App
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qapu-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -49,6 +49,7 @@ Either way installs a `qapu` command (see `pyproject.toml`'s `[project.scripts]`
|
|
|
49
49
|
## Commands
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
+
qapu --version # print the installed qapu-cli version and exit
|
|
52
53
|
qapu health # GET /health - no auth, quick connectivity check
|
|
53
54
|
qapu device list # GET /hermes/devices - every device, bulk, as a table
|
|
54
55
|
qapu device list --json # same data, raw JSON
|
|
@@ -76,6 +77,18 @@ qapu data <device_id> --search vrms # substring match on variable ID, e.g. VRMS
|
|
|
76
77
|
|
|
77
78
|
`qapu data`'s family filters (`--energy`/`--gsm`/`--voltage`/`--current`/`--battery`) are a union - passing more than one shows variables matching any of them. `--search` narrows whatever they leave (or the full list, if none were given) further, by a case-insensitive substring match on the variable ID - the way to pin down an exact family like `VRMS_R`/`VRMS_S`/`VRMS_T`. Values come from the measurement cache's rolling buffer (the same source `/measurement/{device_id}/last/{variable_id}` reads from), not a fresh device poll - "latest" means the most recent packet already ingested, not real-time.
|
|
78
79
|
|
|
80
|
+
```bash
|
|
81
|
+
qapu trend <device_id> <variable_id> # GET /hermes/trend/{device_id}/{variable_id} - last 20 raw readings + trend
|
|
82
|
+
qapu trend <device_id> <variable_id> --last 50 # last N raw readings instead of the default 20
|
|
83
|
+
qapu trend <device_id> <variable_id> --days 2 # daily min/avg/max over the last 2 days, instead of raw readings
|
|
84
|
+
qapu trend <device_id> <variable_id> --days 30 # ...or the last month
|
|
85
|
+
qapu trend <device_id> <variable_id> --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`qapu trend` shows a per-variable history plus a simple linear-regression trend line (slope + direction). Two modes, picked by which option you pass:
|
|
89
|
+
- **Raw readings** (`--last N`, default `N=20`): the measurement cache's rolling buffer - bounded to its own depth (~50 most recent packets), *not* a time window. Good for "what's it doing right now."
|
|
90
|
+
- **Daily aggregates** (`--days N`): one row per calendar day (min/avg/max/count), from the same cache the `/measurement/{device_id}/history` endpoint reads - this is what actually covers multi-day/month windows, since the raw buffer doesn't go back that far. `--days` wins if both are given.
|
|
91
|
+
|
|
79
92
|
Filtering (`--status`/`--model`/`--limit`) happens client-side in the CLI, not on the server - fine at the current fleet size, worth moving server-side (`GET /hermes/devices?status=...`) if it ever grows large enough to matter.
|
|
80
93
|
|
|
81
94
|
## Auth (current placeholder - read this before pointing at production)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|