cortexdb-cli 0.6.0__py3-none-any.whl → 0.7.0__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.
- cortexdb_cli/__init__.py +1 -1
- cortexdb_cli/client.py +104 -9
- cortexdb_cli/commands/access.py +849 -0
- cortexdb_cli/commands/admin.py +80 -14
- cortexdb_cli/commands/admin_access.py +248 -0
- cortexdb_cli/commands/answer.py +13 -5
- cortexdb_cli/commands/apps.py +129 -0
- cortexdb_cli/commands/audit.py +3 -0
- cortexdb_cli/commands/auth.py +12 -57
- cortexdb_cli/commands/beliefs.py +68 -9
- cortexdb_cli/commands/changes.py +433 -0
- cortexdb_cli/commands/claims.py +2 -0
- cortexdb_cli/commands/code.py +12 -0
- cortexdb_cli/commands/compose.py +14 -7
- cortexdb_cli/commands/conflicts.py +15 -3
- cortexdb_cli/commands/directory.py +225 -0
- cortexdb_cli/commands/entities.py +4 -0
- cortexdb_cli/commands/episodes.py +26 -4
- cortexdb_cli/commands/erase.py +302 -0
- cortexdb_cli/commands/experience.py +168 -41
- cortexdb_cli/commands/export_cmd.py +20 -5
- cortexdb_cli/commands/facts.py +43 -15
- cortexdb_cli/commands/forget.py +249 -43
- cortexdb_cli/commands/import_cmd.py +2 -0
- cortexdb_cli/commands/interactive.py +107 -42
- cortexdb_cli/commands/onboard.py +59 -11
- cortexdb_cli/commands/policy.py +3 -0
- cortexdb_cli/commands/publish.py +90 -0
- cortexdb_cli/commands/recall.py +19 -5
- cortexdb_cli/commands/rollups.py +153 -0
- cortexdb_cli/commands/scopes.py +72 -21
- cortexdb_cli/commands/search.py +24 -7
- cortexdb_cli/commands/tokens.py +166 -0
- cortexdb_cli/commands/understanding.py +24 -7
- cortexdb_cli/commands/watch.py +249 -0
- cortexdb_cli/common.py +347 -0
- cortexdb_cli/errors.py +605 -50
- cortexdb_cli/main.py +62 -1
- cortexdb_cli/output.py +124 -8
- cortexdb_cli-0.7.0.dist-info/METADATA +378 -0
- cortexdb_cli-0.7.0.dist-info/RECORD +51 -0
- {cortexdb_cli-0.6.0.dist-info → cortexdb_cli-0.7.0.dist-info}/WHEEL +1 -1
- cortexdb_cli-0.6.0.dist-info/METADATA +0 -138
- cortexdb_cli-0.6.0.dist-info/RECORD +0 -40
- {cortexdb_cli-0.6.0.dist-info → cortexdb_cli-0.7.0.dist-info}/entry_points.txt +0 -0
cortexdb_cli/__init__.py
CHANGED
cortexdb_cli/client.py
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Thin wrapper around httpx that reads credentials + endpoint from the CLI
|
|
4
4
|
context and stamps every request with both `Authorization: Bearer ...` and
|
|
5
|
-
`X-Cortex-Actor: ...`. The actor header is mandatory on v1
|
|
6
|
-
|
|
5
|
+
`X-Cortex-Actor: ...`. The actor header is mandatory on v1 for a token
|
|
6
|
+
whose issuer does not map its subject - missing it returns 401
|
|
7
|
+
ACTOR_MISMATCH no matter how valid the token is.
|
|
8
|
+
|
|
9
|
+
`X-Cortex-Break-Glass` is added when the context carries a break-glass
|
|
10
|
+
reason (`--break-glass`, see `cortexdb_cli.common`): the operator key's
|
|
11
|
+
header for a data read the deployment's or the tenant's
|
|
12
|
+
`CORTEX_OPERATOR_DATA_READS=deny` refuses without it.
|
|
7
13
|
|
|
8
14
|
We deliberately don't depend on the cortexdbai SDK: keeping the CLI a
|
|
9
15
|
straight HTTP client makes the install fast and lets us pin httpx
|
|
@@ -14,13 +20,72 @@ from __future__ import annotations
|
|
|
14
20
|
|
|
15
21
|
from contextlib import contextmanager
|
|
16
22
|
from typing import Any, Generator
|
|
23
|
+
from urllib.parse import quote
|
|
17
24
|
|
|
25
|
+
import click
|
|
18
26
|
import httpx
|
|
19
27
|
|
|
20
28
|
from cortexdb_cli import __version__
|
|
21
29
|
|
|
22
30
|
DEFAULT_ENDPOINT = "https://api-v1.cortexdb.ai"
|
|
23
31
|
|
|
32
|
+
BREAK_GLASS_HEADER = "X-Cortex-Break-Glass"
|
|
33
|
+
BREAK_GLASS_REASONS = (
|
|
34
|
+
"incident",
|
|
35
|
+
"legal",
|
|
36
|
+
"support",
|
|
37
|
+
"security",
|
|
38
|
+
"migration",
|
|
39
|
+
"restore",
|
|
40
|
+
"other",
|
|
41
|
+
)
|
|
42
|
+
_TICKET_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._:-")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def break_glass_header(reason: str, ticket: str | None, note: str | None = None) -> str:
|
|
46
|
+
"""The `X-Cortex-Break-Glass` value (`reason=<r>; ticket=<t>[; note=<pct>]`).
|
|
47
|
+
|
|
48
|
+
Checked here as the server checks it (a malformed header is 400
|
|
49
|
+
`INVALID_HEADER` in every mode), so a typo is a usage error before
|
|
50
|
+
anything is sent. A `reason` that already holds `key=value` fields is
|
|
51
|
+
taken as the whole header.
|
|
52
|
+
"""
|
|
53
|
+
if "=" in reason:
|
|
54
|
+
value = reason.strip()
|
|
55
|
+
else:
|
|
56
|
+
if reason not in BREAK_GLASS_REASONS:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"--break-glass is one of {', '.join(BREAK_GLASS_REASONS)}, not {reason!r}"
|
|
59
|
+
)
|
|
60
|
+
if not ticket:
|
|
61
|
+
raise ValueError("--break-glass needs --break-glass-ticket (the case reference)")
|
|
62
|
+
if not (1 <= len(ticket) <= 64) or not set(ticket) <= _TICKET_CHARS:
|
|
63
|
+
raise ValueError("--break-glass-ticket is 1 to 64 characters of [A-Za-z0-9._:-]")
|
|
64
|
+
value = f"reason={reason}; ticket={ticket}"
|
|
65
|
+
if note:
|
|
66
|
+
if len(note.encode("utf-8")) > 256:
|
|
67
|
+
raise ValueError("--break-glass-note is at most 256 bytes")
|
|
68
|
+
value += f"; note={quote(note, safe='')}"
|
|
69
|
+
if len(value) > 512 or not value.isascii():
|
|
70
|
+
raise ValueError("the break-glass header is at most 512 ASCII bytes")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def request_headers(ctx: dict[str, Any]) -> dict[str, str]:
|
|
75
|
+
"""The headers the CLI context adds to every request beyond auth."""
|
|
76
|
+
headers: dict[str, str] = {}
|
|
77
|
+
reason = ctx.get("break_glass")
|
|
78
|
+
if reason:
|
|
79
|
+
try:
|
|
80
|
+
headers[BREAK_GLASS_HEADER] = break_glass_header(
|
|
81
|
+
str(reason), ctx.get("break_glass_ticket"), ctx.get("break_glass_note")
|
|
82
|
+
)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
raise click.UsageError(str(exc)) from None
|
|
85
|
+
elif ctx.get("break_glass_ticket") or ctx.get("break_glass_note"):
|
|
86
|
+
raise click.UsageError("--break-glass-ticket and --break-glass-note need --break-glass REASON")
|
|
87
|
+
return headers
|
|
88
|
+
|
|
24
89
|
|
|
25
90
|
class CortexClient:
|
|
26
91
|
"""Synchronous HTTP client for the CortexDB v1 API."""
|
|
@@ -31,6 +96,7 @@ class CortexClient:
|
|
|
31
96
|
api_key: str,
|
|
32
97
|
actor: str = "",
|
|
33
98
|
timeout: float = 30.0,
|
|
99
|
+
extra_headers: dict[str, str] | None = None,
|
|
34
100
|
) -> None:
|
|
35
101
|
self.endpoint = endpoint.rstrip("/")
|
|
36
102
|
self.api_key = api_key
|
|
@@ -43,6 +109,7 @@ class CortexClient:
|
|
|
43
109
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
44
110
|
if actor:
|
|
45
111
|
headers["X-Cortex-Actor"] = actor
|
|
112
|
+
headers.update(extra_headers or {})
|
|
46
113
|
self._http = httpx.Client(
|
|
47
114
|
base_url=self.endpoint,
|
|
48
115
|
headers=headers,
|
|
@@ -52,27 +119,53 @@ class CortexClient:
|
|
|
52
119
|
def close(self) -> None:
|
|
53
120
|
self._http.close()
|
|
54
121
|
|
|
55
|
-
def get(
|
|
56
|
-
|
|
122
|
+
def get(
|
|
123
|
+
self,
|
|
124
|
+
path: str,
|
|
125
|
+
params: dict[str, Any] | None = None,
|
|
126
|
+
headers: dict[str, str] | None = None,
|
|
127
|
+
) -> httpx.Response:
|
|
128
|
+
return self._http.get(path, params=params, headers=headers)
|
|
57
129
|
|
|
58
130
|
def post(
|
|
59
131
|
self,
|
|
60
132
|
path: str,
|
|
61
133
|
json: dict[str, Any] | None = None,
|
|
62
134
|
params: dict[str, Any] | None = None,
|
|
135
|
+
headers: dict[str, str] | None = None,
|
|
136
|
+
content: bytes | None = None,
|
|
63
137
|
) -> httpx.Response:
|
|
64
|
-
|
|
138
|
+
if content is not None:
|
|
139
|
+
# A raw body (`code import-scip`, `code import-index`). Before
|
|
140
|
+
# 0.7.0 `post` took no `content`, so those commands raised
|
|
141
|
+
# TypeError before sending anything.
|
|
142
|
+
return self._http.post(path, content=content, params=params, headers=headers)
|
|
143
|
+
return self._http.post(path, json=json, params=params, headers=headers)
|
|
65
144
|
|
|
66
145
|
def put(
|
|
67
146
|
self,
|
|
68
147
|
path: str,
|
|
69
148
|
json: dict[str, Any] | None = None,
|
|
70
149
|
params: dict[str, Any] | None = None,
|
|
150
|
+
headers: dict[str, str] | None = None,
|
|
71
151
|
) -> httpx.Response:
|
|
72
|
-
return self._http.put(path, json=json, params=params)
|
|
152
|
+
return self._http.put(path, json=json, params=params, headers=headers)
|
|
153
|
+
|
|
154
|
+
def delete(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
params: dict[str, Any] | None = None,
|
|
158
|
+
headers: dict[str, str] | None = None,
|
|
159
|
+
) -> httpx.Response:
|
|
160
|
+
return self._http.delete(path, params=params, headers=headers)
|
|
161
|
+
|
|
162
|
+
def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
|
|
163
|
+
return self._http.request(method, path, **kwargs)
|
|
164
|
+
|
|
73
165
|
|
|
74
|
-
|
|
75
|
-
|
|
166
|
+
def path_segment(value: str) -> str:
|
|
167
|
+
"""`value` as one URL path segment: `/` (a scope path's) is sent as %2F."""
|
|
168
|
+
return quote(value, safe=":@!$&'()*+,;=")
|
|
76
169
|
|
|
77
170
|
|
|
78
171
|
def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str, Any]:
|
|
@@ -87,12 +180,14 @@ def signup(endpoint: str = DEFAULT_ENDPOINT, timeout: float = 30.0) -> dict[str,
|
|
|
87
180
|
|
|
88
181
|
|
|
89
182
|
@contextmanager
|
|
90
|
-
def get_client(ctx: dict[str, Any]) -> Generator[CortexClient, None, None]:
|
|
183
|
+
def get_client(ctx: dict[str, Any], timeout: float = 30.0) -> Generator[CortexClient, None, None]:
|
|
91
184
|
"""Create a CortexClient from CLI context."""
|
|
92
185
|
client = CortexClient(
|
|
93
186
|
endpoint=ctx.get("endpoint", DEFAULT_ENDPOINT),
|
|
94
187
|
api_key=ctx.get("api_key", ""),
|
|
95
188
|
actor=ctx.get("actor", ""),
|
|
189
|
+
timeout=timeout,
|
|
190
|
+
extra_headers=request_headers(ctx),
|
|
96
191
|
)
|
|
97
192
|
try:
|
|
98
193
|
yield client
|