agenteye 0.1.0b1__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.
- agenteye-0.1.0b1.dist-info/METADATA +118 -0
- agenteye-0.1.0b1.dist-info/RECORD +24 -0
- agenteye-0.1.0b1.dist-info/WHEEL +5 -0
- agenteye-0.1.0b1.dist-info/entry_points.txt +2 -0
- agenteye-0.1.0b1.dist-info/top_level.txt +1 -0
- agenteye_cli/__init__.py +10 -0
- agenteye_cli/__main__.py +4 -0
- agenteye_cli/_context.py +50 -0
- agenteye_cli/_version.py +1 -0
- agenteye_cli/app.py +92 -0
- agenteye_cli/auth.py +134 -0
- agenteye_cli/client.py +315 -0
- agenteye_cli/commands/__init__.py +0 -0
- agenteye_cli/commands/auth_cmds.py +87 -0
- agenteye_cli/commands/env_cmds.py +35 -0
- agenteye_cli/commands/evals_cmds.py +78 -0
- agenteye_cli/commands/events_cmds.py +82 -0
- agenteye_cli/commands/jobs_cmds.py +40 -0
- agenteye_cli/commands/sessions_cmds.py +161 -0
- agenteye_cli/config.py +96 -0
- agenteye_cli/dates.py +53 -0
- agenteye_cli/errors.py +66 -0
- agenteye_cli/models.py +124 -0
- agenteye_cli/output.py +91 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agenteye
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Command-line client for the AgentEye dashboard API
|
|
5
|
+
Author-email: Exosphere <support@exosphere.host>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://exosphere.host
|
|
8
|
+
Project-URL: Documentation, https://github.com/agenteye-enterprise/releases
|
|
9
|
+
Keywords: agents,observability,ai,monitoring,agenteye,cli
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Requires-Dist: rich>=13
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# AgentEye CLI (`agenteye`)
|
|
28
|
+
|
|
29
|
+
A command-line client for the AgentEye **dashboard** API. It lets a developer —
|
|
30
|
+
or the coding agent working alongside them — authenticate and query agent
|
|
31
|
+
sessions, event logs, and evaluations from the terminal, with a `--json` flag on
|
|
32
|
+
every command for scripting.
|
|
33
|
+
|
|
34
|
+
> This is the **`agenteye` CLI**, distinct from the collector daemon
|
|
35
|
+
> (`agenteye-collector`). The PyPI package and the installed command are both
|
|
36
|
+
> `agenteye`.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install agenteye # recommended (isolated)
|
|
42
|
+
# or: uv tool install agenteye / pip install agenteye
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Install isolated (pipx / uv tool) — the AgentEye Python SDK shares the `agenteye`
|
|
46
|
+
distribution name, so isolation avoids a clash in a shared virtualenv.
|
|
47
|
+
|
|
48
|
+
For development in this repo:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd cli
|
|
52
|
+
uv sync --extra dev
|
|
53
|
+
uv run agenteye --help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
The CLI talks to the dashboard (default `http://localhost:3000`) and logs in with
|
|
59
|
+
an emailed one-time code:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
agenteye login --email you@example.com
|
|
63
|
+
# enter the 6-digit code; the session is stored in ~/.agenteye/cli.json (mode 0600)
|
|
64
|
+
agenteye whoami
|
|
65
|
+
agenteye logout
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Sessions expire (24h by default); re-run `agenteye login` when prompted.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
agenteye sessions [--since 24h] [--status error] [--all]
|
|
74
|
+
agenteye events --session-id <id> [--event-type tool_use,tool_result] [--all]
|
|
75
|
+
agenteye logs ... # alias of events
|
|
76
|
+
agenteye evals --score helpfulness:0.5..0.8 --latest-per-session
|
|
77
|
+
agenteye session show <id>
|
|
78
|
+
agenteye session export <id> -o out.json
|
|
79
|
+
agenteye re-evaluate <id>
|
|
80
|
+
agenteye jobs # in-flight evaluation queue
|
|
81
|
+
agenteye environments [--source events|evals]
|
|
82
|
+
agenteye version # print the CLI version
|
|
83
|
+
agenteye help # show top-level help
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Add `--json` to any command for machine-readable output:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
agenteye events --session-id run-001 --all --json | jq '.events[].payload'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
| Setting | Flag | Env var | Default |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| Dashboard URL | `--base-url` | `AGENTEYE_DASHBOARD_URL` | `http://localhost:3000` |
|
|
97
|
+
| Session token | `--token` | `AGENTEYE_CLI_TOKEN` | from `~/.agenteye/cli.json` |
|
|
98
|
+
| JSON output | `--json` | `AGENTEYE_CLI_JSON` | off |
|
|
99
|
+
|
|
100
|
+
Precedence is **flag > environment variable > config file > default**. The config
|
|
101
|
+
directory honours `AGENTEYE_HOME` (same as the SDK and collector).
|
|
102
|
+
|
|
103
|
+
## Exit codes
|
|
104
|
+
|
|
105
|
+
| Code | Meaning |
|
|
106
|
+
|---|---|
|
|
107
|
+
| 0 | Success |
|
|
108
|
+
| 2 | Usage error (bad arguments) |
|
|
109
|
+
| 3 | Cannot reach the dashboard |
|
|
110
|
+
| 4 | Not logged in / session expired |
|
|
111
|
+
| 5 | Authenticated but missing permission |
|
|
112
|
+
|
|
113
|
+
## Tests
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd cli
|
|
117
|
+
uv run --extra dev pytest
|
|
118
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
agenteye_cli/__init__.py,sha256=eyyZX0uQp1piaMqpjvK32dLQtbmYb1LDWOhjJQlg70Y,377
|
|
2
|
+
agenteye_cli/__main__.py,sha256=GhA_jQK0HPEAxwgfSYLfQz8t5uDWTBxCY5icQ_MuVrw,59
|
|
3
|
+
agenteye_cli/_context.py,sha256=IC2XnFIx6c7NO5LF3O1th9-_gljDaM_qJkt83QUlGTU,1549
|
|
4
|
+
agenteye_cli/_version.py,sha256=2f9m0YyP0UybST_fqXzfDdE3BUiRQVajDfkzhl26WPw,24
|
|
5
|
+
agenteye_cli/app.py,sha256=7cZ3fcEXv3LCoi3JycM0sNsMrPUnMuTGVYJORqTZU4E,2678
|
|
6
|
+
agenteye_cli/auth.py,sha256=s6zP3LCRgKbP36oo0KJtOoeWg_MRPTych8qobPoeaho,4026
|
|
7
|
+
agenteye_cli/client.py,sha256=ND3jFIha6knxV10pdV5iVBvxpzfd87GpFEdfgdE06vE,10004
|
|
8
|
+
agenteye_cli/config.py,sha256=H9-9iAdhja61LB--wwO9xkMpk87ePXXeeHFzaMRBH0A,2989
|
|
9
|
+
agenteye_cli/dates.py,sha256=wCabqz9AgaGwQs6ucs0e_b0MrL_UhcF7hEP-AZN23gs,1616
|
|
10
|
+
agenteye_cli/errors.py,sha256=yUtXlxtPOIt2P8nZuy78ITWEWMEabDiPO496W0Fx-j8,1481
|
|
11
|
+
agenteye_cli/models.py,sha256=NwUOabAayNPvy4bq0S08Xz48o4gN9Cucwb84twdJD2s,3628
|
|
12
|
+
agenteye_cli/output.py,sha256=R0-W2pny8iSYysFxKLhCPsEnNuSJVExfSzzkYOhspcM,2367
|
|
13
|
+
agenteye_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
agenteye_cli/commands/auth_cmds.py,sha256=XP6KKj1XJnC_cPY3-RDNDXFXUQ_9ohUEbr8kVXyVLAk,2841
|
|
15
|
+
agenteye_cli/commands/env_cmds.py,sha256=fG5Q-zBCHAPTwy8gzfidecvAQQWadHYW2pmKjmLG3Pc,975
|
|
16
|
+
agenteye_cli/commands/evals_cmds.py,sha256=Xkm5ebZ8Et4Ds54Oy0OcbrM_i9SMRqcRWfBu-gh52K4,3244
|
|
17
|
+
agenteye_cli/commands/events_cmds.py,sha256=4ThYr3ypJzJm5r3jMgytJXCOb1T4Lbk1i9_22tf22K0,3105
|
|
18
|
+
agenteye_cli/commands/jobs_cmds.py,sha256=KcE2MTuOa2yL2956xQv-yX0-8pepoq1XF7zQTKfRMig,1128
|
|
19
|
+
agenteye_cli/commands/sessions_cmds.py,sha256=gmAO4_I3TYDp82tlZ9YyapGwysH4afv5fZ7JTJkxYds,5885
|
|
20
|
+
agenteye-0.1.0b1.dist-info/METADATA,sha256=JvOOddMyux2a262MirFm1uYEt8TbZvPoQ1Ka51CkXC4,3662
|
|
21
|
+
agenteye-0.1.0b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
22
|
+
agenteye-0.1.0b1.dist-info/entry_points.txt,sha256=lRieI4-qJsfCnYXUW8piSTVY8s7N5D_zHBUmNAqMB4s,50
|
|
23
|
+
agenteye-0.1.0b1.dist-info/top_level.txt,sha256=oc6HWsjPbGZpd-ctGjZd5lq3_4c7HIBW5kfAdIzkbrE,13
|
|
24
|
+
agenteye-0.1.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agenteye_cli
|
agenteye_cli/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""AgentEye CLI — a command-line client for the AgentEye dashboard API.
|
|
2
|
+
|
|
3
|
+
The query layer lives in :mod:`agenteye_cli.client` as pure functions that take a
|
|
4
|
+
``ClientContext`` and return plain dataclasses. They never print and never import
|
|
5
|
+
Typer/Rich, so a future MCP server can wrap them with zero duplication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
agenteye_cli/__main__.py
ADDED
agenteye_cli/_context.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Shared command-layer state and helpers.
|
|
2
|
+
|
|
3
|
+
Kept separate from ``app.py`` so command modules can import these without a
|
|
4
|
+
circular dependency (``app.py`` imports the command modules).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from . import config as cfgmod
|
|
15
|
+
from . import dates as _dates
|
|
16
|
+
from .client import ClientContext
|
|
17
|
+
from .errors import AuthError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AppState:
|
|
22
|
+
json: bool
|
|
23
|
+
base_url: str
|
|
24
|
+
token: Optional[str]
|
|
25
|
+
timeout: float
|
|
26
|
+
config: cfgmod.CliConfig
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_context(state: AppState) -> ClientContext:
|
|
30
|
+
return ClientContext(base_url=state.base_url, token=state.token, timeout=state.timeout)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def require_auth(state: AppState) -> ClientContext:
|
|
34
|
+
"""Return a client context, or raise AuthError if not usably authenticated."""
|
|
35
|
+
if not state.token:
|
|
36
|
+
raise AuthError("Not logged in. Run 'agenteye login'.")
|
|
37
|
+
# Only enforce local expiry when the token came from the stored config; an
|
|
38
|
+
# explicit --token / env override has no known expiry, so trust it.
|
|
39
|
+
if state.token == state.config.session_token and cfgmod.is_expired(state.config):
|
|
40
|
+
raise AuthError("Session expired. Run 'agenteye login'.")
|
|
41
|
+
return build_context(state)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_dates(
|
|
45
|
+
since: Optional[str], ts_from: Optional[str], ts_to: Optional[str]
|
|
46
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
47
|
+
try:
|
|
48
|
+
return _dates.resolve_range(since, ts_from, ts_to)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise click.BadParameter(str(exc))
|
agenteye_cli/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0b1"
|
agenteye_cli/app.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Typer application: global options + command registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from . import config as cfgmod
|
|
10
|
+
from . import output
|
|
11
|
+
from ._context import AppState
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
from .commands import (
|
|
14
|
+
auth_cmds,
|
|
15
|
+
env_cmds,
|
|
16
|
+
evals_cmds,
|
|
17
|
+
events_cmds,
|
|
18
|
+
jobs_cmds,
|
|
19
|
+
sessions_cmds,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
help="Query the AgentEye dashboard — sessions, events, and evaluations — from your terminal.",
|
|
26
|
+
)
|
|
27
|
+
session_app = typer.Typer(no_args_is_help=True, help="Inspect or export a single session.")
|
|
28
|
+
app.add_typer(session_app, name="session")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _version_callback(value: bool) -> None:
|
|
32
|
+
if value:
|
|
33
|
+
print(__version__)
|
|
34
|
+
raise typer.Exit()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.callback()
|
|
38
|
+
def main(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
json_output: bool = typer.Option(
|
|
41
|
+
False, "--json", envvar="AGENTEYE_CLI_JSON", help="Emit JSON to stdout instead of a table."
|
|
42
|
+
),
|
|
43
|
+
base_url: Optional[str] = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--base-url",
|
|
46
|
+
envvar="AGENTEYE_DASHBOARD_URL",
|
|
47
|
+
metavar="URL",
|
|
48
|
+
help="Dashboard base URL (default http://localhost:3000).",
|
|
49
|
+
),
|
|
50
|
+
token: Optional[str] = typer.Option(
|
|
51
|
+
None, "--token", envvar="AGENTEYE_CLI_TOKEN", help="Session token override (for CI/agents)."
|
|
52
|
+
),
|
|
53
|
+
timeout: float = typer.Option(30.0, "--timeout", help="HTTP timeout in seconds."),
|
|
54
|
+
no_color: bool = typer.Option(
|
|
55
|
+
False, "--no-color", envvar="NO_COLOR", help="Disable coloured output."
|
|
56
|
+
),
|
|
57
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress status messages on stderr."),
|
|
58
|
+
version: bool = typer.Option(
|
|
59
|
+
False, "--version", callback=_version_callback, is_eager=True, help="Show version and exit."
|
|
60
|
+
),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Resolve global options into the per-invocation AppState (flag > env > config > default)."""
|
|
63
|
+
cfg = cfgmod.load_config()
|
|
64
|
+
output.configure(no_color=no_color, quiet=quiet)
|
|
65
|
+
ctx.obj = AppState(
|
|
66
|
+
json=json_output,
|
|
67
|
+
base_url=base_url or cfg.base_url or cfgmod.DEFAULT_BASE_URL,
|
|
68
|
+
token=token or cfg.session_token,
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
config=cfg,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def version() -> None:
|
|
76
|
+
"""Show the CLI version."""
|
|
77
|
+
print(__version__)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command("help")
|
|
81
|
+
def help_cmd(ctx: typer.Context) -> None:
|
|
82
|
+
"""Show top-level help and available commands."""
|
|
83
|
+
parent = ctx.parent
|
|
84
|
+
typer.echo((parent or ctx).get_help())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
auth_cmds.register(app)
|
|
88
|
+
events_cmds.register(app)
|
|
89
|
+
evals_cmds.register(app)
|
|
90
|
+
sessions_cmds.register(app, session_app)
|
|
91
|
+
jobs_cmds.register(app)
|
|
92
|
+
env_cmds.register(app)
|
agenteye_cli/auth.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Email-OTP device login against the dashboard.
|
|
2
|
+
|
|
3
|
+
Flow: ``/api/auth/otp/request`` (sends a code) then ``/api/auth/otp/verify``.
|
|
4
|
+
The verify response carries the session token in the ``ae_session`` **Set-Cookie**
|
|
5
|
+
header (not the JSON body), so we read it from ``response.cookies``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from typing import Any, Dict, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .config import CliConfig, save_config
|
|
16
|
+
from .errors import ApiError, AuthError, NetworkError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _iso(dt: datetime) -> str:
|
|
20
|
+
return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _network_error(base_url: str, exc: Exception) -> NetworkError:
|
|
24
|
+
return NetworkError(f"Cannot reach the AgentEye dashboard at {base_url}: {exc}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def request_otp(
|
|
28
|
+
base_url: str,
|
|
29
|
+
email: str,
|
|
30
|
+
*,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Ask the dashboard to email a login code. Always succeeds quietly for a
|
|
35
|
+
valid request (the server returns 200 even for unknown emails)."""
|
|
36
|
+
try:
|
|
37
|
+
with httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, transport=transport) as client:
|
|
38
|
+
response = client.post("/api/auth/otp/request", json={"email": email})
|
|
39
|
+
except httpx.RequestError as exc:
|
|
40
|
+
raise _network_error(base_url, exc)
|
|
41
|
+
if response.status_code >= 400:
|
|
42
|
+
raise ApiError(
|
|
43
|
+
f"Failed to request a login code (HTTP {response.status_code}).",
|
|
44
|
+
status=response.status_code,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def verify_otp(
|
|
49
|
+
base_url: str,
|
|
50
|
+
email: str,
|
|
51
|
+
code: str,
|
|
52
|
+
*,
|
|
53
|
+
timeout: float = 30.0,
|
|
54
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
55
|
+
) -> Tuple[str, int, Dict[str, Any]]:
|
|
56
|
+
"""Exchange the code for a session token. Returns ``(token, expires_in_secs, user)``."""
|
|
57
|
+
try:
|
|
58
|
+
with httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout, transport=transport) as client:
|
|
59
|
+
response = client.post(
|
|
60
|
+
"/api/auth/otp/verify", json={"email": email, "code": code}
|
|
61
|
+
)
|
|
62
|
+
except httpx.RequestError as exc:
|
|
63
|
+
raise _network_error(base_url, exc)
|
|
64
|
+
|
|
65
|
+
if response.status_code == 401:
|
|
66
|
+
raise AuthError("Invalid or expired login code.")
|
|
67
|
+
if response.status_code >= 400:
|
|
68
|
+
raise ApiError(
|
|
69
|
+
f"Login verification failed (HTTP {response.status_code}).",
|
|
70
|
+
status=response.status_code,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
token = response.cookies.get("ae_session")
|
|
74
|
+
if not token:
|
|
75
|
+
raise AuthError("The dashboard did not return a session token.")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
body = response.json()
|
|
79
|
+
except Exception:
|
|
80
|
+
body = {}
|
|
81
|
+
if not isinstance(body, dict):
|
|
82
|
+
body = {}
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
expires_in = int(body.get("expires_in_secs"))
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
expires_in = 86400
|
|
88
|
+
|
|
89
|
+
user = body.get("user") or {}
|
|
90
|
+
if not isinstance(user, dict):
|
|
91
|
+
user = {}
|
|
92
|
+
|
|
93
|
+
return token, expires_in, user
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def persist_session(
|
|
97
|
+
cfg: CliConfig,
|
|
98
|
+
base_url: str,
|
|
99
|
+
token: str,
|
|
100
|
+
expires_in_secs: int,
|
|
101
|
+
user: Dict[str, Any],
|
|
102
|
+
*,
|
|
103
|
+
now: Optional[datetime] = None,
|
|
104
|
+
) -> CliConfig:
|
|
105
|
+
now = now or datetime.now(timezone.utc)
|
|
106
|
+
cfg.base_url = base_url
|
|
107
|
+
cfg.session_token = token
|
|
108
|
+
cfg.expires_at = _iso(now + timedelta(seconds=expires_in_secs))
|
|
109
|
+
cfg.email = (user or {}).get("email") or cfg.email
|
|
110
|
+
cfg.user_id = (user or {}).get("id") or cfg.user_id
|
|
111
|
+
save_config(cfg)
|
|
112
|
+
return cfg
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def logout(
|
|
116
|
+
base_url: str,
|
|
117
|
+
token: Optional[str],
|
|
118
|
+
*,
|
|
119
|
+
timeout: float = 30.0,
|
|
120
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Best-effort server-side session revocation; never raises."""
|
|
123
|
+
if not token:
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
with httpx.Client(
|
|
127
|
+
base_url=base_url.rstrip("/"),
|
|
128
|
+
cookies={"ae_session": token},
|
|
129
|
+
timeout=timeout,
|
|
130
|
+
transport=transport,
|
|
131
|
+
) as client:
|
|
132
|
+
client.post("/api/auth/logout")
|
|
133
|
+
except httpx.RequestError:
|
|
134
|
+
pass
|