cortexdb-cli 0.5.4__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.
Files changed (47) hide show
  1. cortexdb_cli/__init__.py +3 -3
  2. cortexdb_cli/client.py +104 -9
  3. cortexdb_cli/commands/__init__.py +1 -1
  4. cortexdb_cli/commands/access.py +849 -0
  5. cortexdb_cli/commands/admin.py +80 -14
  6. cortexdb_cli/commands/admin_access.py +248 -0
  7. cortexdb_cli/commands/answer.py +13 -5
  8. cortexdb_cli/commands/apps.py +129 -0
  9. cortexdb_cli/commands/audit.py +3 -0
  10. cortexdb_cli/commands/auth.py +15 -27
  11. cortexdb_cli/commands/beliefs.py +68 -9
  12. cortexdb_cli/commands/changes.py +433 -0
  13. cortexdb_cli/commands/claims.py +2 -0
  14. cortexdb_cli/commands/code.py +12 -0
  15. cortexdb_cli/commands/compose.py +14 -7
  16. cortexdb_cli/commands/config_cmd.py +50 -50
  17. cortexdb_cli/commands/conflicts.py +15 -3
  18. cortexdb_cli/commands/directory.py +225 -0
  19. cortexdb_cli/commands/entities.py +4 -0
  20. cortexdb_cli/commands/episodes.py +26 -4
  21. cortexdb_cli/commands/erase.py +302 -0
  22. cortexdb_cli/commands/experience.py +168 -41
  23. cortexdb_cli/commands/export_cmd.py +20 -5
  24. cortexdb_cli/commands/facts.py +43 -15
  25. cortexdb_cli/commands/forget.py +249 -43
  26. cortexdb_cli/commands/import_cmd.py +2 -0
  27. cortexdb_cli/commands/interactive.py +107 -42
  28. cortexdb_cli/commands/onboard.py +59 -11
  29. cortexdb_cli/commands/policy.py +3 -0
  30. cortexdb_cli/commands/publish.py +90 -0
  31. cortexdb_cli/commands/recall.py +19 -5
  32. cortexdb_cli/commands/rollups.py +153 -0
  33. cortexdb_cli/commands/scopes.py +72 -21
  34. cortexdb_cli/commands/search.py +24 -7
  35. cortexdb_cli/commands/tokens.py +166 -0
  36. cortexdb_cli/commands/understanding.py +24 -7
  37. cortexdb_cli/commands/watch.py +249 -0
  38. cortexdb_cli/common.py +347 -0
  39. cortexdb_cli/errors.py +651 -96
  40. cortexdb_cli/main.py +62 -1
  41. cortexdb_cli/output.py +124 -8
  42. cortexdb_cli-0.7.0.dist-info/METADATA +378 -0
  43. cortexdb_cli-0.7.0.dist-info/RECORD +51 -0
  44. {cortexdb_cli-0.5.4.dist-info → cortexdb_cli-0.7.0.dist-info}/WHEEL +1 -1
  45. cortexdb_cli-0.5.4.dist-info/METADATA +0 -138
  46. cortexdb_cli-0.5.4.dist-info/RECORD +0 -40
  47. {cortexdb_cli-0.5.4.dist-info → cortexdb_cli-0.7.0.dist-info}/entry_points.txt +0 -0
cortexdb_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- """CortexDB CLI - command-line interface for the v1 memory API."""
2
-
3
- __version__ = "0.5.4"
1
+ """CortexDB CLI - command-line interface for the v1 memory API."""
2
+
3
+ __version__ = "0.7.0"
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 - missing it
6
- returns 401 ACTOR_MISMATCH no matter how valid the token is.
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(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
56
- return self._http.get(path, params=params)
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
- return self._http.post(path, json=json, params=params)
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
- def delete(self, path: str, params: dict[str, Any] | None = None) -> httpx.Response:
75
- return self._http.delete(path, params=params)
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
@@ -1 +1 @@
1
- """CortexDB CLI command modules."""
1
+ """CortexDB CLI command modules."""