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,584 @@
|
|
|
1
|
+
"""Pure async operations for value analytics, status, and sharing.
|
|
2
|
+
|
|
3
|
+
This module covers the second slice of the values API surface — per-value
|
|
4
|
+
analytics (status, balance, projections, tag breakdowns, goal progress),
|
|
5
|
+
rule history, affordability checks, calendar export, skill-tree
|
|
6
|
+
publishing, and the projection debug exports. The base CRUD ops live in
|
|
7
|
+
``values.py``; nothing here duplicates a route owned by that file.
|
|
8
|
+
|
|
9
|
+
Each op is a thin async wrapper over :class:`TetraClient`. Optional query
|
|
10
|
+
and body fields are omitted when ``None`` so the backend applies its own
|
|
11
|
+
defaults. Money is expressed in cents and time in seconds at the wire
|
|
12
|
+
level (mirroring the Pydantic request/response models), so values pass
|
|
13
|
+
through unchanged.
|
|
14
|
+
"""
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from tetra_cli.api_client import TetraClient
|
|
18
|
+
from tetra_cli.registry import arg, operation, opt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@operation(
|
|
22
|
+
cli="values status",
|
|
23
|
+
summary="Get cumulative status for a value (timeline, balance, limits).",
|
|
24
|
+
covers=[("GET", "/api/v1/values/{uid}/status")],
|
|
25
|
+
params={
|
|
26
|
+
"uid": arg(help="Value UID"),
|
|
27
|
+
"group_uid": opt("--group-uid", help="Group UID for group-scoped status."),
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
async def get_value_status(
|
|
31
|
+
client: TetraClient,
|
|
32
|
+
uid: str,
|
|
33
|
+
*,
|
|
34
|
+
group_uid: str | None = None,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Get cumulative status for a value, including timeline and balance.
|
|
37
|
+
|
|
38
|
+
Returns current total, the event timeline, expected-vs-actual time and
|
|
39
|
+
money, surplus/deficit, hierarchy buffering, and limit validation.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
client: Authenticated TetraClient.
|
|
43
|
+
uid: Value UID.
|
|
44
|
+
group_uid: Optional group scope.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Status dict with legacy timeline fields plus balance fields.
|
|
48
|
+
"""
|
|
49
|
+
params: dict[str, Any] = {}
|
|
50
|
+
if group_uid is not None:
|
|
51
|
+
params["group_uid"] = group_uid
|
|
52
|
+
return await client.get(
|
|
53
|
+
f"/api/v1/values/{uid}/status", params=params or None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@operation(
|
|
58
|
+
cli="values project-balance",
|
|
59
|
+
summary="Project a value's future balance from historical add/subtract rates.",
|
|
60
|
+
covers=[("GET", "/api/v1/values/{uid}/project-balance")],
|
|
61
|
+
params={
|
|
62
|
+
"uid": arg(help="Value UID"),
|
|
63
|
+
"days": opt("--days", min=1, max=365,
|
|
64
|
+
help="Days ahead to project (1-365, default 30)."),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
async def project_value_balance(
|
|
68
|
+
client: TetraClient,
|
|
69
|
+
uid: str,
|
|
70
|
+
*,
|
|
71
|
+
days: int = 30,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""Project a value's future balance based on historical rates.
|
|
74
|
+
|
|
75
|
+
Uses the average add/subtract rate from the last 30 days to estimate
|
|
76
|
+
the balance ``days`` into the future ("Will I have enough PTO?").
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
client: Authenticated TetraClient.
|
|
80
|
+
uid: Value UID.
|
|
81
|
+
days: Days ahead to project (1-365, default 30).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Projection dict with projected balance and supporting figures.
|
|
85
|
+
"""
|
|
86
|
+
return await client.get(
|
|
87
|
+
f"/api/v1/values/{uid}/project-balance", params={"days": days},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@operation(
|
|
92
|
+
cli="values rule-projection",
|
|
93
|
+
summary="Project a value's balance by simulating each rule on its own schedule.",
|
|
94
|
+
covers=[("GET", "/api/v1/values/{uid}/rule-projection")],
|
|
95
|
+
params={
|
|
96
|
+
"uid": arg(help="Value UID"),
|
|
97
|
+
"months": opt("--months", min=1, max=24,
|
|
98
|
+
help="Months to project (1-24, default 12)."),
|
|
99
|
+
"group_uid": opt("--group-uid", help="Group UID for group context."),
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
async def get_value_rule_projection(
|
|
103
|
+
client: TetraClient,
|
|
104
|
+
uid: str,
|
|
105
|
+
*,
|
|
106
|
+
months: int = 12,
|
|
107
|
+
start_money_balance: int | None = None,
|
|
108
|
+
start_time_balance: int | None = None,
|
|
109
|
+
group_uid: str | None = None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""Project a value's balance using its rule schedules.
|
|
112
|
+
|
|
113
|
+
Unlike the weekly-normalized projection, this simulates each rule
|
|
114
|
+
firing at its natural cadence (bills on the 10th, paychecks every other
|
|
115
|
+
Friday) to produce a stepped trajectory.
|
|
116
|
+
|
|
117
|
+
Money balances are in cents and time balances in seconds, matching the
|
|
118
|
+
backend query contract.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
client: Authenticated TetraClient.
|
|
122
|
+
uid: Value UID.
|
|
123
|
+
months: Months to project (1-24, default 12).
|
|
124
|
+
start_money_balance: Current money balance in cents.
|
|
125
|
+
start_time_balance: Current time balance in seconds.
|
|
126
|
+
group_uid: Optional group context (scales rules by member count).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Rule-projection dict with ``points`` and per-point ``deltas``.
|
|
130
|
+
"""
|
|
131
|
+
params: dict[str, Any] = {"months": months}
|
|
132
|
+
if start_money_balance is not None:
|
|
133
|
+
params["start_money_balance"] = start_money_balance
|
|
134
|
+
if start_time_balance is not None:
|
|
135
|
+
params["start_time_balance"] = start_time_balance
|
|
136
|
+
if group_uid is not None:
|
|
137
|
+
params["group_uid"] = group_uid
|
|
138
|
+
return await client.get(
|
|
139
|
+
f"/api/v1/values/{uid}/rule-projection", params=params,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@operation(
|
|
144
|
+
cli="values goal-progress",
|
|
145
|
+
summary="Get time-to-goal progress for a value with a max_money target.",
|
|
146
|
+
covers=[("GET", "/api/v1/values/{uid}/goal-progress")],
|
|
147
|
+
params={"uid": arg(help="Value UID")},
|
|
148
|
+
)
|
|
149
|
+
async def get_value_goal_progress(
|
|
150
|
+
client: TetraClient, uid: str,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Get goal progress for a value that has a max_money target.
|
|
153
|
+
|
|
154
|
+
Returns the time-to-goal calculation derived from the value's scheduled
|
|
155
|
+
money rule (current balance, target, remaining, rate, expected
|
|
156
|
+
completion date, progress percentage).
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
client: Authenticated TetraClient.
|
|
160
|
+
uid: Value UID.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Goal-progress dict, or a dict with error flags if not computable.
|
|
164
|
+
"""
|
|
165
|
+
return await client.get(f"/api/v1/values/{uid}/goal-progress")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@operation(
|
|
169
|
+
cli="values tag-analytics",
|
|
170
|
+
summary="Get per-tag totals for a value's events within a date range.",
|
|
171
|
+
covers=[("GET", "/api/v1/values/{uid}/tag-analytics")],
|
|
172
|
+
params={
|
|
173
|
+
"uid": arg(help="Value UID"),
|
|
174
|
+
"group_uid": opt("--group-uid", help="Group UID for group-scoped analytics."),
|
|
175
|
+
"target_account_uid": opt("--target-account-uid",
|
|
176
|
+
help="Target member account UID (group context)."),
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
async def get_value_tag_analytics(
|
|
180
|
+
client: TetraClient,
|
|
181
|
+
uid: str,
|
|
182
|
+
*,
|
|
183
|
+
start_date: str | None = None,
|
|
184
|
+
end_date: str | None = None,
|
|
185
|
+
group_uid: str | None = None,
|
|
186
|
+
target_account_uid: str | None = None,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""Get per-tag totals for a value's events within a date range.
|
|
189
|
+
|
|
190
|
+
Money mode is detected automatically. Events with multiple tags are
|
|
191
|
+
fully attributed to each; untagged events are excluded.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
client: Authenticated TetraClient.
|
|
195
|
+
uid: Value UID.
|
|
196
|
+
start_date: Range start (YYYY-MM-DD, default 6 months ago).
|
|
197
|
+
end_date: Range end (YYYY-MM-DD, default today).
|
|
198
|
+
group_uid: Optional group scope.
|
|
199
|
+
target_account_uid: Optional target member for group context.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Tag-analytics dict with per-tag totals.
|
|
203
|
+
"""
|
|
204
|
+
params: dict[str, Any] = {}
|
|
205
|
+
if start_date is not None:
|
|
206
|
+
params["start_date"] = start_date
|
|
207
|
+
if end_date is not None:
|
|
208
|
+
params["end_date"] = end_date
|
|
209
|
+
if group_uid is not None:
|
|
210
|
+
params["group_uid"] = group_uid
|
|
211
|
+
if target_account_uid is not None:
|
|
212
|
+
params["target_account_uid"] = target_account_uid
|
|
213
|
+
return await client.get(
|
|
214
|
+
f"/api/v1/values/{uid}/tag-analytics", params=params or None,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@operation(
|
|
219
|
+
cli="values tag-time-series",
|
|
220
|
+
summary="Get per-tag totals bucketed by week/month for a value's events.",
|
|
221
|
+
covers=[("GET", "/api/v1/values/{uid}/tag-time-series")],
|
|
222
|
+
params={
|
|
223
|
+
"uid": arg(help="Value UID"),
|
|
224
|
+
"granularity": opt("--granularity", choices=["month", "week"],
|
|
225
|
+
help="Bucket granularity (default month)."),
|
|
226
|
+
"group_uid": opt("--group-uid", help="Group UID for group-scoped analytics."),
|
|
227
|
+
"target_account_uid": opt("--target-account-uid",
|
|
228
|
+
help="Target member account UID (group context)."),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
async def get_value_tag_time_series(
|
|
232
|
+
client: TetraClient,
|
|
233
|
+
uid: str,
|
|
234
|
+
*,
|
|
235
|
+
granularity: str = "month",
|
|
236
|
+
start_date: str | None = None,
|
|
237
|
+
end_date: str | None = None,
|
|
238
|
+
group_uid: str | None = None,
|
|
239
|
+
target_account_uid: str | None = None,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Get per-tag totals bucketed by time period for a value's events.
|
|
242
|
+
|
|
243
|
+
Aggregates event amounts by tag into monthly or weekly buckets for
|
|
244
|
+
time-series charting. Empty periods within the range are included so
|
|
245
|
+
the chart axis stays continuous.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
client: Authenticated TetraClient.
|
|
249
|
+
uid: Value UID.
|
|
250
|
+
granularity: ``month`` or ``week`` (default ``month``).
|
|
251
|
+
start_date: Range start (YYYY-MM-DD, default 6 months ago).
|
|
252
|
+
end_date: Range end (YYYY-MM-DD, default today).
|
|
253
|
+
group_uid: Optional group scope.
|
|
254
|
+
target_account_uid: Optional target member for group context.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Tag time-series dict with per-tag bucketed totals.
|
|
258
|
+
"""
|
|
259
|
+
params: dict[str, Any] = {"granularity": granularity}
|
|
260
|
+
if start_date is not None:
|
|
261
|
+
params["start_date"] = start_date
|
|
262
|
+
if end_date is not None:
|
|
263
|
+
params["end_date"] = end_date
|
|
264
|
+
if group_uid is not None:
|
|
265
|
+
params["group_uid"] = group_uid
|
|
266
|
+
if target_account_uid is not None:
|
|
267
|
+
params["target_account_uid"] = target_account_uid
|
|
268
|
+
return await client.get(
|
|
269
|
+
f"/api/v1/values/{uid}/tag-time-series", params=params,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@operation(
|
|
274
|
+
cli="values week-events",
|
|
275
|
+
summary="Get this-week and last-week events for a value, with summaries.",
|
|
276
|
+
covers=[("GET", "/api/v1/values/{uid}/week-events")],
|
|
277
|
+
params={
|
|
278
|
+
"uid": arg(help="Value UID"),
|
|
279
|
+
"group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
|
|
280
|
+
"target_account_uid": opt("--target-account-uid",
|
|
281
|
+
help="View a specific member's week events."),
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
async def get_value_week_events(
|
|
285
|
+
client: TetraClient,
|
|
286
|
+
uid: str,
|
|
287
|
+
*,
|
|
288
|
+
group_uid: str | None = None,
|
|
289
|
+
target_account_uid: str | None = None,
|
|
290
|
+
) -> dict[str, Any]:
|
|
291
|
+
"""Get this-week and last-week events for a value, with summaries.
|
|
292
|
+
|
|
293
|
+
Each bucket carries summary statistics (total_events,
|
|
294
|
+
total_duration_seconds, avg_feels), used by the week-comparison
|
|
295
|
+
drill-down.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
client: Authenticated TetraClient.
|
|
299
|
+
uid: Value UID.
|
|
300
|
+
group_uid: Optional group scope.
|
|
301
|
+
target_account_uid: Optional member to view.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dict with ``this_week`` and ``last_week`` event lists + summaries.
|
|
305
|
+
"""
|
|
306
|
+
params: dict[str, Any] = {}
|
|
307
|
+
if group_uid is not None:
|
|
308
|
+
params["group_uid"] = group_uid
|
|
309
|
+
if target_account_uid is not None:
|
|
310
|
+
params["target_account_uid"] = target_account_uid
|
|
311
|
+
return await client.get(
|
|
312
|
+
f"/api/v1/values/{uid}/week-events", params=params or None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@operation(
|
|
317
|
+
cli="values rule-history",
|
|
318
|
+
summary="Get the rule-change history for a value (newest first).",
|
|
319
|
+
covers=[("GET", "/api/v1/values/{uid}/rule-history")],
|
|
320
|
+
params={
|
|
321
|
+
"uid": arg(help="Value UID"),
|
|
322
|
+
"field": opt("--field", choices=["time_rule", "money_rule"],
|
|
323
|
+
help="Filter by field (time_rule or money_rule)."),
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
async def get_value_rule_history(
|
|
327
|
+
client: TetraClient,
|
|
328
|
+
uid: str,
|
|
329
|
+
*,
|
|
330
|
+
field: str | None = None,
|
|
331
|
+
) -> list[dict[str, Any]]:
|
|
332
|
+
"""Get the rule-change history for a value.
|
|
333
|
+
|
|
334
|
+
Returns entries sorted by ``changed_at`` (newest first); each carries
|
|
335
|
+
``field``, ``previous``, ``new``, ``changed_at``, ``effective_from``.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
client: Authenticated TetraClient.
|
|
339
|
+
uid: Value UID.
|
|
340
|
+
field: Optional filter — ``time_rule`` or ``money_rule``.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
List of rule-history entry dicts.
|
|
344
|
+
"""
|
|
345
|
+
params: dict[str, Any] = {}
|
|
346
|
+
if field is not None:
|
|
347
|
+
params["field"] = field
|
|
348
|
+
return await client.get(
|
|
349
|
+
f"/api/v1/values/{uid}/rule-history", params=params or None,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@operation(
|
|
354
|
+
cli="values status-recalculate",
|
|
355
|
+
summary="Clear the value status cache and recalculate all value statuses.",
|
|
356
|
+
covers=[("POST", "/api/v1/values/status/recalculate")],
|
|
357
|
+
)
|
|
358
|
+
async def recalculate_all_values(client: TetraClient) -> dict[str, Any]:
|
|
359
|
+
"""Clear all value status cache and recalculate every value's status.
|
|
360
|
+
|
|
361
|
+
Forces a fresh recalculation of all value balances and statuses after
|
|
362
|
+
clearing cached results. The endpoint takes no request body.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
client: Authenticated TetraClient.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dict with ``cache_cleared`` and ``values_recalculated`` counts.
|
|
369
|
+
"""
|
|
370
|
+
return await client.post("/api/v1/values/status/recalculate")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@operation(
|
|
374
|
+
cli="values can-afford",
|
|
375
|
+
summary="Check whether a value's current balance can afford an amount.",
|
|
376
|
+
covers=[("POST", "/api/v1/values/{uid}/can-afford")],
|
|
377
|
+
params={"uid": arg(help="Value UID")},
|
|
378
|
+
)
|
|
379
|
+
async def check_can_afford(
|
|
380
|
+
client: TetraClient,
|
|
381
|
+
uid: str,
|
|
382
|
+
*,
|
|
383
|
+
amount: float,
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
"""Check whether a value's balance can afford a given amount.
|
|
386
|
+
|
|
387
|
+
Useful before planning an activity that draws down PTO, a budget, or
|
|
388
|
+
another accumulated resource. ``amount`` is in the value's native unit
|
|
389
|
+
(cents for money values, seconds for time values), matching the
|
|
390
|
+
request schema.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
client: Authenticated TetraClient.
|
|
394
|
+
uid: Value UID.
|
|
395
|
+
amount: Amount to test against the current balance.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dict with ``can_afford``, ``current_balance``, ``requested_amount``,
|
|
399
|
+
``shortfall``.
|
|
400
|
+
"""
|
|
401
|
+
return await client.post(
|
|
402
|
+
f"/api/v1/values/{uid}/can-afford", json={"amount": amount},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@operation(
|
|
407
|
+
cli="values export-skill-tree",
|
|
408
|
+
summary="Serialize a value subtree as a shareable skill-tree template.",
|
|
409
|
+
covers=[("POST", "/api/v1/values/{uid}/export-skill-tree")],
|
|
410
|
+
params={"uid": arg(help="Root value UID")},
|
|
411
|
+
)
|
|
412
|
+
async def export_value_as_skill_tree(
|
|
413
|
+
client: TetraClient, uid: str,
|
|
414
|
+
) -> dict[str, Any]:
|
|
415
|
+
"""Serialize a value subtree as a shareable skill-tree template.
|
|
416
|
+
|
|
417
|
+
Captures structure only (values + goals), not events or personal
|
|
418
|
+
progress, so the template can be published or adopted by others.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
client: Authenticated TetraClient.
|
|
422
|
+
uid: Root value UID of the subtree to export.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Skill-tree template dict.
|
|
426
|
+
"""
|
|
427
|
+
return await client.post(f"/api/v1/values/{uid}/export-skill-tree")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@operation(
|
|
431
|
+
cli="values share-tree",
|
|
432
|
+
summary="Publish a value's skill-tree subtree into a group's pool.",
|
|
433
|
+
covers=[("POST", "/api/v1/values/{value_name}/share-tree")],
|
|
434
|
+
params={
|
|
435
|
+
"value_name": arg(help="Full hierarchical name of the root value"),
|
|
436
|
+
"icon": opt("--icon", help="Emoji to store on the Arena hub tile."),
|
|
437
|
+
"target_group_uid": opt("--target-group-uid",
|
|
438
|
+
help="Target group UID (default: Everyone)."),
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
async def share_tree(
|
|
442
|
+
client: TetraClient,
|
|
443
|
+
value_name: str,
|
|
444
|
+
*,
|
|
445
|
+
icon: str | None = None,
|
|
446
|
+
target_group_uid: str | None = None,
|
|
447
|
+
also_everyone: bool = True,
|
|
448
|
+
) -> dict[str, Any]:
|
|
449
|
+
"""Publish a personal value's skill-tree subtree into a group's pool.
|
|
450
|
+
|
|
451
|
+
Creates (or resolves) a cohort value in the target group, shares every
|
|
452
|
+
goal bound to the subtree, and enrolls the caller in the cohort
|
|
453
|
+
ranking. Defaults to the Everyone group; publishing into a specific
|
|
454
|
+
group also enrolls in Everyone unless ``also_everyone`` is False.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
client: Authenticated TetraClient.
|
|
458
|
+
value_name: Full hierarchical name of the root value.
|
|
459
|
+
icon: Optional emoji for the Arena hub tile.
|
|
460
|
+
target_group_uid: Optional target group UID (default Everyone).
|
|
461
|
+
also_everyone: Also enroll in Everyone when publishing to a specific
|
|
462
|
+
group (default True; ignored for Everyone).
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Dict with ``cohort_value_uid`` and ``shared_goal_count``.
|
|
466
|
+
"""
|
|
467
|
+
body: dict[str, Any] = {"also_everyone": also_everyone}
|
|
468
|
+
if icon is not None:
|
|
469
|
+
body["icon"] = icon
|
|
470
|
+
if target_group_uid is not None:
|
|
471
|
+
body["target_group_uid"] = target_group_uid
|
|
472
|
+
return await client.post(
|
|
473
|
+
f"/api/v1/values/{value_name}/share-tree", json=body,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@operation(
|
|
478
|
+
cli="values ical",
|
|
479
|
+
summary="Export a value's recurrence schedule as an iCal (.ics) file.",
|
|
480
|
+
covers=[("GET", "/api/v1/values/{uid}/ical")],
|
|
481
|
+
params={"uid": arg(help="Value UID")},
|
|
482
|
+
output="file",
|
|
483
|
+
)
|
|
484
|
+
async def export_value_ical(client: TetraClient, uid: str) -> tuple[bytes, str]:
|
|
485
|
+
"""Export a value's schedule as an iCal (.ics) file.
|
|
486
|
+
|
|
487
|
+
Generates RFC 5545 VEVENT/RRULE entries from the value's schedule-type
|
|
488
|
+
time rule. Only works for values that have a schedule time rule.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
client: Authenticated TetraClient.
|
|
492
|
+
uid: Value UID.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
A ``(content_bytes, filename)`` tuple of iCal content (text/calendar).
|
|
496
|
+
"""
|
|
497
|
+
return await client.download(f"/api/v1/values/{uid}/ical")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@operation(
|
|
501
|
+
cli="values projection-debug-csv",
|
|
502
|
+
summary="Export the week-by-week projection debug CSV for one value.",
|
|
503
|
+
covers=[("GET", "/api/v1/values/analytics/projection/debug-csv")],
|
|
504
|
+
params={
|
|
505
|
+
"value_name": opt("--value-name", help="Value name to debug (required)."),
|
|
506
|
+
},
|
|
507
|
+
output="file",
|
|
508
|
+
)
|
|
509
|
+
async def get_value_projection_debug_csv(
|
|
510
|
+
client: TetraClient,
|
|
511
|
+
*,
|
|
512
|
+
value_name: str,
|
|
513
|
+
start_date: str | None = None,
|
|
514
|
+
end_date: str | None = None,
|
|
515
|
+
include_virtual_events: bool = False,
|
|
516
|
+
) -> tuple[bytes, str]:
|
|
517
|
+
"""Export a detailed week-by-week projection debug CSV for one value.
|
|
518
|
+
|
|
519
|
+
Shows running balance, SET events vs carryover, time-rule expected
|
|
520
|
+
values, draws_from breakdown, and optionally virtual events for
|
|
521
|
+
unfilled scheduled time. The date range defaults to 8 weeks back
|
|
522
|
+
through 8 weeks forward.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
client: Authenticated TetraClient.
|
|
526
|
+
value_name: Name of the value to debug.
|
|
527
|
+
start_date: Range start (YYYY-MM-DD); defaults to 8 weeks back.
|
|
528
|
+
end_date: Range end (YYYY-MM-DD); defaults to 8 weeks forward.
|
|
529
|
+
include_virtual_events: Include virtual events for unfilled time.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
A ``(content_bytes, filename)`` tuple of CSV content (text/csv).
|
|
533
|
+
"""
|
|
534
|
+
params: dict[str, Any] = {
|
|
535
|
+
"value_name": value_name,
|
|
536
|
+
"include_virtual_events": include_virtual_events,
|
|
537
|
+
}
|
|
538
|
+
if start_date is not None:
|
|
539
|
+
params["start_date"] = start_date
|
|
540
|
+
if end_date is not None:
|
|
541
|
+
params["end_date"] = end_date
|
|
542
|
+
return await client.download(
|
|
543
|
+
"/api/v1/values/analytics/projection/debug-csv", params=params,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@operation(
|
|
548
|
+
cli="values projection-debug-zip",
|
|
549
|
+
summary="Export projection debug CSVs for all values as a zip archive.",
|
|
550
|
+
covers=[("GET", "/api/v1/values/analytics/projection/debug-zip")],
|
|
551
|
+
output="file",
|
|
552
|
+
)
|
|
553
|
+
async def get_all_projections_debug_zip(
|
|
554
|
+
client: TetraClient,
|
|
555
|
+
*,
|
|
556
|
+
start_date: str | None = None,
|
|
557
|
+
end_date: str | None = None,
|
|
558
|
+
include_virtual_events: bool = False,
|
|
559
|
+
) -> tuple[bytes, str]:
|
|
560
|
+
"""Export projection debug CSVs for every value with SET events as a zip.
|
|
561
|
+
|
|
562
|
+
Bundles one debug CSV per value that has projection data into a single
|
|
563
|
+
zip archive. The date range defaults to 8 weeks back through 8 weeks
|
|
564
|
+
forward.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
client: Authenticated TetraClient.
|
|
568
|
+
start_date: Range start (YYYY-MM-DD); defaults to 8 weeks back.
|
|
569
|
+
end_date: Range end (YYYY-MM-DD); defaults to 8 weeks forward.
|
|
570
|
+
include_virtual_events: Include virtual events for unfilled time.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
A ``(content_bytes, filename)`` tuple of zip content (application/zip).
|
|
574
|
+
"""
|
|
575
|
+
params: dict[str, Any] = {
|
|
576
|
+
"include_virtual_events": include_virtual_events,
|
|
577
|
+
}
|
|
578
|
+
if start_date is not None:
|
|
579
|
+
params["start_date"] = start_date
|
|
580
|
+
if end_date is not None:
|
|
581
|
+
params["end_date"] = end_date
|
|
582
|
+
return await client.download(
|
|
583
|
+
"/api/v1/values/analytics/projection/debug-zip", params=params,
|
|
584
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Pure async operations for the watch (wearable) API.
|
|
2
|
+
|
|
3
|
+
The watch surface lets a paired wearable list the goals/values it can record
|
|
4
|
+
against and lets the signed-in user approve a pending pairing code. These ops
|
|
5
|
+
are the single source of truth for both the CLI and MCP surfaces — each is a
|
|
6
|
+
thin async wrapper over :class:`TetraClient`.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from tetra_cli.api_client import TetraClient
|
|
11
|
+
from tetra_cli.registry import arg, operation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@operation(
|
|
15
|
+
cli="watch catalog",
|
|
16
|
+
summary="List goals + values the watch can record time against.",
|
|
17
|
+
covers=[("GET", "/api/v1/watch/catalog")],
|
|
18
|
+
)
|
|
19
|
+
async def get_watch_catalog(client: TetraClient) -> dict[str, Any]:
|
|
20
|
+
"""Return the flat list of goals + values the watch can record against.
|
|
21
|
+
|
|
22
|
+
Covers the full account scope (personal + every standard group), deduped
|
|
23
|
+
by UID, each carrying a completion-prompt descriptor. Sorted by recency
|
|
24
|
+
then leaf name; capped at 500 items with ``truncated`` set when exceeded.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
client: Authenticated TetraClient
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with ``items`` and ``truncated``.
|
|
31
|
+
"""
|
|
32
|
+
return await client.get("/api/v1/watch/catalog")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@operation(
|
|
36
|
+
cli="watch start-pair",
|
|
37
|
+
summary="Begin a watch pairing session; returns the user + device codes.",
|
|
38
|
+
covers=[("POST", "/api/v1/watch/pair/start")],
|
|
39
|
+
)
|
|
40
|
+
async def start_watch_pairing(client: TetraClient) -> dict[str, Any]:
|
|
41
|
+
"""Begin a new watch pairing session.
|
|
42
|
+
|
|
43
|
+
Returns the short ``user_code`` the watch displays plus the high-entropy
|
|
44
|
+
``device_code`` it polls with. Both expire in ten minutes. Takes no body.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client: Authenticated TetraClient
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dict with ``user_code``, ``device_code``, and ``expires_at``.
|
|
51
|
+
"""
|
|
52
|
+
return await client.post("/api/v1/watch/pair/start")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@operation(
|
|
56
|
+
cli="watch poll-pair",
|
|
57
|
+
summary="Poll a pending pairing for status; delivers the token once.",
|
|
58
|
+
covers=[("POST", "/api/v1/watch/pair/poll")],
|
|
59
|
+
params={"device_code": arg(help="The high-entropy device_code from start-pair.")},
|
|
60
|
+
)
|
|
61
|
+
async def poll_watch_pairing(
|
|
62
|
+
client: TetraClient, device_code: str,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Poll a pending pairing for status; delivers the raw token exactly once.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
client: Authenticated TetraClient
|
|
68
|
+
device_code: The private poll key returned by start-pair.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
``{"status": "pending"}`` while waiting, ``{"status": "approved",
|
|
72
|
+
"token": ...}`` on the one-time delivery, or ``{"status": "expired"}``.
|
|
73
|
+
"""
|
|
74
|
+
return await client.post(
|
|
75
|
+
"/api/v1/watch/pair/poll", json={"device_code": device_code}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@operation(
|
|
80
|
+
cli="watch approve-pair",
|
|
81
|
+
summary="Approve a pending watch pairing code and mint a watch API key.",
|
|
82
|
+
covers=[("POST", "/api/v1/watch/pair/approve")],
|
|
83
|
+
params={"code": arg(help="The 8-char user code displayed on the watch face.")},
|
|
84
|
+
)
|
|
85
|
+
async def approve_watch_pairing(
|
|
86
|
+
client: TetraClient, code: str,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Approve a pending watch pairing code, linking the watch to the account.
|
|
89
|
+
|
|
90
|
+
The signed-in user confirms they want to link this watch; the watch then
|
|
91
|
+
retrieves its minted token via a single poll call.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
client: Authenticated TetraClient
|
|
95
|
+
code: The 8-char user code currently displayed on the watch face.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
``{"status": "approved"}``.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
TetraClientError: 404 if the code is unknown, already used, or expired.
|
|
102
|
+
"""
|
|
103
|
+
return await client.post(
|
|
104
|
+
"/api/v1/watch/pair/approve", json={"code": code}
|
|
105
|
+
)
|