tetra-cli 0.2.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.
- tetra_cli/__init__.py +6 -0
- tetra_cli/api_client/__init__.py +10 -0
- tetra_cli/api_client/client.py +173 -0
- tetra_cli/api_client/config.py +125 -0
- tetra_cli/api_client/operations/__init__.py +9 -0
- tetra_cli/api_client/operations/accounts.py +303 -0
- tetra_cli/api_client/operations/ai.py +278 -0
- tetra_cli/api_client/operations/analysis.py +190 -0
- tetra_cli/api_client/operations/api_keys.py +145 -0
- tetra_cli/api_client/operations/archive.py +114 -0
- tetra_cli/api_client/operations/awards.py +123 -0
- tetra_cli/api_client/operations/capacity.py +84 -0
- tetra_cli/api_client/operations/conversations.py +447 -0
- tetra_cli/api_client/operations/conversations_2.py +262 -0
- tetra_cli/api_client/operations/cosmetics.py +148 -0
- tetra_cli/api_client/operations/dashboard.py +282 -0
- tetra_cli/api_client/operations/data.py +250 -0
- tetra_cli/api_client/operations/events.py +734 -0
- tetra_cli/api_client/operations/gamification.py +470 -0
- tetra_cli/api_client/operations/goals.py +1144 -0
- tetra_cli/api_client/operations/groups.py +647 -0
- tetra_cli/api_client/operations/issues.py +198 -0
- tetra_cli/api_client/operations/offset.py +61 -0
- tetra_cli/api_client/operations/onboarding.py +284 -0
- tetra_cli/api_client/operations/outcome_schemas.py +292 -0
- tetra_cli/api_client/operations/peer_connections.py +243 -0
- tetra_cli/api_client/operations/plaid.py +329 -0
- tetra_cli/api_client/operations/reminders.py +273 -0
- tetra_cli/api_client/operations/scratches.py +280 -0
- tetra_cli/api_client/operations/skill_trees.py +160 -0
- tetra_cli/api_client/operations/social_2.py +560 -0
- tetra_cli/api_client/operations/social_3.py +618 -0
- tetra_cli/api_client/operations/social_4.py +527 -0
- tetra_cli/api_client/operations/strava.py +215 -0
- tetra_cli/api_client/operations/stripe.py +113 -0
- tetra_cli/api_client/operations/tags.py +488 -0
- tetra_cli/api_client/operations/values.py +867 -0
- tetra_cli/api_client/operations/values_2.py +584 -0
- tetra_cli/api_client/operations/watch.py +105 -0
- tetra_cli/api_client/operations/webhooks.py +50 -0
- tetra_cli/api_client/operations/xp.py +27 -0
- tetra_cli/cli/__init__.py +5 -0
- tetra_cli/cli/__main__.py +5 -0
- tetra_cli/cli/app.py +86 -0
- tetra_cli/cli/commands/__init__.py +1 -0
- tetra_cli/cli/commands/auth.py +201 -0
- tetra_cli/cli/commands/guide.py +8 -0
- tetra_cli/cli/commands/messages.py +161 -0
- tetra_cli/cli/commands/skill.py +71 -0
- tetra_cli/cli/context.py +13 -0
- tetra_cli/cli/generate.py +282 -0
- tetra_cli/cli/output.py +58 -0
- tetra_cli/mcp_gen.py +137 -0
- tetra_cli/ontology.py +70 -0
- tetra_cli/registry.py +118 -0
- tetra_cli/skill/SKILL.md +69 -0
- tetra_cli/skill/__init__.py +1 -0
- tetra_cli-0.2.0.dist-info/METADATA +140 -0
- tetra_cli-0.2.0.dist-info/RECORD +62 -0
- tetra_cli-0.2.0.dist-info/WHEEL +5 -0
- tetra_cli-0.2.0.dist-info/entry_points.txt +2 -0
- tetra_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Pure async operations for the AI chat API.
|
|
2
|
+
|
|
3
|
+
Covers natural-language querying (RAG + analytics tool calling), embedding
|
|
4
|
+
synchronization, chat history, model discovery, and per-account AI config.
|
|
5
|
+
Each op mirrors the FastAPI contract in ``tetra/api/routes/ai.py`` exactly;
|
|
6
|
+
optional body/query fields that are ``None`` are omitted so server defaults
|
|
7
|
+
apply.
|
|
8
|
+
"""
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from tetra_cli.api_client import TetraClient
|
|
12
|
+
from tetra_cli.registry import arg, operation, opt
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@operation(
|
|
16
|
+
cli="ai status",
|
|
17
|
+
summary="Check AI feature availability and configured LLM provider.",
|
|
18
|
+
covers=[("GET", "/api/v1/ai/status")],
|
|
19
|
+
)
|
|
20
|
+
async def ai_status(client: TetraClient) -> dict[str, Any]:
|
|
21
|
+
"""Check AI feature availability.
|
|
22
|
+
|
|
23
|
+
Returns whether AI is enabled, which LLM provider/embedding model are
|
|
24
|
+
configured, and (when enabled) whether the LLM is reachable.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
client: Authenticated TetraClient
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with ai_enabled, llm_provider, embedding_model, and (if enabled)
|
|
31
|
+
llm_available.
|
|
32
|
+
"""
|
|
33
|
+
return await client.get("/api/v1/ai/status")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@operation(
|
|
37
|
+
cli="ai models",
|
|
38
|
+
summary="List available LLM models for the configured provider.",
|
|
39
|
+
covers=[("GET", "/api/v1/ai/models")],
|
|
40
|
+
)
|
|
41
|
+
async def list_models(client: TetraClient) -> dict[str, Any]:
|
|
42
|
+
"""List available LLM models.
|
|
43
|
+
|
|
44
|
+
For Ollama, this queries the local Ollama API for installed models.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client: Authenticated TetraClient
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dict with 'models' list and the 'provider' name.
|
|
51
|
+
"""
|
|
52
|
+
return await client.get("/api/v1/ai/models")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@operation(
|
|
56
|
+
cli="ai config",
|
|
57
|
+
summary="Get the current account's AI configuration.",
|
|
58
|
+
covers=[("GET", "/api/v1/ai/config")],
|
|
59
|
+
)
|
|
60
|
+
async def get_ai_config(client: TetraClient) -> dict[str, Any]:
|
|
61
|
+
"""Get the current account's AI configuration.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
client: Authenticated TetraClient
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict with 'llm_model' (null when using the server default).
|
|
68
|
+
"""
|
|
69
|
+
return await client.get("/api/v1/ai/config")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@operation(
|
|
73
|
+
cli="ai set-config",
|
|
74
|
+
summary="Set or reset the current account's preferred LLM model.",
|
|
75
|
+
covers=[("PUT", "/api/v1/ai/config")],
|
|
76
|
+
params={
|
|
77
|
+
"llm_model": opt(
|
|
78
|
+
"--llm-model",
|
|
79
|
+
help="Preferred model name. Omit/clear to reset to server default.",
|
|
80
|
+
clears=True,
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
async def update_ai_config(
|
|
85
|
+
client: TetraClient,
|
|
86
|
+
*,
|
|
87
|
+
llm_model: str | None = None,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
"""Update the current account's AI configuration.
|
|
90
|
+
|
|
91
|
+
The PUT route always reads the ``llm_model`` field: a non-null value sets
|
|
92
|
+
the preferred model, ``null`` resets to the server default. The body is
|
|
93
|
+
therefore always sent with an explicit ``llm_model`` key (defaulting to
|
|
94
|
+
``None`` = reset) rather than being omitted.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
client: Authenticated TetraClient
|
|
98
|
+
llm_model: Preferred model name, or None to reset to the server
|
|
99
|
+
default.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Updated config dict with the effective 'llm_model'.
|
|
103
|
+
"""
|
|
104
|
+
body: dict[str, Any] = {"llm_model": llm_model}
|
|
105
|
+
return await client.put("/api/v1/ai/config", json=body)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@operation(
|
|
109
|
+
cli="ai query",
|
|
110
|
+
summary="Ask a natural-language question about your data (RAG + tools).",
|
|
111
|
+
covers=[("POST", "/api/v1/ai/query")],
|
|
112
|
+
params={
|
|
113
|
+
"query": arg(help="Natural-language question (1-2000 chars)."),
|
|
114
|
+
"entity_types": opt(
|
|
115
|
+
"--entity-type", repeatable=True,
|
|
116
|
+
help="Restrict search to entity types (value | goal | event).",
|
|
117
|
+
),
|
|
118
|
+
"date_start": opt("--date-start", help="Filter start date (YYYY-MM-DD)."),
|
|
119
|
+
"date_end": opt("--date-end", help="Filter end date (YYYY-MM-DD)."),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
async def ai_query(
|
|
123
|
+
client: TetraClient,
|
|
124
|
+
*,
|
|
125
|
+
query: str,
|
|
126
|
+
entity_types: list[str] | None = None,
|
|
127
|
+
date_start: str | None = None,
|
|
128
|
+
date_end: str | None = None,
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
"""Execute a natural-language query against the account's data.
|
|
131
|
+
|
|
132
|
+
Mirrors ``AIQueryRequest``: ``query`` is required, the rest are optional
|
|
133
|
+
filters omitted when not provided. The exchange is persisted to chat
|
|
134
|
+
history server-side.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
client: Authenticated TetraClient
|
|
138
|
+
query: Natural-language question (1-2000 characters).
|
|
139
|
+
entity_types: Optional entity-type filter ('value', 'goal', 'event').
|
|
140
|
+
date_start: Optional ISO start date (YYYY-MM-DD) for filtering.
|
|
141
|
+
date_end: Optional ISO end date (YYYY-MM-DD) for filtering.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dict with 'answer', 'sources', 'tools_used', and 'query_id'.
|
|
145
|
+
"""
|
|
146
|
+
body: dict[str, Any] = {"query": query}
|
|
147
|
+
if entity_types:
|
|
148
|
+
body["entity_types"] = entity_types
|
|
149
|
+
if date_start is not None:
|
|
150
|
+
body["date_start"] = date_start
|
|
151
|
+
if date_end is not None:
|
|
152
|
+
body["date_end"] = date_end
|
|
153
|
+
return await client.post("/api/v1/ai/query", json=body)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@operation(
|
|
157
|
+
cli="ai query-stream",
|
|
158
|
+
summary="Ask a question and stream progress via Server-Sent Events.",
|
|
159
|
+
covers=[("POST", "/api/v1/ai/query/stream")],
|
|
160
|
+
params={
|
|
161
|
+
"query": arg(help="Natural-language question (1-2000 chars)."),
|
|
162
|
+
"entity_types": opt(
|
|
163
|
+
"--entity-type", repeatable=True,
|
|
164
|
+
help="Restrict search to entity types (value | goal | event).",
|
|
165
|
+
),
|
|
166
|
+
"date_start": opt("--date-start", help="Filter start date (YYYY-MM-DD)."),
|
|
167
|
+
"date_end": opt("--date-end", help="Filter end date (YYYY-MM-DD)."),
|
|
168
|
+
},
|
|
169
|
+
output="stream",
|
|
170
|
+
)
|
|
171
|
+
async def ai_query_stream(
|
|
172
|
+
client: TetraClient,
|
|
173
|
+
*,
|
|
174
|
+
query: str,
|
|
175
|
+
entity_types: list[str] | None = None,
|
|
176
|
+
date_start: str | None = None,
|
|
177
|
+
date_end: str | None = None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Stream AI query progress via Server-Sent Events.
|
|
180
|
+
|
|
181
|
+
Same request shape as :func:`ai_query` (the ``AIQueryRequest`` body); the
|
|
182
|
+
route returns an SSE stream (status events, then a done/error event)
|
|
183
|
+
instead of a single JSON response.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
client: Authenticated TetraClient
|
|
187
|
+
query: Natural-language question (1-2000 characters).
|
|
188
|
+
entity_types: Optional entity-type filter ('value', 'goal', 'event').
|
|
189
|
+
date_start: Optional ISO start date (YYYY-MM-DD) for filtering.
|
|
190
|
+
date_end: Optional ISO end date (YYYY-MM-DD) for filtering.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The raw stream payload as returned by the client transport.
|
|
194
|
+
"""
|
|
195
|
+
body: dict[str, Any] = {"query": query}
|
|
196
|
+
if entity_types:
|
|
197
|
+
body["entity_types"] = entity_types
|
|
198
|
+
if date_start is not None:
|
|
199
|
+
body["date_start"] = date_start
|
|
200
|
+
if date_end is not None:
|
|
201
|
+
body["date_end"] = date_end
|
|
202
|
+
return await client.post("/api/v1/ai/query/stream", json=body)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@operation(
|
|
206
|
+
cli="ai sync",
|
|
207
|
+
summary="Re-index values, goals, and events for semantic search.",
|
|
208
|
+
covers=[("POST", "/api/v1/ai/embeddings/sync")],
|
|
209
|
+
)
|
|
210
|
+
async def sync_embeddings(client: TetraClient) -> dict[str, Any]:
|
|
211
|
+
"""Trigger embedding generation/refresh for the current account.
|
|
212
|
+
|
|
213
|
+
Re-indexes all values, goals, and events for semantic search. This is an
|
|
214
|
+
expensive operation and should be called sparingly. Takes no body.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
client: Authenticated TetraClient
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dict with 'entities_indexed' and 'status'.
|
|
221
|
+
"""
|
|
222
|
+
return await client.post("/api/v1/ai/embeddings/sync")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@operation(
|
|
226
|
+
cli="ai history",
|
|
227
|
+
summary="Get the current account's AI chat history (chronological).",
|
|
228
|
+
covers=[("GET", "/api/v1/ai/chat/history")],
|
|
229
|
+
params={
|
|
230
|
+
"limit": opt("--limit", min=1, max=200,
|
|
231
|
+
help="Max messages to return (1-200, default 50)."),
|
|
232
|
+
"offset": opt("--offset", min=0,
|
|
233
|
+
help="Number of messages to skip (default 0)."),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
async def get_chat_history(
|
|
237
|
+
client: TetraClient,
|
|
238
|
+
*,
|
|
239
|
+
limit: int | None = None,
|
|
240
|
+
offset: int | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Get chat history for the current account.
|
|
243
|
+
|
|
244
|
+
Returns the most recent messages ordered chronologically (ascending).
|
|
245
|
+
Query params are omitted when not provided so the server defaults
|
|
246
|
+
(limit=50, offset=0) apply.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
client: Authenticated TetraClient
|
|
250
|
+
limit: Max number of messages to return (1-200).
|
|
251
|
+
offset: Number of messages to skip for pagination.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict with 'messages' list and 'total' count.
|
|
255
|
+
"""
|
|
256
|
+
params: dict[str, Any] = {}
|
|
257
|
+
if limit is not None:
|
|
258
|
+
params["limit"] = limit
|
|
259
|
+
if offset is not None:
|
|
260
|
+
params["offset"] = offset
|
|
261
|
+
return await client.get("/api/v1/ai/chat/history", params=params or None)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@operation(
|
|
265
|
+
cli="ai clear-history",
|
|
266
|
+
summary="Delete all AI chat history for the current account.",
|
|
267
|
+
covers=[("DELETE", "/api/v1/ai/chat/history")],
|
|
268
|
+
)
|
|
269
|
+
async def clear_chat_history(client: TetraClient) -> dict[str, Any]:
|
|
270
|
+
"""Clear all chat history for the current account.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
client: Authenticated TetraClient
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict with 'deleted' count and 'status'.
|
|
277
|
+
"""
|
|
278
|
+
return await client.delete("/api/v1/ai/chat/history")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Pure async operations for the analysis/export APIs."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from tetra_cli.api_client import TetraClient
|
|
5
|
+
from tetra_cli.registry import operation, opt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@operation(
|
|
9
|
+
cli="analysis time-allocation",
|
|
10
|
+
summary="Show how time is allocated across values for a date range.",
|
|
11
|
+
covers=[("GET", "/api/v1/analysis/time-allocation")],
|
|
12
|
+
params={
|
|
13
|
+
"start_date": opt("--start-date", help="Start date filter (YYYY-MM-DD)"),
|
|
14
|
+
"end_date": opt("--end-date", help="End date filter (YYYY-MM-DD)"),
|
|
15
|
+
},
|
|
16
|
+
)
|
|
17
|
+
async def get_time_allocation(
|
|
18
|
+
client: TetraClient,
|
|
19
|
+
*,
|
|
20
|
+
start_date: str | None = None,
|
|
21
|
+
end_date: str | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Show how time is allocated across values for a date range.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
client: Authenticated TetraClient
|
|
27
|
+
start_date: Optional start-date filter (YYYY-MM-DD)
|
|
28
|
+
end_date: Optional end-date filter (YYYY-MM-DD)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dict with total_hours and a per-value allocation list.
|
|
32
|
+
"""
|
|
33
|
+
params: dict[str, Any] = {}
|
|
34
|
+
if start_date:
|
|
35
|
+
params["start_date"] = start_date
|
|
36
|
+
if end_date:
|
|
37
|
+
params["end_date"] = end_date
|
|
38
|
+
return await client.get("/api/v1/analysis/time-allocation", params=params)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@operation(
|
|
42
|
+
cli="analysis spending-by-goal",
|
|
43
|
+
summary="Show spending (outcomes) distributed across goals for a date range.",
|
|
44
|
+
covers=[("GET", "/api/v1/analysis/spending-by-goal")],
|
|
45
|
+
params={
|
|
46
|
+
"start_date": opt("--start-date", help="Start date filter (YYYY-MM-DD)"),
|
|
47
|
+
"end_date": opt("--end-date", help="End date filter (YYYY-MM-DD)"),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
async def get_spending_by_goal(
|
|
51
|
+
client: TetraClient,
|
|
52
|
+
*,
|
|
53
|
+
start_date: str | None = None,
|
|
54
|
+
end_date: str | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Show spending (outcomes) distributed across goals for a date range.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
client: Authenticated TetraClient
|
|
60
|
+
start_date: Optional start-date filter (YYYY-MM-DD)
|
|
61
|
+
end_date: Optional end-date filter (YYYY-MM-DD)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict with total_spending and a per-goal spending list.
|
|
65
|
+
"""
|
|
66
|
+
params: dict[str, Any] = {}
|
|
67
|
+
if start_date:
|
|
68
|
+
params["start_date"] = start_date
|
|
69
|
+
if end_date:
|
|
70
|
+
params["end_date"] = end_date
|
|
71
|
+
return await client.get("/api/v1/analysis/spending-by-goal", params=params)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@operation(
|
|
75
|
+
cli="analysis effective-hourly",
|
|
76
|
+
summary="Calculate effective hourly rate from an income goal and a work value.",
|
|
77
|
+
covers=[("GET", "/api/v1/analysis/effective-hourly")],
|
|
78
|
+
params={
|
|
79
|
+
"income_goal": opt("--income-goal", help="Goal name that tracks income"),
|
|
80
|
+
"work_value": opt("--work-value", help="Value name that tracks work time"),
|
|
81
|
+
"start": opt("--start", help="Start date filter (YYYY-MM-DD)"),
|
|
82
|
+
"end": opt("--end", help="End date filter (YYYY-MM-DD)"),
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
async def get_effective_hourly(
|
|
86
|
+
client: TetraClient,
|
|
87
|
+
*,
|
|
88
|
+
income_goal: str,
|
|
89
|
+
work_value: str,
|
|
90
|
+
start: str | None = None,
|
|
91
|
+
end: str | None = None,
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
"""Calculate effective hourly rate ($/hour).
|
|
94
|
+
|
|
95
|
+
Compares total income (events on ``income_goal``) to total hours
|
|
96
|
+
(events on ``work_value``) to derive an effective rate. The backend
|
|
97
|
+
requires both ``income_goal`` and ``work_value`` query params; ``start``
|
|
98
|
+
and ``end`` are optional date filters.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
client: Authenticated TetraClient
|
|
102
|
+
income_goal: Goal name that tracks income (required)
|
|
103
|
+
work_value: Value name that tracks work time (required)
|
|
104
|
+
start: Optional start-date filter (YYYY-MM-DD)
|
|
105
|
+
end: Optional end-date filter (YYYY-MM-DD)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict with total_income, total_hours, and effective_rate.
|
|
109
|
+
"""
|
|
110
|
+
params: dict[str, Any] = {
|
|
111
|
+
"income_goal": income_goal,
|
|
112
|
+
"work_value": work_value,
|
|
113
|
+
}
|
|
114
|
+
if start:
|
|
115
|
+
params["start"] = start
|
|
116
|
+
if end:
|
|
117
|
+
params["end"] = end
|
|
118
|
+
return await client.get("/api/v1/analysis/effective-hourly", params=params)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@operation(
|
|
122
|
+
cli="analysis double-draw-detection",
|
|
123
|
+
summary="Detect likely double-counted financial draws within a value hierarchy.",
|
|
124
|
+
covers=[("GET", "/api/v1/analysis/double-draw-detection")],
|
|
125
|
+
params={
|
|
126
|
+
"value_uid": opt("--value-uid", help="Parent value UID to scan"),
|
|
127
|
+
"start": opt("--start", help="Start date (default: 90 days ago)"),
|
|
128
|
+
"end": opt("--end", help="End date (default: today)"),
|
|
129
|
+
"min_confidence": opt("--min-confidence", min=0, max=1,
|
|
130
|
+
help="Minimum confidence threshold (0-1)"),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
async def get_double_draw_detection(
|
|
134
|
+
client: TetraClient,
|
|
135
|
+
*,
|
|
136
|
+
value_uid: str,
|
|
137
|
+
start: str | None = None,
|
|
138
|
+
end: str | None = None,
|
|
139
|
+
min_confidence: float | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""Detect likely double-counted financial draws within a value hierarchy.
|
|
142
|
+
|
|
143
|
+
Scans the parent value (``value_uid``) and its children for lump events
|
|
144
|
+
on one child that match the sum of itemized events on a sibling child.
|
|
145
|
+
The backend requires ``value_uid``; ``start``/``end`` default to a
|
|
146
|
+
90-day window and ``min_confidence`` defaults to 0.5 server-side.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
client: Authenticated TetraClient
|
|
150
|
+
value_uid: Parent value UID to scan (required)
|
|
151
|
+
start: Optional start date (YYYY-MM-DD; default 90 days ago)
|
|
152
|
+
end: Optional end date (YYYY-MM-DD; default today)
|
|
153
|
+
min_confidence: Optional minimum confidence threshold (0-1)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict with a ``matches`` list of detected double-draw matches.
|
|
157
|
+
"""
|
|
158
|
+
params: dict[str, Any] = {"value_uid": value_uid}
|
|
159
|
+
if start:
|
|
160
|
+
params["start"] = start
|
|
161
|
+
if end:
|
|
162
|
+
params["end"] = end
|
|
163
|
+
if min_confidence is not None:
|
|
164
|
+
params["min_confidence"] = min_confidence
|
|
165
|
+
return await client.get(
|
|
166
|
+
"/api/v1/analysis/double-draw-detection", params=params,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@operation(
|
|
171
|
+
cli="data export",
|
|
172
|
+
summary="Export all account data as a .zip of CSVs (values, goals, events).",
|
|
173
|
+
covers=[("GET", "/api/v1/data/export")],
|
|
174
|
+
output="file",
|
|
175
|
+
)
|
|
176
|
+
async def export_data(client: TetraClient) -> tuple[bytes, str]:
|
|
177
|
+
"""Export all account data as a downloadable zip archive.
|
|
178
|
+
|
|
179
|
+
The export endpoint returns a ``.zip`` containing ``values.csv``,
|
|
180
|
+
``goals.csv``, and ``events.csv`` (the same archive ``data import``
|
|
181
|
+
accepts), so the op downloads the raw bytes rather than JSON-decoding
|
|
182
|
+
them. Over MCP the bytes are returned base64-encoded.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
client: Authenticated TetraClient.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
A ``(content_bytes, filename)`` tuple for the file generator to write.
|
|
189
|
+
"""
|
|
190
|
+
return await client.download("/api/v1/data/export")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Pure async operations for the API-key management API.
|
|
2
|
+
|
|
3
|
+
These ops mirror ``backend/tetra/api/routes/api_keys.py``. API keys are the
|
|
4
|
+
agent's own credentials: an account creates a key (the raw token is shown
|
|
5
|
+
ONCE), lists its keys, updates scopes/avatar, and revokes keys. Webhook
|
|
6
|
+
registration on a key lives in ``webhooks.py`` (it shares the PATCH route but
|
|
7
|
+
is exposed as its own dedicated ops); the ``update`` op here handles the
|
|
8
|
+
scopes/avatar path of that same PATCH.
|
|
9
|
+
"""
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from tetra_cli.api_client import TetraClient
|
|
13
|
+
from tetra_cli.registry import arg, operation, opt
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@operation(
|
|
17
|
+
cli="api-keys list",
|
|
18
|
+
summary="List all API keys for the current account.",
|
|
19
|
+
covers=[("GET", "/api/v1/api-keys/")],
|
|
20
|
+
)
|
|
21
|
+
async def list_api_keys(client: TetraClient) -> list[dict[str, Any]]:
|
|
22
|
+
"""List all API keys for the current account.
|
|
23
|
+
|
|
24
|
+
The raw token is never returned here — it is only shown once at
|
|
25
|
+
creation time. Each entry exposes the prefix, scopes, avatar, activity
|
|
26
|
+
timestamps, and whether a webhook is configured.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
client: Authenticated TetraClient
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of API-key dicts (without raw tokens).
|
|
33
|
+
"""
|
|
34
|
+
return await client.get("/api/v1/api-keys/")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@operation(
|
|
38
|
+
cli="api-keys create",
|
|
39
|
+
summary="Create a new API key (raw token is shown only once).",
|
|
40
|
+
covers=[("POST", "/api/v1/api-keys/")],
|
|
41
|
+
params={
|
|
42
|
+
"name": arg(help="Human-readable key name (max 100 chars)."),
|
|
43
|
+
"scopes": opt("--scope", repeatable=True,
|
|
44
|
+
help="Permission scope (repeatable; at least one required)."),
|
|
45
|
+
"expires_at": opt("--expires-at",
|
|
46
|
+
help="ISO-8601 expiry timestamp (UTC). Omit for no expiry."),
|
|
47
|
+
"avatar": opt("--avatar", json=True,
|
|
48
|
+
help="Avatar configuration as a JSON dict."),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
async def create_api_key(
|
|
52
|
+
client: TetraClient,
|
|
53
|
+
*,
|
|
54
|
+
name: str,
|
|
55
|
+
scopes: list[str],
|
|
56
|
+
expires_at: str | None = None,
|
|
57
|
+
avatar: dict[str, Any] | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Create a new API key.
|
|
60
|
+
|
|
61
|
+
The response includes ``raw_token`` exactly once — it cannot be
|
|
62
|
+
retrieved again, so the caller must capture it immediately. Only fields
|
|
63
|
+
that are provided are sent; ``expires_at`` and ``avatar`` are omitted
|
|
64
|
+
when None so the API applies its defaults (no expiry / no avatar).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
client: Authenticated TetraClient
|
|
68
|
+
name: Human-readable key name (required, max 100 chars)
|
|
69
|
+
scopes: Permission scopes (at least one required)
|
|
70
|
+
expires_at: Optional ISO-8601 UTC expiry timestamp
|
|
71
|
+
avatar: Optional avatar configuration dict
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Dict with ``key`` (the API-key metadata) and ``raw_token``.
|
|
75
|
+
"""
|
|
76
|
+
body: dict[str, Any] = {"name": name, "scopes": scopes}
|
|
77
|
+
if expires_at is not None:
|
|
78
|
+
body["expires_at"] = expires_at
|
|
79
|
+
if avatar is not None:
|
|
80
|
+
body["avatar"] = avatar
|
|
81
|
+
return await client.post("/api/v1/api-keys/", json=body)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@operation(
|
|
85
|
+
cli="api-keys update",
|
|
86
|
+
summary="Update an API key's scopes and/or avatar.",
|
|
87
|
+
covers=[("PATCH", "/api/v1/api-keys/{uid}")],
|
|
88
|
+
params={
|
|
89
|
+
"uid": arg(help="API key UID."),
|
|
90
|
+
"scopes": opt("--scope", repeatable=True,
|
|
91
|
+
help="Permission scope (repeatable; replaces all scopes)."),
|
|
92
|
+
"avatar": opt("--avatar", json=True,
|
|
93
|
+
help="Avatar configuration as a JSON dict (pass to clear handled separately)."),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
async def update_api_key(
|
|
97
|
+
client: TetraClient,
|
|
98
|
+
*,
|
|
99
|
+
uid: str,
|
|
100
|
+
scopes: list[str] | None = None,
|
|
101
|
+
avatar: dict[str, Any] | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Update an API key's scopes and/or avatar.
|
|
104
|
+
|
|
105
|
+
The backend keys off ``model_fields_set``: only fields present in the
|
|
106
|
+
request body are applied, and a missing field is left untouched. This op
|
|
107
|
+
therefore only includes ``scopes`` / ``avatar`` when the caller actually
|
|
108
|
+
passed them — a scopes-only update never disturbs the avatar (or a
|
|
109
|
+
configured webhook), and vice-versa. Webhook registration is handled by
|
|
110
|
+
the dedicated ops in ``webhooks.py``.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
client: Authenticated TetraClient
|
|
114
|
+
uid: API key UID
|
|
115
|
+
scopes: New scope list (replaces all scopes; cannot be null)
|
|
116
|
+
avatar: New avatar configuration dict
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dict ``{"updated": True}`` on success.
|
|
120
|
+
"""
|
|
121
|
+
body: dict[str, Any] = {}
|
|
122
|
+
if scopes is not None:
|
|
123
|
+
body["scopes"] = scopes
|
|
124
|
+
if avatar is not None:
|
|
125
|
+
body["avatar"] = avatar
|
|
126
|
+
return await client.patch(f"/api/v1/api-keys/{uid}", json=body)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@operation(
|
|
130
|
+
cli="api-keys revoke",
|
|
131
|
+
summary="Revoke (deactivate) an API key.",
|
|
132
|
+
covers=[("DELETE", "/api/v1/api-keys/{uid}")],
|
|
133
|
+
params={"uid": arg(help="API key UID to revoke.")},
|
|
134
|
+
)
|
|
135
|
+
async def revoke_api_key(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
136
|
+
"""Revoke an API key, immediately deactivating it.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
client: Authenticated TetraClient
|
|
140
|
+
uid: API key UID to revoke
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Dict ``{"revoked": True}`` on success.
|
|
144
|
+
"""
|
|
145
|
+
return await client.delete(f"/api/v1/api-keys/{uid}")
|