feat: category assignment in create_task / update_task (v0.4.10)
Verified via FW_DEBUG=1 + systematic param-name probing that the correct parameter is `taskCategoryId` with value = full metaId from get_categories (e.g. taskCategory/23431854_200). Numeric systemCategoryId alone causes API error; full metaId is accepted and stored. Changes: - create_task: add optional category_id parameter → sent as taskCategoryId - update_task: add optional category_id parameter → sent as taskCategoryId; guard now accepts category_id-only updates - get_tasks: expose category_id field in returned task objects - get_categories: update docstring (param name now known) - SPEC.md: document verified taskCategoryId param + clarify categories[] vs taskCategoryId field distinction - scripts/find_category_param.py: discovery script used to find param name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""Discover the category assignment parameter name for taskcreate2 / taskupdate2.
|
||||
|
||||
Usage:
|
||||
FW_DEBUG=1 python scripts/find_category_param.py
|
||||
|
||||
The script:
|
||||
1. Fetches the first available shopping-list category (German locale).
|
||||
2. For each candidate parameter name, creates a test task with that parameter set,
|
||||
checks whether the category appears in the response, then deletes the task.
|
||||
3. Reports which parameter name (if any) caused the category to be applied.
|
||||
|
||||
Prerequisites: credentials stored in OS keyring or FW_EMAIL / FW_PASSWORD set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Allow running from repo root without installing the package.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from mcp_familywall.auth import get_credentials
|
||||
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Shopping list to use for the test (adjust if needed).
|
||||
TEST_LIST_ID = "taskList/23431854_29740941"
|
||||
|
||||
# Category to assign — will be resolved from the first taskcategorysync entry
|
||||
# for this list in German locale; override here if desired.
|
||||
FORCED_CATEGORY_META_ID: str | None = None
|
||||
|
||||
# Parameter-name candidates not yet tested (already ruled out are commented out):
|
||||
CANDIDATES: list[dict[str, str | None]] = [
|
||||
# key → value format description
|
||||
# Full metaId variants
|
||||
{"key": "systemCategoryId", "fmt": "numeric"}, # just the number, e.g. "200"
|
||||
{"key": "taskCategorySystemId", "fmt": "numeric"},
|
||||
{"key": "categories", "fmt": "meta_id"}, # full taskCategory/… metaId
|
||||
{"key": "categoryIds", "fmt": "meta_id"},
|
||||
{"key": "taskCategoryName", "fmt": "name"}, # category name as string
|
||||
{"key": "categoryName", "fmt": "name"},
|
||||
{"key": "taskCategoryId", "fmt": "meta_id"}, # already tried but try numeric too
|
||||
# Numeric variants of already-tried names
|
||||
{"key": "taskCategoryId", "fmt": "numeric"},
|
||||
{"key": "categoryId", "fmt": "numeric"},
|
||||
{"key": "category", "fmt": "numeric"},
|
||||
{"key": "categoryMetaId", "fmt": "numeric"},
|
||||
]
|
||||
|
||||
|
||||
def _resolve_category(
|
||||
client: FamilyWallClient, list_id: str
|
||||
) -> tuple[str, str, str]:
|
||||
"""Return (meta_id, system_category_id_str, name) for the first German category."""
|
||||
data = client.call(
|
||||
"accgetallfamily",
|
||||
{"a01call": "taskcategorysync", "a02call": "tasksync"},
|
||||
)
|
||||
cats = data["a01"]["r"]["r"]["updatedCreated"]
|
||||
|
||||
# Also resolve list type for filtering.
|
||||
list_type: str | None = None
|
||||
try:
|
||||
list_data = client.call("taskgettasklists", {})
|
||||
for lst in list_data.get("a00", {}).get("r", {}).get("r", []) or []:
|
||||
if lst.get("metaId") == list_id:
|
||||
list_type = lst.get("taskListType")
|
||||
break
|
||||
except FamilyWallError:
|
||||
pass
|
||||
|
||||
for cat in cats:
|
||||
if cat.get("locale") != "de":
|
||||
continue
|
||||
if list_type and cat.get("taskListType") != list_type:
|
||||
continue
|
||||
meta_id: str = cat["metaId"]
|
||||
sys_id: str = str(cat.get("systemCategoryId", ""))
|
||||
name: str = cat.get("name", "")
|
||||
print(f" Using category: metaId={meta_id!r}, systemCategoryId={sys_id!r}, name={name!r}")
|
||||
return meta_id, sys_id, name
|
||||
|
||||
raise RuntimeError("No German category found for list; adjust TEST_LIST_ID.")
|
||||
|
||||
|
||||
def _get_task_categories(task_obj: dict) -> list[str]:
|
||||
"""Extract category names from a task API object."""
|
||||
cats = task_obj.get("categories") or []
|
||||
return [c.get("name", "") for c in cats if isinstance(c, dict)]
|
||||
|
||||
|
||||
def _delete_task(client: FamilyWallClient, task_id: str) -> None:
|
||||
try:
|
||||
client.call("metadelete", {"id": task_id})
|
||||
except FamilyWallError as exc:
|
||||
print(f" [warn] Could not delete task {task_id}: {exc}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
email, password = get_credentials()
|
||||
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
|
||||
print(f"\nResolving category for list {TEST_LIST_ID!r} …")
|
||||
if FORCED_CATEGORY_META_ID:
|
||||
# Can't resolve name/numeric from meta_id easily; just use the meta_id.
|
||||
cat_meta = FORCED_CATEGORY_META_ID
|
||||
cat_numeric = cat_meta.split("_")[-1] if "_" in cat_meta else cat_meta
|
||||
cat_name = cat_meta
|
||||
else:
|
||||
cat_meta, cat_numeric, cat_name = _resolve_category(client, TEST_LIST_ID)
|
||||
|
||||
value_map = {
|
||||
"meta_id": cat_meta,
|
||||
"numeric": cat_numeric,
|
||||
"name": cat_name,
|
||||
}
|
||||
|
||||
print(f"\nTesting {len(CANDIDATES)} candidate parameter names …\n")
|
||||
results: list[tuple[str, str, bool]] = []
|
||||
|
||||
seen: set[tuple[str, str]] = set() # avoid duplicate tests
|
||||
|
||||
for cand in CANDIDATES:
|
||||
key: str = cand["key"] # type: ignore[assignment]
|
||||
fmt: str = cand["fmt"] # type: ignore[assignment]
|
||||
value: str = value_map[fmt]
|
||||
pair = (key, value)
|
||||
if pair in seen:
|
||||
continue
|
||||
seen.add(pair)
|
||||
|
||||
params: dict = {
|
||||
"taskListId": TEST_LIST_ID,
|
||||
"text": f"[TEST] cat-param-probe {key}={fmt}",
|
||||
key: value,
|
||||
}
|
||||
|
||||
print(f" Testing {key!r} = {value!r} …", end=" ", flush=True)
|
||||
try:
|
||||
data = client.call("taskcreate2", params)
|
||||
except FamilyWallError as exc:
|
||||
print(f"API ERROR: {exc}")
|
||||
results.append((key, value, False))
|
||||
continue
|
||||
|
||||
task_obj = data.get("a00", {}).get("r", {}).get("r", {})
|
||||
task_id: str = task_obj.get("metaId", "")
|
||||
applied_cats = _get_task_categories(task_obj)
|
||||
|
||||
# A non-default category was applied when the name matches our target.
|
||||
success = any(cat_name in c or c == cat_name for c in applied_cats)
|
||||
|
||||
if success:
|
||||
print(f"SUCCESS -> categories={applied_cats}")
|
||||
else:
|
||||
print(f"no effect -> categories={applied_cats}")
|
||||
|
||||
results.append((key, value, success))
|
||||
|
||||
if task_id:
|
||||
_delete_task(client, task_id)
|
||||
|
||||
client.logout()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------
|
||||
print("\n" + "=" * 60)
|
||||
print("RESULTS")
|
||||
print("=" * 60)
|
||||
hits = [(k, v) for k, v, ok in results if ok]
|
||||
misses = [(k, v) for k, v, ok in results if not ok]
|
||||
|
||||
if hits:
|
||||
print(f"\nWORKING parameter(s) found ({len(hits)}):")
|
||||
for k, v in hits:
|
||||
print(f" {k!r} = {v!r}")
|
||||
else:
|
||||
print("\nNo working parameter found.")
|
||||
|
||||
print(f"\nDid NOT work ({len(misses)}):")
|
||||
for k, v in misses:
|
||||
print(f" {k!r} = {v!r}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user