tetra-cli 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/PKG-INFO +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/pyproject.toml +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/__init__.py +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/capacity.py +17 -10
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/goals.py +176 -8
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/groups.py +19 -0
- tetra_cli-0.4.0/tetra_cli/api_client/operations/skill_tree_rankings.py +152 -0
- tetra_cli-0.4.0/tetra_cli/api_client/operations/skill_tree_requests.py +161 -0
- tetra_cli-0.4.0/tetra_cli/api_client/operations/skill_trees.py +401 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/values.py +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/values_2.py +23 -6
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/PKG-INFO +1 -1
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/SOURCES.txt +2 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/skill_trees.py +0 -160
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/README.md +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/setup.cfg +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tests/test_no_backend_leak.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tests/test_smoke.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/client.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/config.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/accounts.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/ai.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/analysis.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/api_keys.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/archive.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/awards.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/conversations.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/conversations_2.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/cosmetics.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/dashboard.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/data.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/events.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/gamification.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/issues.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/offset.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/onboarding.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/outcome_schemas.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/peer_connections.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/plaid.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/reminders.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/scratches.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/social_2.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/social_3.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/social_4.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/strava.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/stripe.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/tags.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/watch.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/webhooks.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/api_client/operations/xp.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/__main__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/app.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/commands/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/commands/auth.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/commands/guide.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/commands/messages.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/commands/skill.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/context.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/generate.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/cli/output.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/mcp_gen.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/ontology.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/registry.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/skill/SKILL.md +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli/skill/__init__.py +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/dependency_links.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/entry_points.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/requires.txt +0 -0
- {tetra_cli-0.2.0 → tetra_cli-0.4.0}/tetra_cli.egg-info/top_level.txt +0 -0
|
@@ -61,24 +61,31 @@ async def get_group_capacity(
|
|
|
61
61
|
|
|
62
62
|
@operation(
|
|
63
63
|
cli="capacity purchase-slot",
|
|
64
|
-
summary="Buy the next credit-purchased active capacity
|
|
64
|
+
summary="Buy the next credit-purchased active capacity slots.",
|
|
65
65
|
covers=[("POST", "/api/v1/capacity/purchase-slot")],
|
|
66
|
+
params={"quantity": arg(help="How many slots to buy (default 1)")},
|
|
66
67
|
)
|
|
67
|
-
async def purchase_capacity_slot(
|
|
68
|
-
|
|
68
|
+
async def purchase_capacity_slot(
|
|
69
|
+
client: TetraClient, quantity: int = 1,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Buy the next ``quantity`` credit-purchased active slots.
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
Per-slot cost escalates and the purchase is all-or-nothing: the summed
|
|
74
|
+
cost is debited in one transaction or the call fails without buying any.
|
|
72
75
|
|
|
73
76
|
Args:
|
|
74
77
|
client: Authenticated TetraClient
|
|
78
|
+
quantity: How many slots to buy (default 1)
|
|
75
79
|
|
|
76
80
|
Returns:
|
|
77
|
-
Updated capacity dict reflecting the new ``purchased_slots
|
|
78
|
-
``next_slot_cost
|
|
81
|
+
Updated capacity dict reflecting the new ``purchased_slots``,
|
|
82
|
+
``next_slot_cost``, and ``slot_cost_paid`` (total credits debited).
|
|
79
83
|
|
|
80
84
|
Raises:
|
|
81
|
-
TetraClientError: 400 ``insufficient_credits`` if the balance
|
|
82
|
-
|
|
85
|
+
TetraClientError: 400 ``insufficient_credits`` if the balance can't
|
|
86
|
+
cover the summed cost; 400 ``free_cap_reached`` when a free
|
|
87
|
+
account would end past the slot hard cap.
|
|
83
88
|
"""
|
|
84
|
-
return await client.post(
|
|
89
|
+
return await client.post(
|
|
90
|
+
"/api/v1/capacity/purchase-slot", json={"quantity": quantity}
|
|
91
|
+
)
|
|
@@ -188,6 +188,10 @@ async def create_goal(
|
|
|
188
188
|
help=("Goal status as a label (created | in_progress | archived | "
|
|
189
189
|
"abandoned | completed) or its numeric form (1-5)."),
|
|
190
190
|
),
|
|
191
|
+
"group_uid": opt(
|
|
192
|
+
"--group-uid",
|
|
193
|
+
help="Group UID for a group-scoped update (e.g. editing a goal you "
|
|
194
|
+
"created on a mentee's behalf). Omit for your personal scope."),
|
|
191
195
|
},
|
|
192
196
|
)
|
|
193
197
|
async def update_goal(
|
|
@@ -206,6 +210,7 @@ async def update_goal(
|
|
|
206
210
|
outcome_shape: str | None = None,
|
|
207
211
|
outcome_names: list[str] | None = None,
|
|
208
212
|
status: str | None = None,
|
|
213
|
+
group_uid: str | None = None,
|
|
209
214
|
) -> dict[str, Any]:
|
|
210
215
|
"""Update an existing goal. Only provided (non-None) fields are changed.
|
|
211
216
|
|
|
@@ -227,6 +232,9 @@ async def update_goal(
|
|
|
227
232
|
2=IN_PROGRESS, 3=ARCHIVED, 4=ABANDONED, 5=COMPLETED) or a label
|
|
228
233
|
string ("created" … "completed", case-insensitive). The op
|
|
229
234
|
normalizes labels→int so the CLI/MCP surfaces share the behavior.
|
|
235
|
+
group_uid: Group UID for a group-scoped update. Required to edit a goal
|
|
236
|
+
that lives in a group (e.g. one created on a mentee's behalf);
|
|
237
|
+
omit for personal-scope goals.
|
|
230
238
|
|
|
231
239
|
Returns:
|
|
232
240
|
Updated goal dict
|
|
@@ -260,26 +268,43 @@ async def update_goal(
|
|
|
260
268
|
body["outcome_names"] = outcome_names
|
|
261
269
|
if status is not None:
|
|
262
270
|
body["status"] = _normalize_status(status)
|
|
263
|
-
|
|
271
|
+
path = f"/api/v1/goals/{uid}"
|
|
272
|
+
if group_uid is not None:
|
|
273
|
+
path += f"?group_uid={group_uid}"
|
|
274
|
+
return await client.put(path, json=body)
|
|
264
275
|
|
|
265
276
|
|
|
266
277
|
@operation(
|
|
267
278
|
cli="goals delete",
|
|
268
279
|
summary="Delete a goal permanently.",
|
|
269
280
|
covers=[("DELETE", "/api/v1/goals/{uid}")],
|
|
270
|
-
params={
|
|
281
|
+
params={
|
|
282
|
+
"uid": arg(help="Goal UID to delete"),
|
|
283
|
+
"group_uid": opt(
|
|
284
|
+
"--group-uid",
|
|
285
|
+
help="Group UID for a group-scoped delete (e.g. a goal you created "
|
|
286
|
+
"on a mentee's behalf). Omit for your personal scope."),
|
|
287
|
+
},
|
|
271
288
|
)
|
|
272
|
-
async def delete_goal(
|
|
289
|
+
async def delete_goal(
|
|
290
|
+
client: TetraClient, uid: str, *, group_uid: str | None = None,
|
|
291
|
+
) -> dict[str, Any]:
|
|
273
292
|
"""Delete a goal permanently.
|
|
274
293
|
|
|
275
294
|
Args:
|
|
276
295
|
client: Authenticated TetraClient
|
|
277
296
|
uid: Goal UID to delete
|
|
297
|
+
group_uid: Group UID for a group-scoped delete. Required to delete a
|
|
298
|
+
goal that lives in a group (e.g. one created on a mentee's behalf);
|
|
299
|
+
omit for personal-scope goals.
|
|
278
300
|
|
|
279
301
|
Returns:
|
|
280
302
|
Deletion confirmation dict
|
|
281
303
|
"""
|
|
282
|
-
|
|
304
|
+
path = f"/api/v1/goals/{uid}"
|
|
305
|
+
if group_uid is not None:
|
|
306
|
+
path += f"?group_uid={group_uid}"
|
|
307
|
+
return await client.delete(path)
|
|
283
308
|
|
|
284
309
|
|
|
285
310
|
@operation(
|
|
@@ -476,6 +501,10 @@ async def delete_goal_metric(
|
|
|
476
501
|
help="New parent goal name (omit to keep current)."),
|
|
477
502
|
"new_priority": opt("--priority", min=1,
|
|
478
503
|
help="New priority among siblings (1 = top)."),
|
|
504
|
+
"group_uid": opt(
|
|
505
|
+
"--group-uid",
|
|
506
|
+
help="Group UID for a group-scoped move (e.g. a mentee's goal). "
|
|
507
|
+
"Omit for your personal scope."),
|
|
479
508
|
},
|
|
480
509
|
)
|
|
481
510
|
async def move_goal(
|
|
@@ -484,6 +513,7 @@ async def move_goal(
|
|
|
484
513
|
uid: str,
|
|
485
514
|
new_parent: str | None = None,
|
|
486
515
|
new_priority: int | None = None,
|
|
516
|
+
group_uid: str | None = None,
|
|
487
517
|
) -> dict[str, Any]:
|
|
488
518
|
"""Move a goal to a new parent and/or position in the sibling order.
|
|
489
519
|
|
|
@@ -508,8 +538,12 @@ async def move_goal(
|
|
|
508
538
|
body: dict[str, Any] = {"uid": uid}
|
|
509
539
|
if new_parent is None:
|
|
510
540
|
# Caller didn't specify — pre-fetch and use current parent so the
|
|
511
|
-
# API doesn't interpret missing as "move to root."
|
|
512
|
-
|
|
541
|
+
# API doesn't interpret missing as "move to root." Scope the read to
|
|
542
|
+
# the same group so a mentee's goal resolves.
|
|
543
|
+
read_path = f"/api/v1/goals/{uid}"
|
|
544
|
+
if group_uid is not None:
|
|
545
|
+
read_path += f"?group_uid={group_uid}"
|
|
546
|
+
current = await client.get(read_path)
|
|
513
547
|
if current.get("parent"):
|
|
514
548
|
body["new_parent"] = current["parent"]
|
|
515
549
|
# else: goal is already a root, omit new_parent (API treats omitted
|
|
@@ -521,6 +555,8 @@ async def move_goal(
|
|
|
521
555
|
body["new_parent"] = new_parent
|
|
522
556
|
if new_priority is not None:
|
|
523
557
|
body["new_priority"] = new_priority
|
|
558
|
+
if group_uid is not None:
|
|
559
|
+
body["group_uid"] = group_uid
|
|
524
560
|
return await client.post("/api/v1/goals/move", json=body)
|
|
525
561
|
|
|
526
562
|
|
|
@@ -574,6 +610,10 @@ async def batch_delete_goals(
|
|
|
574
610
|
"updates": opt("--updates", json=True,
|
|
575
611
|
help="JSON map of {goal_uid: new_priority} (1-based, sequential)."),
|
|
576
612
|
"parent": opt("--parent", help="Parent goal name (omit for root siblings)."),
|
|
613
|
+
"group_uid": opt(
|
|
614
|
+
"--group-uid",
|
|
615
|
+
help="Group UID for a group-scoped reorder (e.g. a mentee's goals). "
|
|
616
|
+
"Omit for your personal scope."),
|
|
577
617
|
},
|
|
578
618
|
)
|
|
579
619
|
async def batch_update_goal_priorities(
|
|
@@ -581,6 +621,7 @@ async def batch_update_goal_priorities(
|
|
|
581
621
|
*,
|
|
582
622
|
updates: dict[str, int],
|
|
583
623
|
parent: str | None = None,
|
|
624
|
+
group_uid: str | None = None,
|
|
584
625
|
) -> dict[str, Any]:
|
|
585
626
|
"""Batch-update priorities for goals within one sibling group.
|
|
586
627
|
|
|
@@ -591,6 +632,8 @@ async def batch_update_goal_priorities(
|
|
|
591
632
|
client: Authenticated TetraClient
|
|
592
633
|
updates: Map of ``{goal_uid: new_priority}`` (1 = top).
|
|
593
634
|
parent: Parent goal name (null/omit for root-level siblings).
|
|
635
|
+
group_uid: Group UID for a group-scoped reorder (e.g. a mentee's
|
|
636
|
+
goals); omit for personal scope.
|
|
594
637
|
|
|
595
638
|
Returns:
|
|
596
639
|
Dict with ``updated`` count and the affected ``uids``.
|
|
@@ -598,7 +641,10 @@ async def batch_update_goal_priorities(
|
|
|
598
641
|
body: dict[str, Any] = {"updates": updates}
|
|
599
642
|
if parent is not None:
|
|
600
643
|
body["parent"] = parent
|
|
601
|
-
|
|
644
|
+
path = "/api/v1/goals/batch-update-priorities"
|
|
645
|
+
if group_uid is not None:
|
|
646
|
+
path += f"?group_uid={group_uid}"
|
|
647
|
+
return await client.post(path, json=body)
|
|
602
648
|
|
|
603
649
|
|
|
604
650
|
@operation(
|
|
@@ -610,6 +656,14 @@ async def batch_update_goal_priorities(
|
|
|
610
656
|
help="Source goal UID to merge (repeatable; 2+ required)."),
|
|
611
657
|
"new_goal": opt("--new-goal", json=True,
|
|
612
658
|
help="New goal data as a JSON dict (at least {\"name\": ...})."),
|
|
659
|
+
"group_uid": opt(
|
|
660
|
+
"--group-uid",
|
|
661
|
+
help="Group UID for a group-scoped merge (e.g. a mentee's goals). "
|
|
662
|
+
"Omit for your personal scope."),
|
|
663
|
+
"for_account_uid": opt(
|
|
664
|
+
"--for-account-uid",
|
|
665
|
+
help="Merge on this member's behalf — the merged goal belongs to "
|
|
666
|
+
"them (mentee account UID); requires --group-uid."),
|
|
613
667
|
},
|
|
614
668
|
)
|
|
615
669
|
async def merge_goals(
|
|
@@ -617,6 +671,8 @@ async def merge_goals(
|
|
|
617
671
|
*,
|
|
618
672
|
source_uids: list[str],
|
|
619
673
|
new_goal: dict[str, Any],
|
|
674
|
+
group_uid: str | None = None,
|
|
675
|
+
for_account_uid: str | None = None,
|
|
620
676
|
) -> dict[str, Any]:
|
|
621
677
|
"""Merge two or more goals into a new goal.
|
|
622
678
|
|
|
@@ -628,11 +684,19 @@ async def merge_goals(
|
|
|
628
684
|
source_uids: UIDs of the goals to merge (at least 2).
|
|
629
685
|
new_goal: New goal payload (must include ``name``; may include
|
|
630
686
|
``parent``, ``values``, etc.).
|
|
687
|
+
group_uid: Group UID for a group-scoped merge (e.g. a mentee's
|
|
688
|
+
goals); omit for personal scope.
|
|
689
|
+
for_account_uid: Merge on this member's behalf so the merged goal
|
|
690
|
+
belongs to them (mentee account UID); requires ``group_uid``.
|
|
631
691
|
|
|
632
692
|
Returns:
|
|
633
693
|
The created goal dict.
|
|
634
694
|
"""
|
|
635
695
|
body: dict[str, Any] = {"source_uids": source_uids, "new_goal": new_goal}
|
|
696
|
+
if group_uid is not None:
|
|
697
|
+
body["group_uid"] = group_uid
|
|
698
|
+
if for_account_uid is not None:
|
|
699
|
+
body["for_account_uid"] = for_account_uid
|
|
636
700
|
return await client.post("/api/v1/goals/merge", json=body)
|
|
637
701
|
|
|
638
702
|
|
|
@@ -1085,6 +1149,10 @@ async def get_goal_status_stages(
|
|
|
1085
1149
|
"uid": arg(help="Goal UID"),
|
|
1086
1150
|
"old_name": opt("--old-name", help="Current field name to rename."),
|
|
1087
1151
|
"new_name": opt("--new-name", help="New field name."),
|
|
1152
|
+
"group_uid": opt(
|
|
1153
|
+
"--group-uid",
|
|
1154
|
+
help="Group UID for a group-scoped rename (e.g. a mentee's goal). "
|
|
1155
|
+
"Omit for your personal scope."),
|
|
1088
1156
|
},
|
|
1089
1157
|
)
|
|
1090
1158
|
async def rename_goal_field(
|
|
@@ -1093,6 +1161,7 @@ async def rename_goal_field(
|
|
|
1093
1161
|
*,
|
|
1094
1162
|
old_name: str,
|
|
1095
1163
|
new_name: str,
|
|
1164
|
+
group_uid: str | None = None,
|
|
1096
1165
|
) -> dict[str, Any]:
|
|
1097
1166
|
"""Rename a tuple/record field on a goal and across all its events.
|
|
1098
1167
|
|
|
@@ -1104,13 +1173,18 @@ async def rename_goal_field(
|
|
|
1104
1173
|
uid: Goal UID.
|
|
1105
1174
|
old_name: Existing field name.
|
|
1106
1175
|
new_name: Replacement field name.
|
|
1176
|
+
group_uid: Group UID for a group-scoped rename (e.g. a mentee's
|
|
1177
|
+
goal); omit for personal scope.
|
|
1107
1178
|
|
|
1108
1179
|
Returns:
|
|
1109
1180
|
Dict with ``success``, ``events_updated``, and the new
|
|
1110
1181
|
``outcome_names``.
|
|
1111
1182
|
"""
|
|
1112
1183
|
body: dict[str, Any] = {"old_name": old_name, "new_name": new_name}
|
|
1113
|
-
|
|
1184
|
+
path = f"/api/v1/goals/{uid}/rename-field"
|
|
1185
|
+
if group_uid is not None:
|
|
1186
|
+
path += f"?group_uid={group_uid}"
|
|
1187
|
+
return await client.post(path, json=body)
|
|
1114
1188
|
|
|
1115
1189
|
|
|
1116
1190
|
@operation(
|
|
@@ -1142,3 +1216,97 @@ async def reorder_all_goals(
|
|
|
1142
1216
|
return await client.post(
|
|
1143
1217
|
"/api/v1/goals/reorder-all", json={"order": order}
|
|
1144
1218
|
)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
@operation(
|
|
1222
|
+
cli="goals link-value",
|
|
1223
|
+
summary="Mirror-link a goal to a value: renaming one renames the other.",
|
|
1224
|
+
covers=[("POST", "/api/v1/goals/{uid}/link-value")],
|
|
1225
|
+
params={
|
|
1226
|
+
"uid": arg(help="Goal UID to link"),
|
|
1227
|
+
"value_uid": arg(help="Value UID to link it to (same scope)"),
|
|
1228
|
+
"name_source": opt(
|
|
1229
|
+
"--name-source",
|
|
1230
|
+
choices=["goal", "value"],
|
|
1231
|
+
help="When the leaf names differ, which name wins — the loser "
|
|
1232
|
+
"is renamed to match (with descendant cascade). "
|
|
1233
|
+
"Default: goal.",
|
|
1234
|
+
),
|
|
1235
|
+
"group_uid": opt(
|
|
1236
|
+
"--group-uid",
|
|
1237
|
+
help="Group UID for a group-scoped link (goal + value both in "
|
|
1238
|
+
"that group, e.g. a mentee's). Omit for your personal scope."),
|
|
1239
|
+
},
|
|
1240
|
+
)
|
|
1241
|
+
async def link_goal_value(
|
|
1242
|
+
client: TetraClient,
|
|
1243
|
+
*,
|
|
1244
|
+
uid: str,
|
|
1245
|
+
value_uid: str,
|
|
1246
|
+
name_source: str | None = None,
|
|
1247
|
+
group_uid: str | None = None,
|
|
1248
|
+
) -> dict[str, Any]:
|
|
1249
|
+
"""Create the explicit 1:1 mirror link between a goal and a value.
|
|
1250
|
+
|
|
1251
|
+
Same scope required; a value can be the mirror of at most one goal
|
|
1252
|
+
(409 when it is already linked elsewhere). While linked, leaf renames
|
|
1253
|
+
propagate across the link in both directions. Independent of the
|
|
1254
|
+
draws-from/adds-to assignment in the goal's ``values``.
|
|
1255
|
+
|
|
1256
|
+
Args:
|
|
1257
|
+
client: Authenticated TetraClient
|
|
1258
|
+
uid: Goal UID.
|
|
1259
|
+
value_uid: Value UID in the same scope.
|
|
1260
|
+
name_source: 'goal' or 'value' — which name wins when the leaves
|
|
1261
|
+
differ (the loser is renamed with cascade). Defaults to 'goal'.
|
|
1262
|
+
group_uid: Group UID for a group-scoped link (both goal and value
|
|
1263
|
+
live in that group, e.g. a mentee's); omit for personal scope.
|
|
1264
|
+
|
|
1265
|
+
Returns:
|
|
1266
|
+
The updated goal dict, including ``linked_value`` {uid, name}.
|
|
1267
|
+
"""
|
|
1268
|
+
body: dict[str, Any] = {"value_uid": value_uid}
|
|
1269
|
+
if name_source is not None:
|
|
1270
|
+
body["name_source"] = name_source
|
|
1271
|
+
path = f"/api/v1/goals/{uid}/link-value"
|
|
1272
|
+
if group_uid is not None:
|
|
1273
|
+
path += f"?group_uid={group_uid}"
|
|
1274
|
+
return await client.post(path, json=body)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
@operation(
|
|
1278
|
+
cli="goals unlink-value",
|
|
1279
|
+
summary="Remove a goal's mirror link; both sides rename independently.",
|
|
1280
|
+
covers=[("DELETE", "/api/v1/goals/{uid}/link-value")],
|
|
1281
|
+
params={
|
|
1282
|
+
"uid": arg(help="Goal UID to unlink"),
|
|
1283
|
+
"group_uid": opt(
|
|
1284
|
+
"--group-uid",
|
|
1285
|
+
help="Group UID for a group-scoped unlink (e.g. a mentee's goal). "
|
|
1286
|
+
"Omit for your personal scope."),
|
|
1287
|
+
},
|
|
1288
|
+
)
|
|
1289
|
+
async def unlink_goal_value(
|
|
1290
|
+
client: TetraClient,
|
|
1291
|
+
*,
|
|
1292
|
+
uid: str,
|
|
1293
|
+
group_uid: str | None = None,
|
|
1294
|
+
) -> dict[str, Any]:
|
|
1295
|
+
"""Remove the goal's explicit value link.
|
|
1296
|
+
|
|
1297
|
+
The draws-from/adds-to assignment in ``values`` is kept — unlinking
|
|
1298
|
+
only stops rename mirroring.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
client: Authenticated TetraClient
|
|
1302
|
+
uid: Goal UID.
|
|
1303
|
+
group_uid: Group UID for a group-scoped unlink (e.g. a mentee's
|
|
1304
|
+
goal); omit for personal scope.
|
|
1305
|
+
|
|
1306
|
+
Returns:
|
|
1307
|
+
The updated goal dict with ``linked_value: null``.
|
|
1308
|
+
"""
|
|
1309
|
+
path = f"/api/v1/goals/{uid}/link-value"
|
|
1310
|
+
if group_uid is not None:
|
|
1311
|
+
path += f"?group_uid={group_uid}"
|
|
1312
|
+
return await client.delete(path)
|
|
@@ -484,6 +484,25 @@ async def mark_viewed(client: TetraClient) -> dict[str, Any]:
|
|
|
484
484
|
return await client.post("/api/v1/social/mark-viewed")
|
|
485
485
|
|
|
486
486
|
|
|
487
|
+
@operation(
|
|
488
|
+
cli="social notifications-mark-viewed",
|
|
489
|
+
summary="Mark all social notifications viewed (clears the nav badge).",
|
|
490
|
+
covers=[("POST", "/api/v1/social/notifications/mark-viewed")],
|
|
491
|
+
)
|
|
492
|
+
async def mark_notifications_viewed(client: TetraClient) -> dict[str, Any]:
|
|
493
|
+
"""Mark every unviewed social notification as viewed.
|
|
494
|
+
|
|
495
|
+
Takes no body; keys off the authenticated caller.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
client: Authenticated TetraClient
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Dict with the count of notifications marked viewed.
|
|
502
|
+
"""
|
|
503
|
+
return await client.post("/api/v1/social/notifications/mark-viewed")
|
|
504
|
+
|
|
505
|
+
|
|
487
506
|
# =============================================================================
|
|
488
507
|
# SEARCH / INVITE LINKS / SHARING
|
|
489
508
|
# =============================================================================
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Pure async operations for skill-tree rank measures.
|
|
2
|
+
|
|
3
|
+
A rank measure controls how a published skill tree's cohort standings are
|
|
4
|
+
scored, per goal type. Every MeasureSpec (the shape below) is a JSON object:
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"form": "scalar" | "target_shaped",
|
|
8
|
+
"agg": "sum" | "max" | "count" | "total_duration" | null,
|
|
9
|
+
"column": str | null,
|
|
10
|
+
"equation": str | null,
|
|
11
|
+
"dimensions": [{"column": str, "direction": "asc" | "desc",
|
|
12
|
+
"agg": "max" | "sum"}, ...],
|
|
13
|
+
"label": str | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
``form="scalar"`` needs ``agg`` (plus ``column`` or ``equation`` for outcome
|
|
17
|
+
types; ``total_duration``/``count`` need neither). ``form="target_shaped"``
|
|
18
|
+
needs a non-empty ordered ``dimensions`` list — dimension 0 is the primary
|
|
19
|
+
sort key, later entries break ties, each compared per its own ``direction``.
|
|
20
|
+
|
|
21
|
+
There are two config layers, both keyed by ``type_key`` (a goal's outcome
|
|
22
|
+
schema key):
|
|
23
|
+
- The tree's canonical ``measures`` — set only by the tree's author via
|
|
24
|
+
:func:`set_tree_rank_measures`; drives the persisted standings
|
|
25
|
+
(``SharedGoalRanking``) everyone sees.
|
|
26
|
+
- Each caller's own personal ``prefs`` — set via
|
|
27
|
+
:func:`set_tree_rank_measure_pref`; a client-side-only re-rank
|
|
28
|
+
preference that never touches the canonical measures or persisted
|
|
29
|
+
standings.
|
|
30
|
+
|
|
31
|
+
:func:`get_tree_rank_measures` returns the canonical measures plus, for each
|
|
32
|
+
goal type present in the tree, its ``outcome_columns`` — the valid values for
|
|
33
|
+
a scalar spec's ``column`` or a dimension's ``column``.
|
|
34
|
+
"""
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from tetra_cli.api_client import TetraClient
|
|
38
|
+
from tetra_cli.registry import arg, operation, opt
|
|
39
|
+
|
|
40
|
+
_MEASURE_SPEC_SHAPE = (
|
|
41
|
+
"A MeasureSpec is a JSON object: {form: 'scalar'|'target_shaped', "
|
|
42
|
+
"agg: 'sum'|'max'|'count'|'total_duration'|null, column: str|null, "
|
|
43
|
+
"equation: str|null, dimensions: [{column, direction:'asc'|'desc', "
|
|
44
|
+
"agg:'max'|'sum'}, ...], label: str|null}. form='scalar' needs agg "
|
|
45
|
+
"(plus column or equation for outcome types; total_duration/count need "
|
|
46
|
+
"neither); form='target_shaped' needs a non-empty ordered dimensions "
|
|
47
|
+
"list (dimension 0 is primary, later entries break ties). column/"
|
|
48
|
+
"dimension columns must be one of the goal type's outcome_columns from "
|
|
49
|
+
"get_tree_rank_measures."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@operation(
|
|
54
|
+
cli="values tree rank-measures",
|
|
55
|
+
summary=(
|
|
56
|
+
"Get a published skill tree's configured rank measures plus its "
|
|
57
|
+
"available goal types. Returns {measures: {type_key: MeasureSpec}, "
|
|
58
|
+
"types: [{type_key, label, outcome_columns}]}. " + _MEASURE_SPEC_SHAPE
|
|
59
|
+
),
|
|
60
|
+
covers=[("GET", "/api/v1/skill-trees/{tree_uid}/rank-measures")],
|
|
61
|
+
params={"tree_uid": arg(help="Root value UID of the published skill tree.")},
|
|
62
|
+
)
|
|
63
|
+
async def get_tree_rank_measures(client: TetraClient, tree_uid: str) -> dict[str, Any]:
|
|
64
|
+
"""Get a skill tree's configured rank measures and available goal types.
|
|
65
|
+
|
|
66
|
+
Any caller who can view at least one of the tree's publish scopes may
|
|
67
|
+
call this — reuses the change-request view check, since rank-measure
|
|
68
|
+
config is standings metadata, not sensitive.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
client: Authenticated TetraClient.
|
|
72
|
+
tree_uid: Root value UID of the published skill tree.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict with ``measures`` (``type_key`` -> MeasureSpec dict) and
|
|
76
|
+
``types`` (list of ``{type_key, label, outcome_columns}``).
|
|
77
|
+
"""
|
|
78
|
+
return await client.get(f"/api/v1/skill-trees/{tree_uid}/rank-measures")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@operation(
|
|
82
|
+
cli="values tree set-rank-measures",
|
|
83
|
+
summary=(
|
|
84
|
+
"Author-only: set a published skill tree's canonical rank measure "
|
|
85
|
+
"for one or more goal types, synchronously recomputing every "
|
|
86
|
+
"existing standings row for the tree's cohort goals. measures is a "
|
|
87
|
+
"JSON object mapping type_key -> MeasureSpec: {full replacement per "
|
|
88
|
+
"type_key given; types omitted from measures are left untouched}. "
|
|
89
|
+
+ _MEASURE_SPEC_SHAPE + " Call get_tree_rank_measures first to see "
|
|
90
|
+
"each type's outcome_columns."
|
|
91
|
+
),
|
|
92
|
+
covers=[("PUT", "/api/v1/skill-trees/{tree_uid}/rank-measures")],
|
|
93
|
+
params={
|
|
94
|
+
"tree_uid": arg(help="Root value UID of the published skill tree (author only)."),
|
|
95
|
+
"measures": opt(
|
|
96
|
+
"--measures", json=True,
|
|
97
|
+
help="JSON object: {type_key: MeasureSpec, ...}.",
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
async def set_tree_rank_measures(
|
|
102
|
+
client: TetraClient, tree_uid: str, measures: dict[str, Any]
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""Author-only: set a tree's canonical rank measure per goal type.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
client: Authenticated TetraClient.
|
|
108
|
+
tree_uid: Root value UID of the published skill tree.
|
|
109
|
+
measures: Dict mapping ``type_key`` -> MeasureSpec dict.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dict with the saved ``measures`` map.
|
|
113
|
+
"""
|
|
114
|
+
return await client.put(f"/api/v1/skill-trees/{tree_uid}/rank-measures", json=measures)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@operation(
|
|
118
|
+
cli="values tree set-rank-measure-pref",
|
|
119
|
+
summary=(
|
|
120
|
+
"Set the caller's own personal rank-measure override for one goal "
|
|
121
|
+
"type on a published skill tree — a client-side re-rank preference "
|
|
122
|
+
"that never touches the tree's canonical measures or persisted "
|
|
123
|
+
"standings. spec is a single MeasureSpec JSON object (see below) "
|
|
124
|
+
"applied to type_key. " + _MEASURE_SPEC_SHAPE + " Returns the "
|
|
125
|
+
"caller's full updated prefs map {type_key: MeasureSpec}."
|
|
126
|
+
),
|
|
127
|
+
covers=[("PUT", "/api/v1/skill-trees/{tree_uid}/rank-measures/prefs")],
|
|
128
|
+
params={
|
|
129
|
+
"tree_uid": arg(help="Root value UID of the published skill tree."),
|
|
130
|
+
"type_key": arg(help="Goal type key this preference applies to."),
|
|
131
|
+
"spec": opt("--spec", json=True, help="MeasureSpec as a JSON object."),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
async def set_tree_rank_measure_pref(
|
|
135
|
+
client: TetraClient, tree_uid: str, type_key: str, spec: dict[str, Any]
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""Upsert the caller's own rank-measure preference for one goal type.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
client: Authenticated TetraClient.
|
|
141
|
+
tree_uid: Root value UID of the published skill tree.
|
|
142
|
+
type_key: Goal type key this preference applies to.
|
|
143
|
+
spec: MeasureSpec dict for this type.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Dict with the caller's full updated prefs map (``type_key`` ->
|
|
147
|
+
MeasureSpec dict).
|
|
148
|
+
"""
|
|
149
|
+
return await client.put(
|
|
150
|
+
f"/api/v1/skill-trees/{tree_uid}/rank-measures/prefs",
|
|
151
|
+
json={type_key: spec},
|
|
152
|
+
)
|