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.
Files changed (62) hide show
  1. tetra_cli/__init__.py +6 -0
  2. tetra_cli/api_client/__init__.py +10 -0
  3. tetra_cli/api_client/client.py +173 -0
  4. tetra_cli/api_client/config.py +125 -0
  5. tetra_cli/api_client/operations/__init__.py +9 -0
  6. tetra_cli/api_client/operations/accounts.py +303 -0
  7. tetra_cli/api_client/operations/ai.py +278 -0
  8. tetra_cli/api_client/operations/analysis.py +190 -0
  9. tetra_cli/api_client/operations/api_keys.py +145 -0
  10. tetra_cli/api_client/operations/archive.py +114 -0
  11. tetra_cli/api_client/operations/awards.py +123 -0
  12. tetra_cli/api_client/operations/capacity.py +84 -0
  13. tetra_cli/api_client/operations/conversations.py +447 -0
  14. tetra_cli/api_client/operations/conversations_2.py +262 -0
  15. tetra_cli/api_client/operations/cosmetics.py +148 -0
  16. tetra_cli/api_client/operations/dashboard.py +282 -0
  17. tetra_cli/api_client/operations/data.py +250 -0
  18. tetra_cli/api_client/operations/events.py +734 -0
  19. tetra_cli/api_client/operations/gamification.py +470 -0
  20. tetra_cli/api_client/operations/goals.py +1144 -0
  21. tetra_cli/api_client/operations/groups.py +647 -0
  22. tetra_cli/api_client/operations/issues.py +198 -0
  23. tetra_cli/api_client/operations/offset.py +61 -0
  24. tetra_cli/api_client/operations/onboarding.py +284 -0
  25. tetra_cli/api_client/operations/outcome_schemas.py +292 -0
  26. tetra_cli/api_client/operations/peer_connections.py +243 -0
  27. tetra_cli/api_client/operations/plaid.py +329 -0
  28. tetra_cli/api_client/operations/reminders.py +273 -0
  29. tetra_cli/api_client/operations/scratches.py +280 -0
  30. tetra_cli/api_client/operations/skill_trees.py +160 -0
  31. tetra_cli/api_client/operations/social_2.py +560 -0
  32. tetra_cli/api_client/operations/social_3.py +618 -0
  33. tetra_cli/api_client/operations/social_4.py +527 -0
  34. tetra_cli/api_client/operations/strava.py +215 -0
  35. tetra_cli/api_client/operations/stripe.py +113 -0
  36. tetra_cli/api_client/operations/tags.py +488 -0
  37. tetra_cli/api_client/operations/values.py +867 -0
  38. tetra_cli/api_client/operations/values_2.py +584 -0
  39. tetra_cli/api_client/operations/watch.py +105 -0
  40. tetra_cli/api_client/operations/webhooks.py +50 -0
  41. tetra_cli/api_client/operations/xp.py +27 -0
  42. tetra_cli/cli/__init__.py +5 -0
  43. tetra_cli/cli/__main__.py +5 -0
  44. tetra_cli/cli/app.py +86 -0
  45. tetra_cli/cli/commands/__init__.py +1 -0
  46. tetra_cli/cli/commands/auth.py +201 -0
  47. tetra_cli/cli/commands/guide.py +8 -0
  48. tetra_cli/cli/commands/messages.py +161 -0
  49. tetra_cli/cli/commands/skill.py +71 -0
  50. tetra_cli/cli/context.py +13 -0
  51. tetra_cli/cli/generate.py +282 -0
  52. tetra_cli/cli/output.py +58 -0
  53. tetra_cli/mcp_gen.py +137 -0
  54. tetra_cli/ontology.py +70 -0
  55. tetra_cli/registry.py +118 -0
  56. tetra_cli/skill/SKILL.md +69 -0
  57. tetra_cli/skill/__init__.py +1 -0
  58. tetra_cli-0.2.0.dist-info/METADATA +140 -0
  59. tetra_cli-0.2.0.dist-info/RECORD +62 -0
  60. tetra_cli-0.2.0.dist-info/WHEEL +5 -0
  61. tetra_cli-0.2.0.dist-info/entry_points.txt +2 -0
  62. 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
+ )