plyrana-cli 0.1.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.
- plyrana_cli-0.1.0/PKG-INFO +121 -0
- plyrana_cli-0.1.0/README.md +94 -0
- plyrana_cli-0.1.0/plyrana_cli/__init__.py +3 -0
- plyrana_cli-0.1.0/plyrana_cli/client.py +64 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/__init__.py +1 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/auth.py +84 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/automations.py +88 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/config_cmd.py +40 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/devices.py +101 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/system.py +82 -0
- plyrana_cli-0.1.0/plyrana_cli/commands/variables.py +147 -0
- plyrana_cli-0.1.0/plyrana_cli/config.py +52 -0
- plyrana_cli-0.1.0/plyrana_cli/main.py +48 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/PKG-INFO +121 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/SOURCES.txt +19 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/dependency_links.txt +1 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/entry_points.txt +2 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/requires.txt +3 -0
- plyrana_cli-0.1.0/plyrana_cli.egg-info/top_level.txt +1 -0
- plyrana_cli-0.1.0/pyproject.toml +44 -0
- plyrana_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plyrana-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plyrana CLI — manage any self-hosted Plyrana instance from your terminal. Auth, devices, variables, automations, system.
|
|
5
|
+
Author-email: Plyrana <dev@plyrana.com>
|
|
6
|
+
License: AGPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/plyrana/core
|
|
8
|
+
Project-URL: Documentation, https://github.com/plyrana/core/tree/main/cli
|
|
9
|
+
Project-URL: Repository, https://github.com/plyrana/core
|
|
10
|
+
Project-URL: Issues, https://github.com/plyrana/core/issues
|
|
11
|
+
Keywords: plyrana,iot,cli,devops,automation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: httpx<0.29.0,>=0.23.0
|
|
25
|
+
Requires-Dist: typer>=0.9.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
|
|
28
|
+
# plyrana-cli — Terminal client for Plyrana
|
|
29
|
+
|
|
30
|
+
Thin command-line client for the Plyrana IoT Device Hub REST API. Great for CI/CD, scripting, batch operations.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install plyrana-cli
|
|
36
|
+
# dev install from source:
|
|
37
|
+
pip install -e ./cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## First run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Point to your hub
|
|
44
|
+
plyrana config set-url https://hub.example.com
|
|
45
|
+
|
|
46
|
+
# Login (password prompt if omitted)
|
|
47
|
+
plyrana auth login --email admin@example.com
|
|
48
|
+
|
|
49
|
+
# Verify
|
|
50
|
+
plyrana auth status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### Auth
|
|
56
|
+
```bash
|
|
57
|
+
plyrana auth login --email <EMAIL>
|
|
58
|
+
plyrana auth logout
|
|
59
|
+
plyrana auth status
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Devices
|
|
63
|
+
```bash
|
|
64
|
+
plyrana devices list [--limit 50] [--type hardware]
|
|
65
|
+
plyrana devices get <UID>
|
|
66
|
+
plyrana devices claim <UID>
|
|
67
|
+
plyrana devices unclaim <UID>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Variables
|
|
71
|
+
```bash
|
|
72
|
+
plyrana variables list [--device <UID>]
|
|
73
|
+
plyrana variables get <UID> <KEY>
|
|
74
|
+
plyrana variables set <UID> <KEY> <VALUE> # auto-cast: 22.5 / true / '{"x":1}'
|
|
75
|
+
plyrana variables history <UID> <KEY> [--limit 20]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Automations
|
|
79
|
+
```bash
|
|
80
|
+
plyrana automations list
|
|
81
|
+
plyrana automations run <ID>
|
|
82
|
+
plyrana automations toggle <ID> --on | --off
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### System
|
|
86
|
+
```bash
|
|
87
|
+
plyrana system health # public, no auth
|
|
88
|
+
plyrana system license # CE / Pro / Enterprise + limits
|
|
89
|
+
plyrana system integrations # MQTT / HA / Prometheus / Grafana status
|
|
90
|
+
plyrana system metrics # Prometheus text-format
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Config
|
|
94
|
+
```bash
|
|
95
|
+
plyrana config show
|
|
96
|
+
plyrana config set-url <URL>
|
|
97
|
+
plyrana config clear
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Auth storage
|
|
101
|
+
|
|
102
|
+
Tokens are stored in `~/.plyrana/config.json` with `0600` perms (best-effort on Windows).
|
|
103
|
+
|
|
104
|
+
Override via env:
|
|
105
|
+
- `PLYRANA_URL=https://hub.example.com`
|
|
106
|
+
- `PLYRANA_TOKEN=eyJ...`
|
|
107
|
+
|
|
108
|
+
Env beats file — great for CI.
|
|
109
|
+
|
|
110
|
+
## Example: CI smoke-test
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export PLYRANA_URL=https://hub.example.com
|
|
114
|
+
export PLYRANA_TOKEN=$PLYRANA_CI_TOKEN
|
|
115
|
+
plyrana system health || exit 1
|
|
116
|
+
plyrana devices list --limit 1 || exit 1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
AGPL-3.0 — see [LICENSE](../LICENSE) in the main repo.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# plyrana-cli — Terminal client for Plyrana
|
|
2
|
+
|
|
3
|
+
Thin command-line client for the Plyrana IoT Device Hub REST API. Great for CI/CD, scripting, batch operations.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install plyrana-cli
|
|
9
|
+
# dev install from source:
|
|
10
|
+
pip install -e ./cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## First run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Point to your hub
|
|
17
|
+
plyrana config set-url https://hub.example.com
|
|
18
|
+
|
|
19
|
+
# Login (password prompt if omitted)
|
|
20
|
+
plyrana auth login --email admin@example.com
|
|
21
|
+
|
|
22
|
+
# Verify
|
|
23
|
+
plyrana auth status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
### Auth
|
|
29
|
+
```bash
|
|
30
|
+
plyrana auth login --email <EMAIL>
|
|
31
|
+
plyrana auth logout
|
|
32
|
+
plyrana auth status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Devices
|
|
36
|
+
```bash
|
|
37
|
+
plyrana devices list [--limit 50] [--type hardware]
|
|
38
|
+
plyrana devices get <UID>
|
|
39
|
+
plyrana devices claim <UID>
|
|
40
|
+
plyrana devices unclaim <UID>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Variables
|
|
44
|
+
```bash
|
|
45
|
+
plyrana variables list [--device <UID>]
|
|
46
|
+
plyrana variables get <UID> <KEY>
|
|
47
|
+
plyrana variables set <UID> <KEY> <VALUE> # auto-cast: 22.5 / true / '{"x":1}'
|
|
48
|
+
plyrana variables history <UID> <KEY> [--limit 20]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Automations
|
|
52
|
+
```bash
|
|
53
|
+
plyrana automations list
|
|
54
|
+
plyrana automations run <ID>
|
|
55
|
+
plyrana automations toggle <ID> --on | --off
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### System
|
|
59
|
+
```bash
|
|
60
|
+
plyrana system health # public, no auth
|
|
61
|
+
plyrana system license # CE / Pro / Enterprise + limits
|
|
62
|
+
plyrana system integrations # MQTT / HA / Prometheus / Grafana status
|
|
63
|
+
plyrana system metrics # Prometheus text-format
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Config
|
|
67
|
+
```bash
|
|
68
|
+
plyrana config show
|
|
69
|
+
plyrana config set-url <URL>
|
|
70
|
+
plyrana config clear
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Auth storage
|
|
74
|
+
|
|
75
|
+
Tokens are stored in `~/.plyrana/config.json` with `0600` perms (best-effort on Windows).
|
|
76
|
+
|
|
77
|
+
Override via env:
|
|
78
|
+
- `PLYRANA_URL=https://hub.example.com`
|
|
79
|
+
- `PLYRANA_TOKEN=eyJ...`
|
|
80
|
+
|
|
81
|
+
Env beats file — great for CI.
|
|
82
|
+
|
|
83
|
+
## Example: CI smoke-test
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export PLYRANA_URL=https://hub.example.com
|
|
87
|
+
export PLYRANA_TOKEN=$PLYRANA_CI_TOKEN
|
|
88
|
+
plyrana system health || exit 1
|
|
89
|
+
plyrana devices list --limit 1 || exit 1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
AGPL-3.0 — see [LICENSE](../LICENSE) in the main repo.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Thin httpx wrapper used by all CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from .config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlyranaAPIError(Exception):
|
|
12
|
+
def __init__(self, status: int, detail: str) -> None:
|
|
13
|
+
super().__init__(f"HTTP {status}: {detail}")
|
|
14
|
+
self.status = status
|
|
15
|
+
self.detail = detail
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def require_auth(cfg: Config) -> None:
|
|
19
|
+
if not cfg.is_authenticated:
|
|
20
|
+
typer.secho(
|
|
21
|
+
"Not authenticated. Run: plyrana auth login --email <you> --password <pw>",
|
|
22
|
+
fg=typer.colors.RED,
|
|
23
|
+
err=True,
|
|
24
|
+
)
|
|
25
|
+
raise typer.Exit(code=2)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def http(cfg: Config, method: str, path: str, **kwargs) -> httpx.Response:
|
|
29
|
+
"""Perform an API call. Path is relative (e.g. '/api/v1/devices')."""
|
|
30
|
+
url = cfg.base_url + path
|
|
31
|
+
headers = kwargs.pop("headers", {}) or {}
|
|
32
|
+
if cfg.token:
|
|
33
|
+
headers["Authorization"] = f"Bearer {cfg.token}"
|
|
34
|
+
timeout = kwargs.pop("timeout", 30.0)
|
|
35
|
+
try:
|
|
36
|
+
response = httpx.request(method, url, headers=headers, timeout=timeout, **kwargs)
|
|
37
|
+
except httpx.RequestError as exc:
|
|
38
|
+
typer.secho(f"Network error: {exc}", fg=typer.colors.RED, err=True)
|
|
39
|
+
raise typer.Exit(code=3) from exc
|
|
40
|
+
|
|
41
|
+
if response.status_code >= 400:
|
|
42
|
+
try:
|
|
43
|
+
detail = response.json()
|
|
44
|
+
except ValueError:
|
|
45
|
+
detail = response.text
|
|
46
|
+
raise PlyranaAPIError(response.status_code, str(detail))
|
|
47
|
+
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get(cfg: Config, path: str, **kwargs) -> httpx.Response:
|
|
52
|
+
return http(cfg, "GET", path, **kwargs)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def post(cfg: Config, path: str, json: dict | None = None, **kwargs) -> httpx.Response:
|
|
56
|
+
return http(cfg, "POST", path, json=json, **kwargs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def put(cfg: Config, path: str, json: dict | None = None, **kwargs) -> httpx.Response:
|
|
60
|
+
return http(cfg, "PUT", path, json=json, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def delete(cfg: Config, path: str, **kwargs) -> httpx.Response:
|
|
64
|
+
return http(cfg, "DELETE", path, **kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Auth subcommand: login, logout, status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from ..client import PlyranaAPIError, get, post
|
|
11
|
+
from ..config import Config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("login")
|
|
19
|
+
def login(
|
|
20
|
+
email: str = typer.Option(..., "--email", "-e", prompt=True, help="Your email address."),
|
|
21
|
+
password: str | None = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--password",
|
|
24
|
+
"-p",
|
|
25
|
+
help="Your password (will be prompted if omitted).",
|
|
26
|
+
),
|
|
27
|
+
url: str | None = typer.Option(
|
|
28
|
+
None, "--url", help="Plyrana base URL (override ~/.plyrana/config.json)."
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Authenticate and save a JWT token to ~/.plyrana/config.json."""
|
|
32
|
+
cfg = Config.load()
|
|
33
|
+
if url:
|
|
34
|
+
cfg.base_url = url.rstrip("/")
|
|
35
|
+
if not password:
|
|
36
|
+
password = getpass.getpass("Password: ")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
response = post(
|
|
40
|
+
cfg,
|
|
41
|
+
"/api/v1/auth/login",
|
|
42
|
+
json={"email": email, "password": password},
|
|
43
|
+
)
|
|
44
|
+
except PlyranaAPIError as exc:
|
|
45
|
+
typer.secho(f"Login failed: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
46
|
+
raise typer.Exit(code=1)
|
|
47
|
+
|
|
48
|
+
data = response.json()
|
|
49
|
+
cfg.token = data.get("access_token")
|
|
50
|
+
if not cfg.token:
|
|
51
|
+
typer.secho("Login response missing 'access_token'", fg=typer.colors.RED, err=True)
|
|
52
|
+
raise typer.Exit(code=1)
|
|
53
|
+
cfg.save()
|
|
54
|
+
console.print(f"[green]Logged in[/] as [bold]{email}[/] at [dim]{cfg.base_url}[/]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("logout")
|
|
58
|
+
def logout() -> None:
|
|
59
|
+
"""Remove the saved token."""
|
|
60
|
+
cfg = Config.load()
|
|
61
|
+
cfg.clear()
|
|
62
|
+
console.print("[green]Logged out[/] — ~/.plyrana/config.json removed.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("status")
|
|
66
|
+
def status() -> None:
|
|
67
|
+
"""Show current login status and identity."""
|
|
68
|
+
cfg = Config.load()
|
|
69
|
+
console.print(f"[bold]Base URL:[/] {cfg.base_url}")
|
|
70
|
+
if not cfg.is_authenticated:
|
|
71
|
+
console.print("[yellow]Not authenticated.[/]")
|
|
72
|
+
console.print("Run: [cyan]plyrana auth login --email <you>[/]")
|
|
73
|
+
raise typer.Exit(code=0)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = get(cfg, "/api/v1/users/me")
|
|
77
|
+
except PlyranaAPIError as exc:
|
|
78
|
+
typer.secho(f"Auth check failed: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
79
|
+
raise typer.Exit(code=1)
|
|
80
|
+
|
|
81
|
+
me = response.json()
|
|
82
|
+
console.print(
|
|
83
|
+
f"[green]Authenticated[/] as [bold]{me.get('email')}[/] (role: {me.get('role')})"
|
|
84
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Automations subcommand: list / run / toggle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..client import PlyranaAPIError, get, post, put, require_auth
|
|
10
|
+
from ..config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def list_automations() -> None:
|
|
19
|
+
"""List all automations in your org."""
|
|
20
|
+
cfg = Config.load()
|
|
21
|
+
require_auth(cfg)
|
|
22
|
+
try:
|
|
23
|
+
response = get(cfg, "/api/v1/automations")
|
|
24
|
+
except PlyranaAPIError as exc:
|
|
25
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
26
|
+
raise typer.Exit(code=1)
|
|
27
|
+
|
|
28
|
+
data = response.json()
|
|
29
|
+
automations = data if isinstance(data, list) else data.get("items", [])
|
|
30
|
+
|
|
31
|
+
if not automations:
|
|
32
|
+
console.print("[dim]No automations found.[/]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
table = Table(title=f"Automations ({len(automations)})")
|
|
36
|
+
table.add_column("ID", style="cyan")
|
|
37
|
+
table.add_column("Name")
|
|
38
|
+
table.add_column("Trigger", style="magenta")
|
|
39
|
+
table.add_column("Enabled")
|
|
40
|
+
table.add_column("Last run", style="dim")
|
|
41
|
+
for auto in automations:
|
|
42
|
+
table.add_row(
|
|
43
|
+
str(auto.get("id", "-")),
|
|
44
|
+
auto.get("name") or "-",
|
|
45
|
+
auto.get("trigger_type", "-"),
|
|
46
|
+
"[green]yes[/]" if auto.get("enabled") else "[red]no[/]",
|
|
47
|
+
auto.get("last_triggered_at") or "-",
|
|
48
|
+
)
|
|
49
|
+
console.print(table)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("run")
|
|
53
|
+
def run_automation(
|
|
54
|
+
automation_id: int = typer.Argument(..., help="Automation ID."),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Manually trigger an automation (runs actions once, bypassing trigger)."""
|
|
57
|
+
cfg = Config.load()
|
|
58
|
+
require_auth(cfg)
|
|
59
|
+
try:
|
|
60
|
+
response = post(cfg, f"/api/v1/automations/{automation_id}/run")
|
|
61
|
+
except PlyranaAPIError as exc:
|
|
62
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
console.print(f"[green]Triggered[/] automation #{automation_id}")
|
|
65
|
+
console.print_json(data=response.json())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("toggle")
|
|
69
|
+
def toggle_automation(
|
|
70
|
+
automation_id: int = typer.Argument(..., help="Automation ID."),
|
|
71
|
+
enabled: bool = typer.Option(True, "--on/--off", help="Turn automation on or off."),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Enable or disable an automation."""
|
|
74
|
+
cfg = Config.load()
|
|
75
|
+
require_auth(cfg)
|
|
76
|
+
try:
|
|
77
|
+
response = put(
|
|
78
|
+
cfg,
|
|
79
|
+
f"/api/v1/automations/{automation_id}",
|
|
80
|
+
json={"enabled": enabled},
|
|
81
|
+
)
|
|
82
|
+
except PlyranaAPIError as exc:
|
|
83
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
|
|
86
|
+
state = "on" if enabled else "off"
|
|
87
|
+
console.print(f"[green]Automation #{automation_id} is now {state}[/]")
|
|
88
|
+
console.print_json(data=response.json())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Config subcommand: show / set-url / clear."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ..config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("show")
|
|
16
|
+
def show() -> None:
|
|
17
|
+
"""Show current CLI configuration."""
|
|
18
|
+
cfg = Config.load()
|
|
19
|
+
console.print(f"[bold]Base URL:[/] {cfg.base_url}")
|
|
20
|
+
if cfg.token:
|
|
21
|
+
console.print(f"[bold]Token:[/] {cfg.token[:20]}... [dim](stored)[/]")
|
|
22
|
+
else:
|
|
23
|
+
console.print("[yellow]No token[/] — run 'plyrana auth login'.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("set-url")
|
|
27
|
+
def set_url(url: str = typer.Argument(..., help="New base URL.")) -> None:
|
|
28
|
+
"""Change the Plyrana base URL."""
|
|
29
|
+
cfg = Config.load()
|
|
30
|
+
cfg.base_url = url.rstrip("/")
|
|
31
|
+
cfg.save()
|
|
32
|
+
console.print(f"[green]Base URL set to[/] {cfg.base_url}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("clear")
|
|
36
|
+
def clear() -> None:
|
|
37
|
+
"""Delete the config file (token + URL)."""
|
|
38
|
+
cfg = Config.load()
|
|
39
|
+
cfg.clear()
|
|
40
|
+
console.print("[green]Config cleared[/] — ~/.plyrana/config.json removed.")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Devices subcommand: list / get / claim / unclaim."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..client import PlyranaAPIError, delete, get, post, require_auth
|
|
10
|
+
from ..config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def list_devices(
|
|
19
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Max rows to show."),
|
|
20
|
+
device_type: str | None = typer.Option(None, "--type", help="Filter by device type."),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""List all devices in your org."""
|
|
23
|
+
cfg = Config.load()
|
|
24
|
+
require_auth(cfg)
|
|
25
|
+
|
|
26
|
+
params = {"limit": limit}
|
|
27
|
+
if device_type:
|
|
28
|
+
params["device_type"] = device_type
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
response = get(cfg, "/api/v1/devices", params=params)
|
|
32
|
+
except PlyranaAPIError as exc:
|
|
33
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
34
|
+
raise typer.Exit(code=1)
|
|
35
|
+
|
|
36
|
+
data = response.json()
|
|
37
|
+
devices = data if isinstance(data, list) else data.get("items", [])
|
|
38
|
+
|
|
39
|
+
if not devices:
|
|
40
|
+
console.print("[dim]No devices found.[/]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
table = Table(title=f"Devices ({len(devices)})")
|
|
44
|
+
table.add_column("UID", style="cyan", no_wrap=True)
|
|
45
|
+
table.add_column("Name")
|
|
46
|
+
table.add_column("Type", style="magenta")
|
|
47
|
+
table.add_column("Status")
|
|
48
|
+
table.add_column("Last seen", style="dim")
|
|
49
|
+
|
|
50
|
+
for dev in devices[:limit]:
|
|
51
|
+
table.add_row(
|
|
52
|
+
dev.get("uid", "-"),
|
|
53
|
+
dev.get("name") or "-",
|
|
54
|
+
dev.get("device_type", "-"),
|
|
55
|
+
"[green]online[/]" if dev.get("online") else "[red]offline[/]",
|
|
56
|
+
dev.get("last_seen_at") or "-",
|
|
57
|
+
)
|
|
58
|
+
console.print(table)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("get")
|
|
62
|
+
def get_device(uid: str = typer.Argument(..., help="Device UID.")) -> None:
|
|
63
|
+
"""Show full details for a device."""
|
|
64
|
+
cfg = Config.load()
|
|
65
|
+
require_auth(cfg)
|
|
66
|
+
try:
|
|
67
|
+
response = get(cfg, f"/api/v1/devices/{uid}")
|
|
68
|
+
except PlyranaAPIError as exc:
|
|
69
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
70
|
+
raise typer.Exit(code=1)
|
|
71
|
+
|
|
72
|
+
console.print_json(data=response.json())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("claim")
|
|
76
|
+
def claim_device(uid: str = typer.Argument(..., help="Device UID to claim.")) -> None:
|
|
77
|
+
"""Claim an unclaimed device for your organization."""
|
|
78
|
+
cfg = Config.load()
|
|
79
|
+
require_auth(cfg)
|
|
80
|
+
try:
|
|
81
|
+
response = post(cfg, f"/api/v1/devices/{uid}/claim")
|
|
82
|
+
except PlyranaAPIError as exc:
|
|
83
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
console.print(f"[green]Claimed[/] device [bold]{uid}[/]")
|
|
86
|
+
console.print_json(data=response.json())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("unclaim")
|
|
90
|
+
def unclaim_device(uid: str = typer.Argument(..., help="Device UID to unclaim.")) -> None:
|
|
91
|
+
"""Release a device back to the pool."""
|
|
92
|
+
cfg = Config.load()
|
|
93
|
+
require_auth(cfg)
|
|
94
|
+
if not typer.confirm(f"Really unclaim device {uid}?"):
|
|
95
|
+
raise typer.Exit(code=0)
|
|
96
|
+
try:
|
|
97
|
+
post(cfg, f"/api/v1/devices/{uid}/unclaim")
|
|
98
|
+
except PlyranaAPIError as exc:
|
|
99
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
100
|
+
raise typer.Exit(code=1)
|
|
101
|
+
console.print(f"[green]Unclaimed[/] device [bold]{uid}[/]")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""System subcommand: health / license / integrations / metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..client import PlyranaAPIError, get, require_auth
|
|
10
|
+
from ..config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("health")
|
|
18
|
+
def health() -> None:
|
|
19
|
+
"""Public backend health check (no auth)."""
|
|
20
|
+
cfg = Config.load()
|
|
21
|
+
try:
|
|
22
|
+
response = get(cfg, "/health")
|
|
23
|
+
except PlyranaAPIError as exc:
|
|
24
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
25
|
+
raise typer.Exit(code=1)
|
|
26
|
+
data = response.json()
|
|
27
|
+
status = data.get("status", "unknown")
|
|
28
|
+
version = data.get("version", "?")
|
|
29
|
+
color = "green" if status == "ok" else "red"
|
|
30
|
+
console.print(f"[{color}]{status.upper()}[/] (version {version}) @ {cfg.base_url}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("license")
|
|
34
|
+
def license_info() -> None:
|
|
35
|
+
"""Show current license (CE / Pro / Enterprise)."""
|
|
36
|
+
cfg = Config.load()
|
|
37
|
+
require_auth(cfg)
|
|
38
|
+
try:
|
|
39
|
+
response = get(cfg, "/api/v1/license")
|
|
40
|
+
except PlyranaAPIError as exc:
|
|
41
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
lic = response.json()
|
|
45
|
+
table = Table(title="License")
|
|
46
|
+
table.add_column("Field", style="cyan")
|
|
47
|
+
table.add_column("Value")
|
|
48
|
+
|
|
49
|
+
for key in ("license_id", "issued_to", "edition", "expires_at", "signed", "valid"):
|
|
50
|
+
table.add_row(key, str(lic.get(key, "-")))
|
|
51
|
+
|
|
52
|
+
limits = lic.get("limits", {})
|
|
53
|
+
for key, value in limits.items():
|
|
54
|
+
display = "∞" if value == -1 else str(value)
|
|
55
|
+
table.add_row(f"limits.{key}", display)
|
|
56
|
+
|
|
57
|
+
console.print(table)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("integrations")
|
|
61
|
+
def integrations_status() -> None:
|
|
62
|
+
"""Aggregated status of MQTT / HA / Prometheus / Grafana integrations."""
|
|
63
|
+
cfg = Config.load()
|
|
64
|
+
require_auth(cfg)
|
|
65
|
+
try:
|
|
66
|
+
response = get(cfg, "/api/v1/integrations/status")
|
|
67
|
+
except PlyranaAPIError as exc:
|
|
68
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
console.print_json(data=response.json())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("metrics")
|
|
74
|
+
def metrics() -> None:
|
|
75
|
+
"""Fetch raw Prometheus metrics output (text/plain)."""
|
|
76
|
+
cfg = Config.load()
|
|
77
|
+
try:
|
|
78
|
+
response = get(cfg, "/metrics")
|
|
79
|
+
except PlyranaAPIError as exc:
|
|
80
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
console.print(response.text)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Variables subcommand: list / get / set / history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_lib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ..client import PlyranaAPIError, get, post, put, require_auth
|
|
13
|
+
from ..config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _coerce_value(raw: str) -> Any:
|
|
21
|
+
"""Coerce a CLI string to int/float/bool/json/string."""
|
|
22
|
+
lower = raw.lower()
|
|
23
|
+
if lower in {"true", "false"}:
|
|
24
|
+
return lower == "true"
|
|
25
|
+
try:
|
|
26
|
+
return int(raw)
|
|
27
|
+
except ValueError:
|
|
28
|
+
pass
|
|
29
|
+
try:
|
|
30
|
+
return float(raw)
|
|
31
|
+
except ValueError:
|
|
32
|
+
pass
|
|
33
|
+
if raw.startswith(("{", "[")):
|
|
34
|
+
try:
|
|
35
|
+
return json_lib.loads(raw)
|
|
36
|
+
except json_lib.JSONDecodeError:
|
|
37
|
+
pass
|
|
38
|
+
return raw
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
def list_variables(
|
|
43
|
+
device_uid: str | None = typer.Option(None, "--device", "-d", help="Filter by device UID."),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""List variable definitions (optionally per device)."""
|
|
46
|
+
cfg = Config.load()
|
|
47
|
+
require_auth(cfg)
|
|
48
|
+
if device_uid:
|
|
49
|
+
path = f"/api/v1/variables/device/{device_uid}"
|
|
50
|
+
else:
|
|
51
|
+
path = "/api/v1/variables/defs"
|
|
52
|
+
try:
|
|
53
|
+
response = get(cfg, path)
|
|
54
|
+
except PlyranaAPIError as exc:
|
|
55
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
58
|
+
data = response.json()
|
|
59
|
+
variables = data if isinstance(data, list) else data.get("items", [])
|
|
60
|
+
|
|
61
|
+
if not variables:
|
|
62
|
+
console.print("[dim]No variables found.[/]")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
table = Table(title=f"Variables ({len(variables)})")
|
|
66
|
+
table.add_column("Key", style="cyan")
|
|
67
|
+
table.add_column("Name")
|
|
68
|
+
table.add_column("Type", style="magenta")
|
|
69
|
+
table.add_column("Unit", style="dim")
|
|
70
|
+
for var in variables:
|
|
71
|
+
table.add_row(
|
|
72
|
+
str(var.get("key", "-")),
|
|
73
|
+
var.get("name") or "-",
|
|
74
|
+
var.get("var_type", "-"),
|
|
75
|
+
var.get("unit") or "-",
|
|
76
|
+
)
|
|
77
|
+
console.print(table)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command("get")
|
|
81
|
+
def get_variable(
|
|
82
|
+
device_uid: str = typer.Argument(..., help="Device UID."),
|
|
83
|
+
key: str = typer.Argument(..., help="Variable key."),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Read the current value of a variable."""
|
|
86
|
+
cfg = Config.load()
|
|
87
|
+
require_auth(cfg)
|
|
88
|
+
try:
|
|
89
|
+
response = get(cfg, f"/api/v1/devices/{device_uid}/variables/{key}")
|
|
90
|
+
except PlyranaAPIError as exc:
|
|
91
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
console.print_json(data=response.json())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("set")
|
|
97
|
+
def set_variable(
|
|
98
|
+
device_uid: str = typer.Argument(..., help="Device UID."),
|
|
99
|
+
key: str = typer.Argument(..., help="Variable key."),
|
|
100
|
+
value: str = typer.Argument(..., help="Value (auto-cast bool/int/float/JSON)."),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Write a new value to a variable."""
|
|
103
|
+
cfg = Config.load()
|
|
104
|
+
require_auth(cfg)
|
|
105
|
+
payload = {"value": _coerce_value(value)}
|
|
106
|
+
try:
|
|
107
|
+
response = put(cfg, f"/api/v1/devices/{device_uid}/variables/{key}", json=payload)
|
|
108
|
+
except PlyranaAPIError as exc:
|
|
109
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
console.print(f"[green]Set[/] {device_uid}.{key} = [bold]{payload['value']!r}[/]")
|
|
112
|
+
console.print_json(data=response.json())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command("history")
|
|
116
|
+
def history(
|
|
117
|
+
device_uid: str = typer.Argument(..., help="Device UID."),
|
|
118
|
+
key: str = typer.Argument(..., help="Variable key."),
|
|
119
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Max samples."),
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Show recent history for a variable."""
|
|
122
|
+
cfg = Config.load()
|
|
123
|
+
require_auth(cfg)
|
|
124
|
+
try:
|
|
125
|
+
response = get(
|
|
126
|
+
cfg,
|
|
127
|
+
f"/api/v1/devices/{device_uid}/variables/{key}/history",
|
|
128
|
+
params={"limit": limit},
|
|
129
|
+
)
|
|
130
|
+
except PlyranaAPIError as exc:
|
|
131
|
+
typer.secho(f"Error: {exc.detail}", fg=typer.colors.RED, err=True)
|
|
132
|
+
raise typer.Exit(code=1)
|
|
133
|
+
|
|
134
|
+
samples = response.json()
|
|
135
|
+
if not samples:
|
|
136
|
+
console.print("[dim]No history samples.[/]")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
table = Table(title=f"{device_uid}.{key} history")
|
|
140
|
+
table.add_column("Timestamp", style="dim")
|
|
141
|
+
table.add_column("Value", style="cyan")
|
|
142
|
+
for sample in samples[:limit]:
|
|
143
|
+
table.add_row(
|
|
144
|
+
str(sample.get("timestamp", "-")),
|
|
145
|
+
str(sample.get("value", "-")),
|
|
146
|
+
)
|
|
147
|
+
console.print(table)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Plyrana CLI config: load/save token + base URL from ~/.plyrana/config.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = "http://localhost:8000"
|
|
11
|
+
CONFIG_DIR = Path(os.path.expanduser("~")) / ".plyrana"
|
|
12
|
+
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
def __init__(self, base_url: str = DEFAULT_BASE_URL, token: str | None = None) -> None:
|
|
17
|
+
self.base_url = base_url.rstrip("/")
|
|
18
|
+
self.token = token
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def load(cls) -> "Config":
|
|
22
|
+
"""Load from file + env-var overrides. Env beats file."""
|
|
23
|
+
data: dict = {}
|
|
24
|
+
if CONFIG_PATH.exists():
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
27
|
+
except (OSError, json.JSONDecodeError):
|
|
28
|
+
data = {}
|
|
29
|
+
|
|
30
|
+
base_url = os.environ.get("PLYRANA_URL") or data.get("base_url") or DEFAULT_BASE_URL
|
|
31
|
+
token = os.environ.get("PLYRANA_TOKEN") or data.get("token")
|
|
32
|
+
return cls(base_url=base_url, token=token)
|
|
33
|
+
|
|
34
|
+
def save(self) -> None:
|
|
35
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
CONFIG_PATH.write_text(
|
|
37
|
+
json.dumps({"base_url": self.base_url, "token": self.token}, indent=2),
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
# Best-effort: restrict to user only (0600). No-op on Windows without admin.
|
|
41
|
+
try:
|
|
42
|
+
os.chmod(CONFIG_PATH, 0o600)
|
|
43
|
+
except OSError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def clear(self) -> None:
|
|
47
|
+
if CONFIG_PATH.exists():
|
|
48
|
+
CONFIG_PATH.unlink()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_authenticated(self) -> bool:
|
|
52
|
+
return bool(self.token)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""plyrana CLI entry point — `plyrana <subcommand>`.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
auth login / logout / status
|
|
5
|
+
devices list / get / claim / unclaim
|
|
6
|
+
variables list / get / set / history
|
|
7
|
+
automations list / run / toggle
|
|
8
|
+
system health / license / integrations / metrics
|
|
9
|
+
config show / set-url / clear
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .commands import automations, auth, config_cmd, devices, system, variables
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="plyrana",
|
|
22
|
+
help="Plyrana CLI — manage a Plyrana IoT Device Hub from your terminal.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app.add_typer(auth.app, name="auth", help="Authentication: login / logout / status.")
|
|
28
|
+
app.add_typer(devices.app, name="devices", help="List, inspect, claim and unclaim devices.")
|
|
29
|
+
app.add_typer(variables.app, name="variables", help="Read and write device variables.")
|
|
30
|
+
app.add_typer(automations.app, name="automations", help="List, run, toggle automations.")
|
|
31
|
+
app.add_typer(system.app, name="system", help="Health, license and integration status.")
|
|
32
|
+
app.add_typer(config_cmd.app, name="config", help="CLI configuration (base URL, token).")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback(invoke_without_command=True)
|
|
36
|
+
def root(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
version: bool = typer.Option(False, "--version", "-V", help="Print version and exit."),
|
|
39
|
+
) -> None:
|
|
40
|
+
if version:
|
|
41
|
+
typer.echo(f"plyrana {__version__}")
|
|
42
|
+
raise typer.Exit(code=0)
|
|
43
|
+
if ctx.invoked_subcommand is None:
|
|
44
|
+
typer.echo(ctx.get_help())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
app()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plyrana-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plyrana CLI — manage any self-hosted Plyrana instance from your terminal. Auth, devices, variables, automations, system.
|
|
5
|
+
Author-email: Plyrana <dev@plyrana.com>
|
|
6
|
+
License: AGPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/plyrana/core
|
|
8
|
+
Project-URL: Documentation, https://github.com/plyrana/core/tree/main/cli
|
|
9
|
+
Project-URL: Repository, https://github.com/plyrana/core
|
|
10
|
+
Project-URL: Issues, https://github.com/plyrana/core/issues
|
|
11
|
+
Keywords: plyrana,iot,cli,devops,automation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: httpx<0.29.0,>=0.23.0
|
|
25
|
+
Requires-Dist: typer>=0.9.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
|
|
28
|
+
# plyrana-cli — Terminal client for Plyrana
|
|
29
|
+
|
|
30
|
+
Thin command-line client for the Plyrana IoT Device Hub REST API. Great for CI/CD, scripting, batch operations.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install plyrana-cli
|
|
36
|
+
# dev install from source:
|
|
37
|
+
pip install -e ./cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## First run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Point to your hub
|
|
44
|
+
plyrana config set-url https://hub.example.com
|
|
45
|
+
|
|
46
|
+
# Login (password prompt if omitted)
|
|
47
|
+
plyrana auth login --email admin@example.com
|
|
48
|
+
|
|
49
|
+
# Verify
|
|
50
|
+
plyrana auth status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### Auth
|
|
56
|
+
```bash
|
|
57
|
+
plyrana auth login --email <EMAIL>
|
|
58
|
+
plyrana auth logout
|
|
59
|
+
plyrana auth status
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Devices
|
|
63
|
+
```bash
|
|
64
|
+
plyrana devices list [--limit 50] [--type hardware]
|
|
65
|
+
plyrana devices get <UID>
|
|
66
|
+
plyrana devices claim <UID>
|
|
67
|
+
plyrana devices unclaim <UID>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Variables
|
|
71
|
+
```bash
|
|
72
|
+
plyrana variables list [--device <UID>]
|
|
73
|
+
plyrana variables get <UID> <KEY>
|
|
74
|
+
plyrana variables set <UID> <KEY> <VALUE> # auto-cast: 22.5 / true / '{"x":1}'
|
|
75
|
+
plyrana variables history <UID> <KEY> [--limit 20]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Automations
|
|
79
|
+
```bash
|
|
80
|
+
plyrana automations list
|
|
81
|
+
plyrana automations run <ID>
|
|
82
|
+
plyrana automations toggle <ID> --on | --off
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### System
|
|
86
|
+
```bash
|
|
87
|
+
plyrana system health # public, no auth
|
|
88
|
+
plyrana system license # CE / Pro / Enterprise + limits
|
|
89
|
+
plyrana system integrations # MQTT / HA / Prometheus / Grafana status
|
|
90
|
+
plyrana system metrics # Prometheus text-format
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Config
|
|
94
|
+
```bash
|
|
95
|
+
plyrana config show
|
|
96
|
+
plyrana config set-url <URL>
|
|
97
|
+
plyrana config clear
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Auth storage
|
|
101
|
+
|
|
102
|
+
Tokens are stored in `~/.plyrana/config.json` with `0600` perms (best-effort on Windows).
|
|
103
|
+
|
|
104
|
+
Override via env:
|
|
105
|
+
- `PLYRANA_URL=https://hub.example.com`
|
|
106
|
+
- `PLYRANA_TOKEN=eyJ...`
|
|
107
|
+
|
|
108
|
+
Env beats file — great for CI.
|
|
109
|
+
|
|
110
|
+
## Example: CI smoke-test
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export PLYRANA_URL=https://hub.example.com
|
|
114
|
+
export PLYRANA_TOKEN=$PLYRANA_CI_TOKEN
|
|
115
|
+
plyrana system health || exit 1
|
|
116
|
+
plyrana devices list --limit 1 || exit 1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
AGPL-3.0 — see [LICENSE](../LICENSE) in the main repo.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
plyrana_cli/__init__.py
|
|
4
|
+
plyrana_cli/client.py
|
|
5
|
+
plyrana_cli/config.py
|
|
6
|
+
plyrana_cli/main.py
|
|
7
|
+
plyrana_cli.egg-info/PKG-INFO
|
|
8
|
+
plyrana_cli.egg-info/SOURCES.txt
|
|
9
|
+
plyrana_cli.egg-info/dependency_links.txt
|
|
10
|
+
plyrana_cli.egg-info/entry_points.txt
|
|
11
|
+
plyrana_cli.egg-info/requires.txt
|
|
12
|
+
plyrana_cli.egg-info/top_level.txt
|
|
13
|
+
plyrana_cli/commands/__init__.py
|
|
14
|
+
plyrana_cli/commands/auth.py
|
|
15
|
+
plyrana_cli/commands/automations.py
|
|
16
|
+
plyrana_cli/commands/config_cmd.py
|
|
17
|
+
plyrana_cli/commands/devices.py
|
|
18
|
+
plyrana_cli/commands/system.py
|
|
19
|
+
plyrana_cli/commands/variables.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plyrana_cli
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "plyrana-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Plyrana CLI — manage any self-hosted Plyrana instance from your terminal. Auth, devices, variables, automations, system."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "AGPL-3.0" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Plyrana", email = "dev@plyrana.com" }
|
|
9
|
+
]
|
|
10
|
+
keywords = ["plyrana", "iot", "cli", "devops", "automation"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: System Administrators",
|
|
16
|
+
"License :: OSI Approved :: GNU Affero General Public License v3",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: System :: Systems Administration",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.23.0,<0.29.0",
|
|
26
|
+
"typer>=0.9.0",
|
|
27
|
+
"rich>=13.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
plyrana = "plyrana_cli.main:app"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/plyrana/core"
|
|
35
|
+
Documentation = "https://github.com/plyrana/core/tree/main/cli"
|
|
36
|
+
Repository = "https://github.com/plyrana/core"
|
|
37
|
+
Issues = "https://github.com/plyrana/core/issues"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
include = ["plyrana_cli*"]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools>=68", "wheel"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|