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,734 @@
|
|
|
1
|
+
"""Pure async operations for the events API."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from tetra_cli.api_client import TetraClient
|
|
5
|
+
from tetra_cli.api_client.operations.tags import resolve_tag_uid
|
|
6
|
+
from tetra_cli.registry import arg, operation, opt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@operation(
|
|
10
|
+
cli="events list",
|
|
11
|
+
summary="List events with optional filtering and pagination.",
|
|
12
|
+
covers=[("GET", "/api/v1/events/")],
|
|
13
|
+
params={
|
|
14
|
+
"on_date": opt("--on-date", help="Filter to a specific date (YYYY-MM-DD)"),
|
|
15
|
+
"start_date": opt("--start", help="Range start, inclusive (YYYY-MM-DD). May be used alone."),
|
|
16
|
+
"end_date": opt("--end", help="Range end, inclusive (YYYY-MM-DD). May be used alone."),
|
|
17
|
+
"limit": opt("--limit", help="Maximum events to return"),
|
|
18
|
+
"offset": opt("--offset", help="Events to skip for pagination"),
|
|
19
|
+
"sort_order": opt("--sort-order", choices=["asc", "desc"],
|
|
20
|
+
help="Sort order (newest first = desc)"),
|
|
21
|
+
"goal": opt("--goal", help="Filter to a goal's full hierarchical name"),
|
|
22
|
+
"tag": opt("--tag", help="Filter to a tag (case-insensitive, hierarchy-expanded)"),
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
async def list_events(
|
|
26
|
+
client: TetraClient,
|
|
27
|
+
*,
|
|
28
|
+
on_date: str | None = None,
|
|
29
|
+
start_date: str | None = None,
|
|
30
|
+
end_date: str | None = None,
|
|
31
|
+
limit: int = 50,
|
|
32
|
+
offset: int = 0,
|
|
33
|
+
sort_order: str = "desc",
|
|
34
|
+
goal: str | None = None,
|
|
35
|
+
tag: str | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""List events with optional filtering and pagination.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
client: Authenticated TetraClient
|
|
41
|
+
on_date: Filter to specific date (YYYY-MM-DD format)
|
|
42
|
+
start_date: Range start, inclusive (YYYY-MM-DD). May be given without
|
|
43
|
+
end_date to fetch everything on/after the date.
|
|
44
|
+
end_date: Range end, inclusive (YYYY-MM-DD). May be given without
|
|
45
|
+
start_date to fetch everything on/before the date.
|
|
46
|
+
limit: Maximum events to return (default 50)
|
|
47
|
+
offset: Number of events to skip for pagination
|
|
48
|
+
sort_order: 'desc' (newest first) or 'asc' (oldest first)
|
|
49
|
+
goal: Filter to events linked to this goal's full hierarchical name
|
|
50
|
+
(e.g. "Social > Trent"). Unknown names return an empty list.
|
|
51
|
+
tag: Filter to events tagged with this tag (case-insensitive exact
|
|
52
|
+
name). Tag hierarchy is expanded — a parent tag includes events
|
|
53
|
+
tagged with any descendant. Unknown names return an empty list.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict with 'events' list and 'total' count
|
|
57
|
+
"""
|
|
58
|
+
return await client.get(
|
|
59
|
+
"/api/v1/events/",
|
|
60
|
+
params={
|
|
61
|
+
"on_date": on_date,
|
|
62
|
+
"start_date": start_date,
|
|
63
|
+
"end_date": end_date,
|
|
64
|
+
"limit": limit,
|
|
65
|
+
"offset": offset,
|
|
66
|
+
"sort_order": sort_order,
|
|
67
|
+
"goal": goal,
|
|
68
|
+
"tag": tag,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@operation(
|
|
74
|
+
cli="events get",
|
|
75
|
+
summary="Get a single event by its UID.",
|
|
76
|
+
covers=[("GET", "/api/v1/events/{uid}")],
|
|
77
|
+
params={"uid": arg(help="Event UID")},
|
|
78
|
+
)
|
|
79
|
+
async def get_event(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
80
|
+
"""Get a single event by its UID.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
client: Authenticated TetraClient
|
|
84
|
+
uid: The event's unique identifier
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Event dict
|
|
88
|
+
"""
|
|
89
|
+
return await client.get(f"/api/v1/events/{uid}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@operation(
|
|
93
|
+
cli="events create",
|
|
94
|
+
summary="Create a new event recording time or money spent.",
|
|
95
|
+
covers=[("POST", "/api/v1/events/")],
|
|
96
|
+
params={
|
|
97
|
+
"day": arg(help="Date in YYYY-MM-DD format"),
|
|
98
|
+
"goals": opt("--goal", repeatable=True, help="Goal name (repeatable)"),
|
|
99
|
+
"values": opt("--value", repeatable=True, help="Value name (repeatable)"),
|
|
100
|
+
"duration_minutes": opt("--duration-minutes",
|
|
101
|
+
help="Duration in minutes (converted to seconds)"),
|
|
102
|
+
"amount_dollars": opt("--amount-dollars",
|
|
103
|
+
help="Amount in dollars (converted to cents)"),
|
|
104
|
+
"description": opt("--description", help="What happened during this event"),
|
|
105
|
+
"feels": opt("--feels", min=-2, max=2, help="Mood rating from -2 to +2"),
|
|
106
|
+
"outcome": opt("--outcome", json=True,
|
|
107
|
+
help="Goal metric outcome as a JSON value"),
|
|
108
|
+
"tag_uids": opt("--tag-uid", repeatable=True, help="Tag UID (repeatable)"),
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
async def create_event(
|
|
112
|
+
client: TetraClient,
|
|
113
|
+
*,
|
|
114
|
+
day: str,
|
|
115
|
+
goals: list[str] | None = None,
|
|
116
|
+
values: list[str] | None = None,
|
|
117
|
+
duration_minutes: float | None = None,
|
|
118
|
+
amount_dollars: float | None = None,
|
|
119
|
+
description: str | None = None,
|
|
120
|
+
feels: int | None = None,
|
|
121
|
+
start: str | None = None,
|
|
122
|
+
end: str | None = None,
|
|
123
|
+
outcome: Any | None = None,
|
|
124
|
+
tag_uids: list[str] | None = None,
|
|
125
|
+
group_uid: str | None = None,
|
|
126
|
+
for_account_uid: str | None = None,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""Create a new event recording time or money spent.
|
|
129
|
+
|
|
130
|
+
Converts duration_minutes -> seconds and amount_dollars -> cents before
|
|
131
|
+
sending to the API, which stores all time in seconds and money in cents.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
client: Authenticated TetraClient
|
|
135
|
+
day: Date in YYYY-MM-DD format (required)
|
|
136
|
+
goals: List of goal names (full hierarchical paths)
|
|
137
|
+
values: List of value names (full hierarchical paths)
|
|
138
|
+
duration_minutes: Duration in minutes; converted to seconds for API
|
|
139
|
+
amount_dollars: Money amount in dollars; converted to cents for API
|
|
140
|
+
description: What happened during this event
|
|
141
|
+
feels: Mood rating from -2 (terrible) to +2 (great)
|
|
142
|
+
start: Start time as ISO datetime
|
|
143
|
+
end: End time as ISO datetime
|
|
144
|
+
outcome: Goal metric outcome - number, list of numbers, or list of records
|
|
145
|
+
tag_uids: UIDs of tags to attach to the event
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Created event dict
|
|
149
|
+
"""
|
|
150
|
+
body: dict[str, Any] = {"day": day}
|
|
151
|
+
if goals:
|
|
152
|
+
body["goals"] = goals
|
|
153
|
+
if values:
|
|
154
|
+
body["values"] = values
|
|
155
|
+
if duration_minutes is not None:
|
|
156
|
+
body["duration"] = int(duration_minutes * 60)
|
|
157
|
+
if amount_dollars is not None:
|
|
158
|
+
body["amount"] = int(amount_dollars * 100)
|
|
159
|
+
if description:
|
|
160
|
+
body["description"] = description
|
|
161
|
+
if feels is not None:
|
|
162
|
+
body["feels"] = feels
|
|
163
|
+
if start:
|
|
164
|
+
body["start"] = start
|
|
165
|
+
if end:
|
|
166
|
+
body["end"] = end
|
|
167
|
+
if outcome is not None:
|
|
168
|
+
body["outcome"] = outcome
|
|
169
|
+
if tag_uids:
|
|
170
|
+
body["tag_uids"] = tag_uids
|
|
171
|
+
# Creation context — pair (group_uid, for_account_uid) picks the
|
|
172
|
+
# shape A/B/C per docs/architecture/creation-context.md. Backend
|
|
173
|
+
# resolver validates the combination.
|
|
174
|
+
if group_uid is not None:
|
|
175
|
+
body["group_uid"] = group_uid
|
|
176
|
+
if for_account_uid is not None:
|
|
177
|
+
body["for_account_uid"] = for_account_uid
|
|
178
|
+
return await client.post("/api/v1/events/", json=body)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@operation(
|
|
182
|
+
cli="events update",
|
|
183
|
+
summary="Update an existing event. Only provided fields are changed.",
|
|
184
|
+
covers=[("PUT", "/api/v1/events/{uid}")],
|
|
185
|
+
params={
|
|
186
|
+
"uid": arg(help="Event UID"),
|
|
187
|
+
"goals": opt("--goal", repeatable=True,
|
|
188
|
+
help="Goal name (repeatable; replaces all)"),
|
|
189
|
+
"values": opt("--value", repeatable=True,
|
|
190
|
+
help="Value name (repeatable; replaces all)"),
|
|
191
|
+
"duration_minutes": opt("--duration-minutes",
|
|
192
|
+
help="Duration in minutes (converted to seconds)"),
|
|
193
|
+
"amount_dollars": opt("--amount-dollars",
|
|
194
|
+
help="Amount in dollars (converted to cents)"),
|
|
195
|
+
"feels": opt("--feels", min=-2, max=2, help="Mood rating from -2 to +2"),
|
|
196
|
+
"outcome": opt("--outcome", json=True,
|
|
197
|
+
help="Goal metric outcome as a JSON value"),
|
|
198
|
+
"tag_uids": opt("--tag-uid", repeatable=True,
|
|
199
|
+
help="Tag UID (repeatable; replaces all)"),
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
async def update_event(
|
|
203
|
+
client: TetraClient,
|
|
204
|
+
*,
|
|
205
|
+
uid: str,
|
|
206
|
+
day: str | None = None,
|
|
207
|
+
goals: list[str] | None = None,
|
|
208
|
+
values: list[str] | None = None,
|
|
209
|
+
duration_minutes: float | None = None,
|
|
210
|
+
amount_dollars: float | None = None,
|
|
211
|
+
description: str | None = None,
|
|
212
|
+
feels: int | None = None,
|
|
213
|
+
start: str | None = None,
|
|
214
|
+
end: str | None = None,
|
|
215
|
+
outcome: Any | None = None,
|
|
216
|
+
tag_uids: list[str] | None = None,
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""Update an existing event. Only provided fields are changed.
|
|
219
|
+
|
|
220
|
+
Like create_event, converts duration_minutes -> seconds and
|
|
221
|
+
amount_dollars -> cents. Only fields explicitly passed (not None) are
|
|
222
|
+
included in the PATCH body so callers can update a single field cleanly.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
client: Authenticated TetraClient
|
|
226
|
+
uid: The event's unique identifier (required)
|
|
227
|
+
day: New date in YYYY-MM-DD format
|
|
228
|
+
goals: New list of goal names (replaces all goals)
|
|
229
|
+
values: New list of value names (replaces all values)
|
|
230
|
+
duration_minutes: New duration in minutes
|
|
231
|
+
amount_dollars: New money amount in dollars
|
|
232
|
+
description: New description
|
|
233
|
+
feels: New mood rating (-2 to +2)
|
|
234
|
+
start: New start time as ISO datetime
|
|
235
|
+
end: New end time as ISO datetime
|
|
236
|
+
outcome: New goal metric outcome
|
|
237
|
+
tag_uids: New tag UIDs (replaces all tags)
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Updated event dict
|
|
241
|
+
"""
|
|
242
|
+
body: dict[str, Any] = {}
|
|
243
|
+
if day is not None:
|
|
244
|
+
body["day"] = day
|
|
245
|
+
if goals is not None:
|
|
246
|
+
body["goals"] = goals
|
|
247
|
+
if values is not None:
|
|
248
|
+
body["values"] = values
|
|
249
|
+
if duration_minutes is not None:
|
|
250
|
+
body["duration"] = int(duration_minutes * 60)
|
|
251
|
+
if amount_dollars is not None:
|
|
252
|
+
body["amount"] = int(amount_dollars * 100)
|
|
253
|
+
if description is not None:
|
|
254
|
+
body["description"] = description
|
|
255
|
+
if feels is not None:
|
|
256
|
+
body["feels"] = feels
|
|
257
|
+
if start is not None:
|
|
258
|
+
body["start"] = start
|
|
259
|
+
if end is not None:
|
|
260
|
+
body["end"] = end
|
|
261
|
+
if outcome is not None:
|
|
262
|
+
body["outcome"] = outcome
|
|
263
|
+
if tag_uids is not None:
|
|
264
|
+
body["tag_uids"] = tag_uids
|
|
265
|
+
return await client.put(f"/api/v1/events/{uid}", json=body)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@operation(
|
|
269
|
+
cli="events delete",
|
|
270
|
+
summary="Delete an event permanently.",
|
|
271
|
+
covers=[("DELETE", "/api/v1/events/{uid}")],
|
|
272
|
+
params={"uid": arg(help="Event UID")},
|
|
273
|
+
)
|
|
274
|
+
async def delete_event(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
275
|
+
"""Delete an event permanently.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
client: Authenticated TetraClient
|
|
279
|
+
uid: The event's unique identifier
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Deletion confirmation dict
|
|
283
|
+
"""
|
|
284
|
+
return await client.delete(f"/api/v1/events/{uid}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@operation(
|
|
288
|
+
cli="events bulk-edit",
|
|
289
|
+
summary="Edit multiple events at once (UID-based).",
|
|
290
|
+
covers=[("POST", "/api/v1/events/bulk-edit")],
|
|
291
|
+
params={
|
|
292
|
+
"event_uids": opt("--uid", repeatable=True,
|
|
293
|
+
help="Event UID to edit (repeatable, required)"),
|
|
294
|
+
"set_goals": opt("--set-goal", repeatable=True,
|
|
295
|
+
help="Replace goals on all selected events"),
|
|
296
|
+
"set_values": opt("--set-value", repeatable=True,
|
|
297
|
+
help="Replace values on all selected events"),
|
|
298
|
+
"set_feels": opt("--set-feels", min=-2, max=2,
|
|
299
|
+
help="Set feels on all selected events"),
|
|
300
|
+
"add_tag_uids": opt("--add-tag-uid", repeatable=True,
|
|
301
|
+
help="Add these tag UIDs to all selected events"),
|
|
302
|
+
"remove_tag_uids": opt("--remove-tag-uid", repeatable=True,
|
|
303
|
+
help="Remove these tag UIDs from all selected events"),
|
|
304
|
+
"add_tags": opt("--add-tag", repeatable=True,
|
|
305
|
+
help="Add tag by NAME (resolved to a UID; repeatable)"),
|
|
306
|
+
"remove_tags": opt("--remove-tag", repeatable=True,
|
|
307
|
+
help="Remove tag by NAME (resolved to a UID; repeatable)"),
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
async def bulk_edit_events(
|
|
311
|
+
client: TetraClient,
|
|
312
|
+
*,
|
|
313
|
+
event_uids: list[str],
|
|
314
|
+
set_goals: list[str] | None = None,
|
|
315
|
+
set_values: list[str] | None = None,
|
|
316
|
+
set_feels: int | None = None,
|
|
317
|
+
add_tag_uids: list[str] | None = None,
|
|
318
|
+
remove_tag_uids: list[str] | None = None,
|
|
319
|
+
add_tags: list[str] | None = None,
|
|
320
|
+
remove_tags: list[str] | None = None,
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""Edit multiple events at once. Useful for moving events between goals.
|
|
323
|
+
|
|
324
|
+
Builds a patch dict from only the provided fields, so partial updates work
|
|
325
|
+
without overwriting fields the caller didn't touch.
|
|
326
|
+
|
|
327
|
+
Tags may be supplied by UID (``add_tag_uids`` / ``remove_tag_uids``) or by
|
|
328
|
+
NAME (``add_tags`` / ``remove_tags``). Names are resolved to UIDs inside
|
|
329
|
+
the op via the shared ``resolve_tag_uid`` helper, so both the CLI and the
|
|
330
|
+
MCP surface inherit name→uid resolution. The resolved names are appended to
|
|
331
|
+
any UIDs already passed.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
client: Authenticated TetraClient
|
|
335
|
+
event_uids: List of event UIDs to edit (required)
|
|
336
|
+
set_goals: Replace goals on all selected events
|
|
337
|
+
set_values: Replace values on all selected events
|
|
338
|
+
set_feels: Set feels on all selected events
|
|
339
|
+
add_tag_uids: Add these tag UIDs to all selected events
|
|
340
|
+
remove_tag_uids: Remove these tag UIDs from all selected events
|
|
341
|
+
add_tags: Tag names to add (each resolved to a UID via /tags/search)
|
|
342
|
+
remove_tags: Tag names to remove (each resolved to a UID)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict with 'updated_count'
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
TetraClientError: If a tag name in add_tags/remove_tags has no exact
|
|
349
|
+
(case-insensitive) match.
|
|
350
|
+
"""
|
|
351
|
+
resolved_add: list[str] = list(add_tag_uids or [])
|
|
352
|
+
for tag_name in add_tags or []:
|
|
353
|
+
resolved_add.append(await resolve_tag_uid(client, tag_name))
|
|
354
|
+
resolved_remove: list[str] = list(remove_tag_uids or [])
|
|
355
|
+
for tag_name in remove_tags or []:
|
|
356
|
+
resolved_remove.append(await resolve_tag_uid(client, tag_name))
|
|
357
|
+
|
|
358
|
+
patch: dict[str, Any] = {}
|
|
359
|
+
if set_goals is not None:
|
|
360
|
+
patch["goals"] = set_goals
|
|
361
|
+
if set_values is not None:
|
|
362
|
+
patch["values"] = set_values
|
|
363
|
+
if set_feels is not None:
|
|
364
|
+
patch["feels"] = set_feels
|
|
365
|
+
if resolved_add:
|
|
366
|
+
patch["add_tag_uids"] = resolved_add
|
|
367
|
+
if resolved_remove:
|
|
368
|
+
patch["remove_tag_uids"] = resolved_remove
|
|
369
|
+
return await client.post(
|
|
370
|
+
"/api/v1/events/bulk-edit", json={"uids": event_uids, "patch": patch}
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@operation(
|
|
375
|
+
cli="events calendar",
|
|
376
|
+
summary="Get events organized by day for a calendar month view.",
|
|
377
|
+
covers=[("GET", "/api/v1/events/calendar/{year}/{month}")],
|
|
378
|
+
params={
|
|
379
|
+
"year": arg(help="Calendar year (e.g. 2026)"),
|
|
380
|
+
"month": arg(help="Calendar month (1-12)"),
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
async def get_calendar(client: TetraClient, *, year: int, month: int) -> dict[str, Any]:
|
|
384
|
+
"""Get events organized by day for a calendar month view.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
client: Authenticated TetraClient
|
|
388
|
+
year: Calendar year (e.g., 2026)
|
|
389
|
+
month: Calendar month (1-12)
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Dict with 'days' mapping date strings to lists of events
|
|
393
|
+
"""
|
|
394
|
+
return await client.get(f"/api/v1/events/calendar/{year}/{month}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@operation(
|
|
398
|
+
cli="events get-batch",
|
|
399
|
+
summary="Fetch multiple events by UID in a single request.",
|
|
400
|
+
covers=[("POST", "/api/v1/events/batch")],
|
|
401
|
+
params={
|
|
402
|
+
"uids": opt("--uid", repeatable=True,
|
|
403
|
+
help="Event UID to fetch (repeatable, required)"),
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
async def get_events_batch(
|
|
407
|
+
client: TetraClient, *, uids: list[str],
|
|
408
|
+
) -> dict[str, Any]:
|
|
409
|
+
"""Fetch multiple events by UID in a single request.
|
|
410
|
+
|
|
411
|
+
Performance helper for bulk reads — fetches up to 100 events in one
|
|
412
|
+
call instead of N round-trips.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
client: Authenticated TetraClient
|
|
416
|
+
uids: Event UIDs to fetch (max 100)
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Dict with 'events' (found) and 'not_found' (missing UIDs) lists
|
|
420
|
+
"""
|
|
421
|
+
return await client.post("/api/v1/events/batch", json={"uids": uids})
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@operation(
|
|
425
|
+
cli="events bulk-delete",
|
|
426
|
+
summary="Delete multiple events in one batch operation.",
|
|
427
|
+
covers=[("POST", "/api/v1/events/bulk-delete")],
|
|
428
|
+
params={
|
|
429
|
+
"uids": opt("--uid", repeatable=True,
|
|
430
|
+
help="Event UID to delete (repeatable, required)"),
|
|
431
|
+
"reason": opt("--reason", help="Reason recorded on the soft delete"),
|
|
432
|
+
},
|
|
433
|
+
)
|
|
434
|
+
async def bulk_delete_events(
|
|
435
|
+
client: TetraClient,
|
|
436
|
+
*,
|
|
437
|
+
uids: list[str],
|
|
438
|
+
reason: str | None = None,
|
|
439
|
+
) -> dict[str, Any]:
|
|
440
|
+
"""Delete multiple events in a single batch operation.
|
|
441
|
+
|
|
442
|
+
Auto-generated events are not deletable and come back in ``failed_uids``;
|
|
443
|
+
the rest of the batch still succeeds.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
client: Authenticated TetraClient
|
|
447
|
+
uids: Event UIDs to delete (required)
|
|
448
|
+
reason: Optional reason recorded on the soft delete
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Dict with 'deleted_count', 'deleted_uids', and 'failed_uids'
|
|
452
|
+
"""
|
|
453
|
+
body: dict[str, Any] = {"uids": uids}
|
|
454
|
+
if reason is not None:
|
|
455
|
+
body["reason"] = reason
|
|
456
|
+
return await client.post("/api/v1/events/bulk-delete", json=body)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@operation(
|
|
460
|
+
cli="events duplicates",
|
|
461
|
+
summary="Find groups of duplicate events sharing identical field values.",
|
|
462
|
+
covers=[("GET", "/api/v1/events/duplicates")],
|
|
463
|
+
)
|
|
464
|
+
async def find_duplicates(client: TetraClient) -> dict[str, Any]:
|
|
465
|
+
"""Find groups of duplicate events sharing identical field values.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
client: Authenticated TetraClient
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dict with 'duplicate_count', 'duplicate_groups', and a 'duplicates'
|
|
472
|
+
list of {fingerprint, uids, count}
|
|
473
|
+
"""
|
|
474
|
+
return await client.get("/api/v1/events/duplicates")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@operation(
|
|
478
|
+
cli="events delete-duplicates",
|
|
479
|
+
summary="Delete duplicate events, keeping one of each group.",
|
|
480
|
+
covers=[("POST", "/api/v1/events/duplicates/delete")],
|
|
481
|
+
)
|
|
482
|
+
async def delete_duplicates(client: TetraClient) -> dict[str, Any]:
|
|
483
|
+
"""Delete duplicate events, keeping one of each duplicate group.
|
|
484
|
+
|
|
485
|
+
For each group of identical events the lowest UID is kept and the rest
|
|
486
|
+
are deleted.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
client: Authenticated TetraClient
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dict with 'deleted_count', 'kept_count', and 'deleted_uids'
|
|
493
|
+
"""
|
|
494
|
+
return await client.post("/api/v1/events/duplicates/delete")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@operation(
|
|
498
|
+
cli="events generate",
|
|
499
|
+
summary="Generate (or preview) PLANNED events for unlogged time slots.",
|
|
500
|
+
covers=[("POST", "/api/v1/events/generate")],
|
|
501
|
+
params={
|
|
502
|
+
"start_date": arg(help="Range start date (YYYY-MM-DD)"),
|
|
503
|
+
"end_date": arg(help="Range end date (YYYY-MM-DD)"),
|
|
504
|
+
"persist": opt("--persist",
|
|
505
|
+
help="Save generated events (omit for preview only)"),
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
async def generate_events(
|
|
509
|
+
client: TetraClient,
|
|
510
|
+
*,
|
|
511
|
+
start_date: str,
|
|
512
|
+
end_date: str,
|
|
513
|
+
persist: bool = False,
|
|
514
|
+
) -> dict[str, Any]:
|
|
515
|
+
"""Generate system PLANNED events for unlogged time slots in a date range.
|
|
516
|
+
|
|
517
|
+
PLANNED events are derived from Value time_rules for slots not already
|
|
518
|
+
covered by RECORD or user PLANNED events. With ``persist=False`` (default)
|
|
519
|
+
the result is a preview; ``persist=True`` saves them.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
client: Authenticated TetraClient
|
|
523
|
+
start_date: Range start date (YYYY-MM-DD)
|
|
524
|
+
end_date: Range end date (YYYY-MM-DD)
|
|
525
|
+
persist: True to save events, False to preview only
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Dict with 'generated_count' and 'events'
|
|
529
|
+
"""
|
|
530
|
+
return await client.post(
|
|
531
|
+
"/api/v1/events/generate",
|
|
532
|
+
json={
|
|
533
|
+
"start_date": start_date,
|
|
534
|
+
"end_date": end_date,
|
|
535
|
+
"persist": persist,
|
|
536
|
+
},
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@operation(
|
|
541
|
+
cli="events auto-accept",
|
|
542
|
+
summary="Auto-accept PLANNED events for a past date.",
|
|
543
|
+
covers=[("POST", "/api/v1/events/auto-accept")],
|
|
544
|
+
params={"date": arg(help="Date to auto-accept (YYYY-MM-DD)")},
|
|
545
|
+
)
|
|
546
|
+
async def auto_accept_events(
|
|
547
|
+
client: TetraClient, *, date: str,
|
|
548
|
+
) -> dict[str, Any]:
|
|
549
|
+
"""Auto-accept PLANNED events for a past date.
|
|
550
|
+
|
|
551
|
+
PLANNED events with no RECORD overlap become EXECUTED; those that overlap
|
|
552
|
+
a RECORD become NOT_EXECUTED.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
client: Authenticated TetraClient
|
|
556
|
+
date: Target date (YYYY-MM-DD)
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Dict with 'executed_count', 'not_executed_count', 'executed_uids',
|
|
560
|
+
and 'not_executed_uids'
|
|
561
|
+
"""
|
|
562
|
+
return await client.post(
|
|
563
|
+
"/api/v1/events/auto-accept", json={"date": date},
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@operation(
|
|
568
|
+
cli="events accept",
|
|
569
|
+
summary="Accept a PLANNED event, marking it EXECUTED.",
|
|
570
|
+
covers=[("POST", "/api/v1/events/{uid}/accept")],
|
|
571
|
+
params={"uid": arg(help="PLANNED event UID")},
|
|
572
|
+
)
|
|
573
|
+
async def accept_planned_event(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
574
|
+
"""Accept a PLANNED event, marking it as EXECUTED.
|
|
575
|
+
|
|
576
|
+
Confirms the planned event happened as scheduled.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
client: Authenticated TetraClient
|
|
580
|
+
uid: The PLANNED event's UID
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
The updated event dict
|
|
584
|
+
"""
|
|
585
|
+
return await client.post(f"/api/v1/events/{uid}/accept")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@operation(
|
|
589
|
+
cli="events reject",
|
|
590
|
+
summary="Reject a PLANNED event, marking it NOT_EXECUTED.",
|
|
591
|
+
covers=[("POST", "/api/v1/events/{uid}/reject")],
|
|
592
|
+
params={"uid": arg(help="PLANNED event UID")},
|
|
593
|
+
)
|
|
594
|
+
async def reject_planned_event(client: TetraClient, uid: str) -> dict[str, Any]:
|
|
595
|
+
"""Reject a PLANNED event, marking it as NOT_EXECUTED.
|
|
596
|
+
|
|
597
|
+
Records that the planned event did not happen; the event is kept for
|
|
598
|
+
planning-accuracy metrics.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
client: Authenticated TetraClient
|
|
602
|
+
uid: The PLANNED event's UID
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
The updated event dict
|
|
606
|
+
"""
|
|
607
|
+
return await client.post(f"/api/v1/events/{uid}/reject")
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@operation(
|
|
611
|
+
cli="events promote-to-goal",
|
|
612
|
+
summary="Promote a free-form event into a new record-shaped goal.",
|
|
613
|
+
covers=[("POST", "/api/v1/events/{uid}/promote-to-goal")],
|
|
614
|
+
params={
|
|
615
|
+
"uid": arg(help="Event UID to promote"),
|
|
616
|
+
"goal_name": opt("--goal-name", help="Name for the new goal (required)"),
|
|
617
|
+
},
|
|
618
|
+
)
|
|
619
|
+
async def promote_event_to_goal(
|
|
620
|
+
client: TetraClient, *, uid: str, goal_name: str,
|
|
621
|
+
) -> dict[str, Any]:
|
|
622
|
+
"""Promote a free-form event into a new record-shaped goal.
|
|
623
|
+
|
|
624
|
+
Creates a ``record``-shaped goal whose outcome field names mirror the
|
|
625
|
+
event's outcome columns, links the event to it, and canonicalizes the
|
|
626
|
+
event's outcome keys to the goal's fields.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
client: Authenticated TetraClient
|
|
630
|
+
uid: UID of the event to promote (must have free-form columns)
|
|
631
|
+
goal_name: Name for the new goal
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Dict with 'goal' and 'event'
|
|
635
|
+
"""
|
|
636
|
+
return await client.post(
|
|
637
|
+
f"/api/v1/events/{uid}/promote-to-goal",
|
|
638
|
+
json={"goal_name": goal_name},
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@operation(
|
|
643
|
+
cli="events add-with",
|
|
644
|
+
summary="Tag a co-attendee on an event.",
|
|
645
|
+
covers=[("POST", "/api/v1/events/{event_uid}/with")],
|
|
646
|
+
params={
|
|
647
|
+
"event_uid": arg(help="Event UID to tag the co-attendee on"),
|
|
648
|
+
"with_account_uid": opt("--with-account-uid",
|
|
649
|
+
help="Account UID of the co-attendee (required)"),
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
async def add_with_tag(
|
|
653
|
+
client: TetraClient, *, event_uid: str, with_account_uid: str,
|
|
654
|
+
) -> dict[str, Any]:
|
|
655
|
+
"""Tag a co-attendee on an event ("with" feature).
|
|
656
|
+
|
|
657
|
+
If the pair is already connected the tag is active immediately; otherwise
|
|
658
|
+
a peer-connection request is created and the tag is pending_connect.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
client: Authenticated TetraClient
|
|
662
|
+
event_uid: UID of the event to tag
|
|
663
|
+
with_account_uid: Account UID of the co-attendee
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
The created with-tag dict
|
|
667
|
+
"""
|
|
668
|
+
return await client.post(
|
|
669
|
+
f"/api/v1/events/{event_uid}/with",
|
|
670
|
+
json={"with_account_uid": with_account_uid},
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@operation(
|
|
675
|
+
cli="events remove-with",
|
|
676
|
+
summary="Remove a co-attendee with-tag from an event.",
|
|
677
|
+
covers=[("DELETE", "/api/v1/events/with/{tag_uid}")],
|
|
678
|
+
params={"tag_uid": arg(help="With-tag UID to remove")},
|
|
679
|
+
)
|
|
680
|
+
async def remove_with_tag(client: TetraClient, tag_uid: str) -> dict[str, Any]:
|
|
681
|
+
"""Remove a co-attendee with-tag.
|
|
682
|
+
|
|
683
|
+
Either the event owner or the tagged account may remove it. Removing does
|
|
684
|
+
not delete an already-created mirror event.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
client: Authenticated TetraClient
|
|
688
|
+
tag_uid: The with-tag's UID
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Empty dict (the endpoint returns 204 No Content)
|
|
692
|
+
"""
|
|
693
|
+
return await client.delete(f"/api/v1/events/with/{tag_uid}")
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@operation(
|
|
697
|
+
cli="events with-inbox",
|
|
698
|
+
summary="List pending with-tags awaiting a mirror event.",
|
|
699
|
+
covers=[("GET", "/api/v1/events/with/inbox")],
|
|
700
|
+
)
|
|
701
|
+
async def list_with_inbox(client: TetraClient) -> dict[str, Any]:
|
|
702
|
+
"""List pending with-tags where the caller is the tagged co-attendee.
|
|
703
|
+
|
|
704
|
+
These are tags that have no mirror event yet — candidates for
|
|
705
|
+
``create_with_mirror``.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
client: Authenticated TetraClient
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Dict with a 'tags' list
|
|
712
|
+
"""
|
|
713
|
+
return await client.get("/api/v1/events/with/inbox")
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@operation(
|
|
717
|
+
cli="events mirror-with",
|
|
718
|
+
summary="Create the mirror event for an accepted with-tag.",
|
|
719
|
+
covers=[("POST", "/api/v1/events/with/{tag_uid}/mirror")],
|
|
720
|
+
params={"tag_uid": arg(help="With-tag UID to mirror")},
|
|
721
|
+
)
|
|
722
|
+
async def create_with_mirror(client: TetraClient, tag_uid: str) -> dict[str, Any]:
|
|
723
|
+
"""Create the mirror event for an accepted with-tag.
|
|
724
|
+
|
|
725
|
+
Idempotent — re-calling returns the existing mirror.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
client: Authenticated TetraClient
|
|
729
|
+
tag_uid: The with-tag's UID
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Dict with 'mirror_event_uid', 'group_uid', and 'goal_uids'
|
|
733
|
+
"""
|
|
734
|
+
return await client.post(f"/api/v1/events/with/{tag_uid}/mirror")
|