tetra-cli 0.2.0__tar.gz → 0.5.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.
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/PKG-INFO +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/pyproject.toml +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tests/test_smoke.py +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/__init__.py +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/capacity.py +17 -10
- tetra_cli-0.5.0/tetra_cli/api_client/operations/event_sessions.py +228 -0
- tetra_cli-0.5.0/tetra_cli/api_client/operations/foods.py +31 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/goals.py +176 -8
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/groups.py +24 -5
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/peer_connections.py +31 -0
- tetra_cli-0.5.0/tetra_cli/api_client/operations/skill_tree_rankings.py +152 -0
- tetra_cli-0.5.0/tetra_cli/api_client/operations/skill_tree_requests.py +161 -0
- tetra_cli-0.5.0/tetra_cli/api_client/operations/skill_trees.py +401 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/social_2.py +10 -10
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/social_3.py +6 -6
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/values.py +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/values_2.py +23 -6
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/PKG-INFO +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/SOURCES.txt +4 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/skill_trees.py +0 -160
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/README.md +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/setup.cfg +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tests/test_no_backend_leak.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/client.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/config.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/accounts.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/ai.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/analysis.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/api_keys.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/archive.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/awards.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/conversations.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/conversations_2.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/cosmetics.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/dashboard.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/data.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/events.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/gamification.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/issues.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/offset.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/onboarding.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/outcome_schemas.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/plaid.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/reminders.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/scratches.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/social_4.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/strava.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/stripe.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/tags.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/watch.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/webhooks.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/api_client/operations/xp.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/__main__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/app.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/commands/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/commands/auth.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/commands/guide.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/commands/messages.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/commands/skill.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/context.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/generate.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/cli/output.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/mcp_gen.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/ontology.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/registry.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/skill/SKILL.md +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli/skill/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/dependency_links.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/entry_points.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/requires.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.5.0}/tetra_cli.egg-info/top_level.txt +0 -0
|
@@ -61,24 +61,31 @@ async def get_group_capacity(
|
|
|
61
61
|
|
|
62
62
|
@operation(
|
|
63
63
|
cli="capacity purchase-slot",
|
|
64
|
-
summary="Buy the next credit-purchased active capacity
|
|
64
|
+
summary="Buy the next credit-purchased active capacity slots.",
|
|
65
65
|
covers=[("POST", "/api/v1/capacity/purchase-slot")],
|
|
66
|
+
params={"quantity": arg(help="How many slots to buy (default 1)")},
|
|
66
67
|
)
|
|
67
|
-
async def purchase_capacity_slot(
|
|
68
|
-
|
|
68
|
+
async def purchase_capacity_slot(
|
|
69
|
+
client: TetraClient, quantity: int = 1,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Buy the next ``quantity`` credit-purchased active slots.
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
Per-slot cost escalates and the purchase is all-or-nothing: the summed
|
|
74
|
+
cost is debited in one transaction or the call fails without buying any.
|
|
72
75
|
|
|
73
76
|
Args:
|
|
74
77
|
client: Authenticated TetraClient
|
|
78
|
+
quantity: How many slots to buy (default 1)
|
|
75
79
|
|
|
76
80
|
Returns:
|
|
77
|
-
Updated capacity dict reflecting the new ``purchased_slots
|
|
78
|
-
``next_slot_cost
|
|
81
|
+
Updated capacity dict reflecting the new ``purchased_slots``,
|
|
82
|
+
``next_slot_cost``, and ``slot_cost_paid`` (total credits debited).
|
|
79
83
|
|
|
80
84
|
Raises:
|
|
81
|
-
TetraClientError: 400 ``insufficient_credits`` if the balance
|
|
82
|
-
|
|
85
|
+
TetraClientError: 400 ``insufficient_credits`` if the balance can't
|
|
86
|
+
cover the summed cost; 400 ``free_cap_reached`` when a free
|
|
87
|
+
account would end past the slot hard cap.
|
|
83
88
|
"""
|
|
84
|
-
return await client.post(
|
|
89
|
+
return await client.post(
|
|
90
|
+
"/api/v1/capacity/purchase-slot", json={"quantity": quantity}
|
|
91
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Pure async operations for the event-sessions API.
|
|
2
|
+
|
|
3
|
+
Event Sessions group two or more existing events on the same subject timeline
|
|
4
|
+
into a named "occurrence" (e.g. three lifts logged in one gym visit). Grouping
|
|
5
|
+
is a first-class capability, so it must be drivable from the CLI and the
|
|
6
|
+
generated MCP tools — not just the web app. Each op here is a thin wrapper over
|
|
7
|
+
one ``/api/v1/event-sessions`` endpoint; the backend owns quorum, shared-scope,
|
|
8
|
+
and authorization rules.
|
|
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="events session group",
|
|
18
|
+
summary="Group >= 2 existing events into a new session.",
|
|
19
|
+
covers=[("POST", "/api/v1/event-sessions")],
|
|
20
|
+
params={
|
|
21
|
+
"event_uids": opt("--uid", repeatable=True,
|
|
22
|
+
help="Event UID to group (repeatable, >= 2 required)"),
|
|
23
|
+
"name": opt("--name", help="Optional session label"),
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
async def group_events(
|
|
27
|
+
client: TetraClient,
|
|
28
|
+
*,
|
|
29
|
+
event_uids: list[str],
|
|
30
|
+
name: str | None = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Group existing events into a new session.
|
|
33
|
+
|
|
34
|
+
All events must resolve and share one ``group_uid`` (the same subject
|
|
35
|
+
timeline); the backend rejects cross-group grouping with HTTP 400.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
client: Authenticated TetraClient
|
|
39
|
+
event_uids: Event UIDs to group (at least two, same subject timeline)
|
|
40
|
+
name: Optional display label for the session
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The created session detail (uid, members, summary fields).
|
|
44
|
+
"""
|
|
45
|
+
body: dict[str, Any] = {"event_uids": event_uids}
|
|
46
|
+
if name is not None:
|
|
47
|
+
body["name"] = name
|
|
48
|
+
return await client.post("/api/v1/event-sessions", json=body)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@operation(
|
|
52
|
+
cli="events session show",
|
|
53
|
+
summary="Get a session with its member events.",
|
|
54
|
+
covers=[("GET", "/api/v1/event-sessions/{uid}")],
|
|
55
|
+
params={
|
|
56
|
+
"uid": arg(help="Session UID"),
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
async def show_event_session(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
60
|
+
"""Get one session and its member events.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
client: Authenticated TetraClient
|
|
64
|
+
uid: Session UID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The session detail dict (summary fields plus member events).
|
|
68
|
+
"""
|
|
69
|
+
return await client.get(f"/api/v1/event-sessions/{uid}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@operation(
|
|
73
|
+
cli="events session list",
|
|
74
|
+
summary="List sessions on the caller's own timeline.",
|
|
75
|
+
covers=[("GET", "/api/v1/event-sessions")],
|
|
76
|
+
)
|
|
77
|
+
async def list_event_sessions(client: TetraClient) -> list[dict[str, Any]]:
|
|
78
|
+
"""List the caller's own personal-timeline sessions.
|
|
79
|
+
|
|
80
|
+
Intentionally self-scoped to the caller's ``personal_group_uid`` — group
|
|
81
|
+
sessions are reached via their events, not this listing.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
client: Authenticated TetraClient
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A list of session summary dicts.
|
|
88
|
+
"""
|
|
89
|
+
return await client.get("/api/v1/event-sessions")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@operation(
|
|
93
|
+
cli="events session rename",
|
|
94
|
+
summary="Set a session's display label.",
|
|
95
|
+
covers=[("PATCH", "/api/v1/event-sessions/{uid}")],
|
|
96
|
+
params={
|
|
97
|
+
"uid": arg(help="Session UID"),
|
|
98
|
+
"name": arg(help="New display label"),
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
async def rename_event_session(
|
|
102
|
+
client: TetraClient, uid: str, name: str,
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""Relabel a session.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
client: Authenticated TetraClient
|
|
108
|
+
uid: Session UID
|
|
109
|
+
name: New display label
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The updated session summary dict.
|
|
113
|
+
"""
|
|
114
|
+
return await client.patch(
|
|
115
|
+
f"/api/v1/event-sessions/{uid}", json={"name": name}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@operation(
|
|
120
|
+
cli="events session add-member",
|
|
121
|
+
summary="Attach one more event to an existing session.",
|
|
122
|
+
covers=[("POST", "/api/v1/event-sessions/{uid}/members")],
|
|
123
|
+
params={
|
|
124
|
+
"uid": arg(help="Session UID"),
|
|
125
|
+
"event_uid": arg(help="Event UID to attach"),
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
async def add_event_session_member(
|
|
129
|
+
client: TetraClient, uid: str, event_uid: str,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""Attach one more event to a session.
|
|
132
|
+
|
|
133
|
+
The event must share the session's ``group_uid``; if it already belongs to
|
|
134
|
+
another session it is re-homed here.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
client: Authenticated TetraClient
|
|
138
|
+
uid: Session UID to grow
|
|
139
|
+
event_uid: Event UID to attach
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The updated session summary dict.
|
|
143
|
+
"""
|
|
144
|
+
return await client.post(
|
|
145
|
+
f"/api/v1/event-sessions/{uid}/members", json={"event_uid": event_uid}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@operation(
|
|
150
|
+
cli="events session remove-member",
|
|
151
|
+
summary="Detach one event; the session dissolves below quorum.",
|
|
152
|
+
covers=[("DELETE", "/api/v1/event-sessions/{uid}/members/{event_uid}")],
|
|
153
|
+
params={
|
|
154
|
+
"uid": arg(help="Session UID"),
|
|
155
|
+
"event_uid": arg(help="Event UID to detach"),
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
async def remove_event_session_member(
|
|
159
|
+
client: TetraClient, uid: str, event_uid: str,
|
|
160
|
+
) -> dict[str, Any] | None:
|
|
161
|
+
"""Detach one event from a session.
|
|
162
|
+
|
|
163
|
+
If removing the event drops the session below quorum, the session is
|
|
164
|
+
dissolved entirely and the response is ``null``.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
client: Authenticated TetraClient
|
|
168
|
+
uid: Session UID
|
|
169
|
+
event_uid: Event UID to detach
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The resulting session summary, or ``None`` if the session dissolved.
|
|
173
|
+
"""
|
|
174
|
+
return await client.delete(
|
|
175
|
+
f"/api/v1/event-sessions/{uid}/members/{event_uid}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@operation(
|
|
180
|
+
cli="events session ungroup",
|
|
181
|
+
summary="Dissolve a session entirely, unlinking all its members.",
|
|
182
|
+
covers=[("DELETE", "/api/v1/event-sessions/{uid}")],
|
|
183
|
+
params={
|
|
184
|
+
"uid": arg(help="Session UID"),
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
async def ungroup_event_session(
|
|
188
|
+
client: TetraClient, uid: str,
|
|
189
|
+
) -> dict[str, Any] | None:
|
|
190
|
+
"""Dissolve a session, unlinking every member event.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
client: Authenticated TetraClient
|
|
194
|
+
uid: Session UID
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Empty/``None`` — the backend responds 204 No Content.
|
|
198
|
+
"""
|
|
199
|
+
return await client.delete(f"/api/v1/event-sessions/{uid}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@operation(
|
|
203
|
+
cli="events session suggest",
|
|
204
|
+
summary="Advisory: which session/event should an event join? (never stamps)",
|
|
205
|
+
covers=[("POST", "/api/v1/event-sessions/suggest")],
|
|
206
|
+
params={
|
|
207
|
+
"event_uid": arg(help="Candidate event UID"),
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
async def suggest_event_session(
|
|
211
|
+
client: TetraClient, event_uid: str,
|
|
212
|
+
) -> dict[str, Any] | None:
|
|
213
|
+
"""Ask which session/event the given event should join.
|
|
214
|
+
|
|
215
|
+
Purely advisory — it never mutates. Returns ``null`` when nothing on the
|
|
216
|
+
same subject timeline matches within the session window.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
client: Authenticated TetraClient
|
|
220
|
+
event_uid: Candidate event UID
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A suggestion dict (join_session_uid / join_event_uid / derived_label),
|
|
224
|
+
or ``None`` when nothing matches.
|
|
225
|
+
"""
|
|
226
|
+
return await client.post(
|
|
227
|
+
"/api/v1/event-sessions/suggest", json={"event_uid": event_uid}
|
|
228
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Pure async operations for the foods API."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from tetra_cli.api_client import TetraClient
|
|
5
|
+
from tetra_cli.registry import arg, operation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@operation(
|
|
9
|
+
cli="foods search",
|
|
10
|
+
summary="Search foods by free-text description (USDA-backed, cached).",
|
|
11
|
+
covers=[("GET", "/api/v1/foods/search")],
|
|
12
|
+
params={
|
|
13
|
+
"q": arg(help="Free-text food search term (e.g. \"egg\")"),
|
|
14
|
+
},
|
|
15
|
+
)
|
|
16
|
+
async def food_search(client: TetraClient, q: str) -> Any:
|
|
17
|
+
"""Search cached/USDA foods by free-text description.
|
|
18
|
+
|
|
19
|
+
Delegates to the server's ``FoodLookupService``: a local-cache hit
|
|
20
|
+
answers immediately, a miss fills in from USDA FoodData Central. Returns
|
|
21
|
+
whatever shape the endpoint returns, unmodified.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
client: Authenticated TetraClient
|
|
25
|
+
q: Non-empty free-text search term (e.g. "egg")
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of food dicts (fdc_food_id, description, brand, kcal_per_100g,
|
|
29
|
+
protein_per_100g, carb_per_100g, fat_per_100g, serving_size_g)
|
|
30
|
+
"""
|
|
31
|
+
return await client.get("/api/v1/foods/search", params={"q": q})
|
|
@@ -188,6 +188,10 @@ async def create_goal(
|
|
|
188
188
|
help=("Goal status as a label (created | in_progress | archived | "
|
|
189
189
|
"abandoned | completed) or its numeric form (1-5)."),
|
|
190
190
|
),
|
|
191
|
+
"group_uid": opt(
|
|
192
|
+
"--group-uid",
|
|
193
|
+
help="Group UID for a group-scoped update (e.g. editing a goal you "
|
|
194
|
+
"created on a mentee's behalf). Omit for your personal scope."),
|
|
191
195
|
},
|
|
192
196
|
)
|
|
193
197
|
async def update_goal(
|
|
@@ -206,6 +210,7 @@ async def update_goal(
|
|
|
206
210
|
outcome_shape: str | None = None,
|
|
207
211
|
outcome_names: list[str] | None = None,
|
|
208
212
|
status: str | None = None,
|
|
213
|
+
group_uid: str | None = None,
|
|
209
214
|
) -> dict[str, Any]:
|
|
210
215
|
"""Update an existing goal. Only provided (non-None) fields are changed.
|
|
211
216
|
|
|
@@ -227,6 +232,9 @@ async def update_goal(
|
|
|
227
232
|
2=IN_PROGRESS, 3=ARCHIVED, 4=ABANDONED, 5=COMPLETED) or a label
|
|
228
233
|
string ("created" … "completed", case-insensitive). The op
|
|
229
234
|
normalizes labels→int so the CLI/MCP surfaces share the behavior.
|
|
235
|
+
group_uid: Group UID for a group-scoped update. Required to edit a goal
|
|
236
|
+
that lives in a group (e.g. one created on a mentee's behalf);
|
|
237
|
+
omit for personal-scope goals.
|
|
230
238
|
|
|
231
239
|
Returns:
|
|
232
240
|
Updated goal dict
|
|
@@ -260,26 +268,43 @@ async def update_goal(
|
|
|
260
268
|
body["outcome_names"] = outcome_names
|
|
261
269
|
if status is not None:
|
|
262
270
|
body["status"] = _normalize_status(status)
|
|
263
|
-
|
|
271
|
+
path = f"/api/v1/goals/{uid}"
|
|
272
|
+
if group_uid is not None:
|
|
273
|
+
path += f"?group_uid={group_uid}"
|
|
274
|
+
return await client.put(path, json=body)
|
|
264
275
|
|
|
265
276
|
|
|
266
277
|
@operation(
|
|
267
278
|
cli="goals delete",
|
|
268
279
|
summary="Delete a goal permanently.",
|
|
269
280
|
covers=[("DELETE", "/api/v1/goals/{uid}")],
|
|
270
|
-
params={
|
|
281
|
+
params={
|
|
282
|
+
"uid": arg(help="Goal UID to delete"),
|
|
283
|
+
"group_uid": opt(
|
|
284
|
+
"--group-uid",
|
|
285
|
+
help="Group UID for a group-scoped delete (e.g. a goal you created "
|
|
286
|
+
"on a mentee's behalf). Omit for your personal scope."),
|
|
287
|
+
},
|
|
271
288
|
)
|
|
272
|
-
async def delete_goal(
|
|
289
|
+
async def delete_goal(
|
|
290
|
+
client: TetraClient, uid: str, *, group_uid: str | None = None,
|
|
291
|
+
) -> dict[str, Any]:
|
|
273
292
|
"""Delete a goal permanently.
|
|
274
293
|
|
|
275
294
|
Args:
|
|
276
295
|
client: Authenticated TetraClient
|
|
277
296
|
uid: Goal UID to delete
|
|
297
|
+
group_uid: Group UID for a group-scoped delete. Required to delete a
|
|
298
|
+
goal that lives in a group (e.g. one created on a mentee's behalf);
|
|
299
|
+
omit for personal-scope goals.
|
|
278
300
|
|
|
279
301
|
Returns:
|
|
280
302
|
Deletion confirmation dict
|
|
281
303
|
"""
|
|
282
|
-
|
|
304
|
+
path = f"/api/v1/goals/{uid}"
|
|
305
|
+
if group_uid is not None:
|
|
306
|
+
path += f"?group_uid={group_uid}"
|
|
307
|
+
return await client.delete(path)
|
|
283
308
|
|
|
284
309
|
|
|
285
310
|
@operation(
|
|
@@ -476,6 +501,10 @@ async def delete_goal_metric(
|
|
|
476
501
|
help="New parent goal name (omit to keep current)."),
|
|
477
502
|
"new_priority": opt("--priority", min=1,
|
|
478
503
|
help="New priority among siblings (1 = top)."),
|
|
504
|
+
"group_uid": opt(
|
|
505
|
+
"--group-uid",
|
|
506
|
+
help="Group UID for a group-scoped move (e.g. a mentee's goal). "
|
|
507
|
+
"Omit for your personal scope."),
|
|
479
508
|
},
|
|
480
509
|
)
|
|
481
510
|
async def move_goal(
|
|
@@ -484,6 +513,7 @@ async def move_goal(
|
|
|
484
513
|
uid: str,
|
|
485
514
|
new_parent: str | None = None,
|
|
486
515
|
new_priority: int | None = None,
|
|
516
|
+
group_uid: str | None = None,
|
|
487
517
|
) -> dict[str, Any]:
|
|
488
518
|
"""Move a goal to a new parent and/or position in the sibling order.
|
|
489
519
|
|
|
@@ -508,8 +538,12 @@ async def move_goal(
|
|
|
508
538
|
body: dict[str, Any] = {"uid": uid}
|
|
509
539
|
if new_parent is None:
|
|
510
540
|
# Caller didn't specify — pre-fetch and use current parent so the
|
|
511
|
-
# API doesn't interpret missing as "move to root."
|
|
512
|
-
|
|
541
|
+
# API doesn't interpret missing as "move to root." Scope the read to
|
|
542
|
+
# the same group so a mentee's goal resolves.
|
|
543
|
+
read_path = f"/api/v1/goals/{uid}"
|
|
544
|
+
if group_uid is not None:
|
|
545
|
+
read_path += f"?group_uid={group_uid}"
|
|
546
|
+
current = await client.get(read_path)
|
|
513
547
|
if current.get("parent"):
|
|
514
548
|
body["new_parent"] = current["parent"]
|
|
515
549
|
# else: goal is already a root, omit new_parent (API treats omitted
|
|
@@ -521,6 +555,8 @@ async def move_goal(
|
|
|
521
555
|
body["new_parent"] = new_parent
|
|
522
556
|
if new_priority is not None:
|
|
523
557
|
body["new_priority"] = new_priority
|
|
558
|
+
if group_uid is not None:
|
|
559
|
+
body["group_uid"] = group_uid
|
|
524
560
|
return await client.post("/api/v1/goals/move", json=body)
|
|
525
561
|
|
|
526
562
|
|
|
@@ -574,6 +610,10 @@ async def batch_delete_goals(
|
|
|
574
610
|
"updates": opt("--updates", json=True,
|
|
575
611
|
help="JSON map of {goal_uid: new_priority} (1-based, sequential)."),
|
|
576
612
|
"parent": opt("--parent", help="Parent goal name (omit for root siblings)."),
|
|
613
|
+
"group_uid": opt(
|
|
614
|
+
"--group-uid",
|
|
615
|
+
help="Group UID for a group-scoped reorder (e.g. a mentee's goals). "
|
|
616
|
+
"Omit for your personal scope."),
|
|
577
617
|
},
|
|
578
618
|
)
|
|
579
619
|
async def batch_update_goal_priorities(
|
|
@@ -581,6 +621,7 @@ async def batch_update_goal_priorities(
|
|
|
581
621
|
*,
|
|
582
622
|
updates: dict[str, int],
|
|
583
623
|
parent: str | None = None,
|
|
624
|
+
group_uid: str | None = None,
|
|
584
625
|
) -> dict[str, Any]:
|
|
585
626
|
"""Batch-update priorities for goals within one sibling group.
|
|
586
627
|
|
|
@@ -591,6 +632,8 @@ async def batch_update_goal_priorities(
|
|
|
591
632
|
client: Authenticated TetraClient
|
|
592
633
|
updates: Map of ``{goal_uid: new_priority}`` (1 = top).
|
|
593
634
|
parent: Parent goal name (null/omit for root-level siblings).
|
|
635
|
+
group_uid: Group UID for a group-scoped reorder (e.g. a mentee's
|
|
636
|
+
goals); omit for personal scope.
|
|
594
637
|
|
|
595
638
|
Returns:
|
|
596
639
|
Dict with ``updated`` count and the affected ``uids``.
|
|
@@ -598,7 +641,10 @@ async def batch_update_goal_priorities(
|
|
|
598
641
|
body: dict[str, Any] = {"updates": updates}
|
|
599
642
|
if parent is not None:
|
|
600
643
|
body["parent"] = parent
|
|
601
|
-
|
|
644
|
+
path = "/api/v1/goals/batch-update-priorities"
|
|
645
|
+
if group_uid is not None:
|
|
646
|
+
path += f"?group_uid={group_uid}"
|
|
647
|
+
return await client.post(path, json=body)
|
|
602
648
|
|
|
603
649
|
|
|
604
650
|
@operation(
|
|
@@ -610,6 +656,14 @@ async def batch_update_goal_priorities(
|
|
|
610
656
|
help="Source goal UID to merge (repeatable; 2+ required)."),
|
|
611
657
|
"new_goal": opt("--new-goal", json=True,
|
|
612
658
|
help="New goal data as a JSON dict (at least {\"name\": ...})."),
|
|
659
|
+
"group_uid": opt(
|
|
660
|
+
"--group-uid",
|
|
661
|
+
help="Group UID for a group-scoped merge (e.g. a mentee's goals). "
|
|
662
|
+
"Omit for your personal scope."),
|
|
663
|
+
"for_account_uid": opt(
|
|
664
|
+
"--for-account-uid",
|
|
665
|
+
help="Merge on this member's behalf — the merged goal belongs to "
|
|
666
|
+
"them (mentee account UID); requires --group-uid."),
|
|
613
667
|
},
|
|
614
668
|
)
|
|
615
669
|
async def merge_goals(
|
|
@@ -617,6 +671,8 @@ async def merge_goals(
|
|
|
617
671
|
*,
|
|
618
672
|
source_uids: list[str],
|
|
619
673
|
new_goal: dict[str, Any],
|
|
674
|
+
group_uid: str | None = None,
|
|
675
|
+
for_account_uid: str | None = None,
|
|
620
676
|
) -> dict[str, Any]:
|
|
621
677
|
"""Merge two or more goals into a new goal.
|
|
622
678
|
|
|
@@ -628,11 +684,19 @@ async def merge_goals(
|
|
|
628
684
|
source_uids: UIDs of the goals to merge (at least 2).
|
|
629
685
|
new_goal: New goal payload (must include ``name``; may include
|
|
630
686
|
``parent``, ``values``, etc.).
|
|
687
|
+
group_uid: Group UID for a group-scoped merge (e.g. a mentee's
|
|
688
|
+
goals); omit for personal scope.
|
|
689
|
+
for_account_uid: Merge on this member's behalf so the merged goal
|
|
690
|
+
belongs to them (mentee account UID); requires ``group_uid``.
|
|
631
691
|
|
|
632
692
|
Returns:
|
|
633
693
|
The created goal dict.
|
|
634
694
|
"""
|
|
635
695
|
body: dict[str, Any] = {"source_uids": source_uids, "new_goal": new_goal}
|
|
696
|
+
if group_uid is not None:
|
|
697
|
+
body["group_uid"] = group_uid
|
|
698
|
+
if for_account_uid is not None:
|
|
699
|
+
body["for_account_uid"] = for_account_uid
|
|
636
700
|
return await client.post("/api/v1/goals/merge", json=body)
|
|
637
701
|
|
|
638
702
|
|
|
@@ -1085,6 +1149,10 @@ async def get_goal_status_stages(
|
|
|
1085
1149
|
"uid": arg(help="Goal UID"),
|
|
1086
1150
|
"old_name": opt("--old-name", help="Current field name to rename."),
|
|
1087
1151
|
"new_name": opt("--new-name", help="New field name."),
|
|
1152
|
+
"group_uid": opt(
|
|
1153
|
+
"--group-uid",
|
|
1154
|
+
help="Group UID for a group-scoped rename (e.g. a mentee's goal). "
|
|
1155
|
+
"Omit for your personal scope."),
|
|
1088
1156
|
},
|
|
1089
1157
|
)
|
|
1090
1158
|
async def rename_goal_field(
|
|
@@ -1093,6 +1161,7 @@ async def rename_goal_field(
|
|
|
1093
1161
|
*,
|
|
1094
1162
|
old_name: str,
|
|
1095
1163
|
new_name: str,
|
|
1164
|
+
group_uid: str | None = None,
|
|
1096
1165
|
) -> dict[str, Any]:
|
|
1097
1166
|
"""Rename a tuple/record field on a goal and across all its events.
|
|
1098
1167
|
|
|
@@ -1104,13 +1173,18 @@ async def rename_goal_field(
|
|
|
1104
1173
|
uid: Goal UID.
|
|
1105
1174
|
old_name: Existing field name.
|
|
1106
1175
|
new_name: Replacement field name.
|
|
1176
|
+
group_uid: Group UID for a group-scoped rename (e.g. a mentee's
|
|
1177
|
+
goal); omit for personal scope.
|
|
1107
1178
|
|
|
1108
1179
|
Returns:
|
|
1109
1180
|
Dict with ``success``, ``events_updated``, and the new
|
|
1110
1181
|
``outcome_names``.
|
|
1111
1182
|
"""
|
|
1112
1183
|
body: dict[str, Any] = {"old_name": old_name, "new_name": new_name}
|
|
1113
|
-
|
|
1184
|
+
path = f"/api/v1/goals/{uid}/rename-field"
|
|
1185
|
+
if group_uid is not None:
|
|
1186
|
+
path += f"?group_uid={group_uid}"
|
|
1187
|
+
return await client.post(path, json=body)
|
|
1114
1188
|
|
|
1115
1189
|
|
|
1116
1190
|
@operation(
|
|
@@ -1142,3 +1216,97 @@ async def reorder_all_goals(
|
|
|
1142
1216
|
return await client.post(
|
|
1143
1217
|
"/api/v1/goals/reorder-all", json={"order": order}
|
|
1144
1218
|
)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
@operation(
|
|
1222
|
+
cli="goals link-value",
|
|
1223
|
+
summary="Mirror-link a goal to a value: renaming one renames the other.",
|
|
1224
|
+
covers=[("POST", "/api/v1/goals/{uid}/link-value")],
|
|
1225
|
+
params={
|
|
1226
|
+
"uid": arg(help="Goal UID to link"),
|
|
1227
|
+
"value_uid": arg(help="Value UID to link it to (same scope)"),
|
|
1228
|
+
"name_source": opt(
|
|
1229
|
+
"--name-source",
|
|
1230
|
+
choices=["goal", "value"],
|
|
1231
|
+
help="When the leaf names differ, which name wins — the loser "
|
|
1232
|
+
"is renamed to match (with descendant cascade). "
|
|
1233
|
+
"Default: goal.",
|
|
1234
|
+
),
|
|
1235
|
+
"group_uid": opt(
|
|
1236
|
+
"--group-uid",
|
|
1237
|
+
help="Group UID for a group-scoped link (goal + value both in "
|
|
1238
|
+
"that group, e.g. a mentee's). Omit for your personal scope."),
|
|
1239
|
+
},
|
|
1240
|
+
)
|
|
1241
|
+
async def link_goal_value(
|
|
1242
|
+
client: TetraClient,
|
|
1243
|
+
*,
|
|
1244
|
+
uid: str,
|
|
1245
|
+
value_uid: str,
|
|
1246
|
+
name_source: str | None = None,
|
|
1247
|
+
group_uid: str | None = None,
|
|
1248
|
+
) -> dict[str, Any]:
|
|
1249
|
+
"""Create the explicit 1:1 mirror link between a goal and a value.
|
|
1250
|
+
|
|
1251
|
+
Same scope required; a value can be the mirror of at most one goal
|
|
1252
|
+
(409 when it is already linked elsewhere). While linked, leaf renames
|
|
1253
|
+
propagate across the link in both directions. Independent of the
|
|
1254
|
+
draws-from/adds-to assignment in the goal's ``values``.
|
|
1255
|
+
|
|
1256
|
+
Args:
|
|
1257
|
+
client: Authenticated TetraClient
|
|
1258
|
+
uid: Goal UID.
|
|
1259
|
+
value_uid: Value UID in the same scope.
|
|
1260
|
+
name_source: 'goal' or 'value' — which name wins when the leaves
|
|
1261
|
+
differ (the loser is renamed with cascade). Defaults to 'goal'.
|
|
1262
|
+
group_uid: Group UID for a group-scoped link (both goal and value
|
|
1263
|
+
live in that group, e.g. a mentee's); omit for personal scope.
|
|
1264
|
+
|
|
1265
|
+
Returns:
|
|
1266
|
+
The updated goal dict, including ``linked_value`` {uid, name}.
|
|
1267
|
+
"""
|
|
1268
|
+
body: dict[str, Any] = {"value_uid": value_uid}
|
|
1269
|
+
if name_source is not None:
|
|
1270
|
+
body["name_source"] = name_source
|
|
1271
|
+
path = f"/api/v1/goals/{uid}/link-value"
|
|
1272
|
+
if group_uid is not None:
|
|
1273
|
+
path += f"?group_uid={group_uid}"
|
|
1274
|
+
return await client.post(path, json=body)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
@operation(
|
|
1278
|
+
cli="goals unlink-value",
|
|
1279
|
+
summary="Remove a goal's mirror link; both sides rename independently.",
|
|
1280
|
+
covers=[("DELETE", "/api/v1/goals/{uid}/link-value")],
|
|
1281
|
+
params={
|
|
1282
|
+
"uid": arg(help="Goal UID to unlink"),
|
|
1283
|
+
"group_uid": opt(
|
|
1284
|
+
"--group-uid",
|
|
1285
|
+
help="Group UID for a group-scoped unlink (e.g. a mentee's goal). "
|
|
1286
|
+
"Omit for your personal scope."),
|
|
1287
|
+
},
|
|
1288
|
+
)
|
|
1289
|
+
async def unlink_goal_value(
|
|
1290
|
+
client: TetraClient,
|
|
1291
|
+
*,
|
|
1292
|
+
uid: str,
|
|
1293
|
+
group_uid: str | None = None,
|
|
1294
|
+
) -> dict[str, Any]:
|
|
1295
|
+
"""Remove the goal's explicit value link.
|
|
1296
|
+
|
|
1297
|
+
The draws-from/adds-to assignment in ``values`` is kept — unlinking
|
|
1298
|
+
only stops rename mirroring.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
client: Authenticated TetraClient
|
|
1302
|
+
uid: Goal UID.
|
|
1303
|
+
group_uid: Group UID for a group-scoped unlink (e.g. a mentee's
|
|
1304
|
+
goal); omit for personal scope.
|
|
1305
|
+
|
|
1306
|
+
Returns:
|
|
1307
|
+
The updated goal dict with ``linked_value: null``.
|
|
1308
|
+
"""
|
|
1309
|
+
path = f"/api/v1/goals/{uid}/link-value"
|
|
1310
|
+
if group_uid is not None:
|
|
1311
|
+
path += f"?group_uid={group_uid}"
|
|
1312
|
+
return await client.delete(path)
|