hap-cli 0.6.5__tar.gz → 0.6.7__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.
- {hap_cli-0.6.5 → hap_cli-0.6.7}/PKG-INFO +4 -3
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/README.md +3 -2
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/README_CN.md +3 -2
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/__init__.py +1 -1
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/contact_cmd.py +35 -21
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/department_cmd.py +26 -23
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/post_cmd.py +49 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/contact.py +40 -22
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/post.py +12 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/skills/SKILL.md +3 -2
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_core.py +35 -23
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_approval.py +1 -1
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_social.py +48 -7
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/PKG-INFO +4 -3
- {hap_cli-0.6.5 → hap_cli-0.6.7}/setup.py +1 -1
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/context.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/app.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/department.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/group.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/page.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/record.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/role.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/session.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/skills/__init__.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/SOURCES.txt +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.6.5 → hap_cli-0.6.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hap-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
4
4
|
Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
|
|
5
5
|
Author: hap-cli
|
|
6
6
|
License: Apache-2.0
|
|
@@ -333,8 +333,8 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
333
333
|
|
|
334
334
|
| Command | Description |
|
|
335
335
|
|---|---|
|
|
336
|
-
| `post list` | List feed posts |
|
|
337
|
-
| `post search` | Search posts by keyword and date range |
|
|
336
|
+
| `post list` | List feed posts (`--topic-id` filters by topic tag) |
|
|
337
|
+
| `post search` | Search posts by keyword and date range (`--topic-id` filters by topic tag) |
|
|
338
338
|
| `post get` | Show a post with its comment thread |
|
|
339
339
|
| `post create` | Create a new post |
|
|
340
340
|
| `post update` | Edit a post's content |
|
|
@@ -348,6 +348,7 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
348
348
|
| `post pin` | Pin a post (`--hours` for duration) |
|
|
349
349
|
| `post unpin` | Unpin a post |
|
|
350
350
|
| `post pinned` | List pinned posts |
|
|
351
|
+
| `post topics` | List topic tags (autocomplete) |
|
|
351
352
|
|
|
352
353
|
### calendar — Calendar Management
|
|
353
354
|
|
|
@@ -304,8 +304,8 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
304
304
|
|
|
305
305
|
| Command | Description |
|
|
306
306
|
|---|---|
|
|
307
|
-
| `post list` | List feed posts |
|
|
308
|
-
| `post search` | Search posts by keyword and date range |
|
|
307
|
+
| `post list` | List feed posts (`--topic-id` filters by topic tag) |
|
|
308
|
+
| `post search` | Search posts by keyword and date range (`--topic-id` filters by topic tag) |
|
|
309
309
|
| `post get` | Show a post with its comment thread |
|
|
310
310
|
| `post create` | Create a new post |
|
|
311
311
|
| `post update` | Edit a post's content |
|
|
@@ -319,6 +319,7 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
319
319
|
| `post pin` | Pin a post (`--hours` for duration) |
|
|
320
320
|
| `post unpin` | Unpin a post |
|
|
321
321
|
| `post pinned` | List pinned posts |
|
|
322
|
+
| `post topics` | List topic tags (autocomplete) |
|
|
322
323
|
|
|
323
324
|
### calendar — Calendar Management
|
|
324
325
|
|
|
@@ -311,8 +311,8 @@ hap --json worksheet record list 工作表ID
|
|
|
311
311
|
|
|
312
312
|
| 命令 | 说明 |
|
|
313
313
|
|---|---|
|
|
314
|
-
| `post list` |
|
|
315
|
-
| `post search` |
|
|
314
|
+
| `post list` | 列出动态帖子(`--topic-id` 按话题标签过滤) |
|
|
315
|
+
| `post search` | 按关键词和日期范围搜索帖子(`--topic-id` 按话题标签过滤) |
|
|
316
316
|
| `post get` | 查看帖子详情(含评论) |
|
|
317
317
|
| `post create` | 创建新帖子 |
|
|
318
318
|
| `post update` | 编辑帖子内容 |
|
|
@@ -326,6 +326,7 @@ hap --json worksheet record list 工作表ID
|
|
|
326
326
|
| `post pin` | 置顶帖子(`--hours` 指定时长) |
|
|
327
327
|
| `post unpin` | 取消置顶 |
|
|
328
328
|
| `post pinned` | 列出置顶帖子 |
|
|
329
|
+
| `post topics` | 列出话题标签(自动补全) |
|
|
329
330
|
|
|
330
331
|
### calendar — 日程管理
|
|
331
332
|
|
|
@@ -51,14 +51,33 @@ def _simplify_user(u: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
|
51
51
|
v = u.get(src)
|
|
52
52
|
if v:
|
|
53
53
|
out[dst] = v
|
|
54
|
+
# Newer endpoints (User.GetContactUserList,
|
|
55
|
+
# User.GetProjectResignedUserList) nest the department under
|
|
56
|
+
# `departmentInfo`. Flatten it here so the rendered shape stays flat.
|
|
57
|
+
dept = u.get("departmentInfo") or {}
|
|
58
|
+
if isinstance(dept, dict):
|
|
59
|
+
if dept.get("departmentName") and not out.get("department"):
|
|
60
|
+
out["department"] = dept["departmentName"]
|
|
61
|
+
if dept.get("departmentId"):
|
|
62
|
+
out["department_id"] = dept["departmentId"]
|
|
54
63
|
if u.get("accountStatus") and u["accountStatus"] != 0:
|
|
55
64
|
out["status"] = u["accountStatus"]
|
|
56
65
|
return out
|
|
57
66
|
|
|
58
67
|
|
|
59
68
|
def _simplify_search_result(raw: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
"""Unified envelope for the two contact-search endpoints.
|
|
70
|
+
|
|
71
|
+
Both `User.GetContactUserList` (after extracting the `users`
|
|
72
|
+
sub-dict in the core layer) and `User.GetProjectResignedUserList`
|
|
73
|
+
hand us a ``{"list": [...], "allCount": N}`` shape — project it to
|
|
74
|
+
``{"total": N, "users": [...]}`` so a single renderer handles both.
|
|
75
|
+
"""
|
|
76
|
+
users = [_simplify_user(u) for u in (raw.get("list") or [])]
|
|
77
|
+
return {
|
|
78
|
+
"total": raw.get("allCount", 0),
|
|
79
|
+
"users": [u for u in users if u],
|
|
80
|
+
}
|
|
62
81
|
|
|
63
82
|
|
|
64
83
|
def _simplify_friends_result(raw: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -151,40 +170,35 @@ def _render_friend_requests(summary: dict[str, Any]) -> None:
|
|
|
151
170
|
@contact.command("search")
|
|
152
171
|
@click.argument("keywords")
|
|
153
172
|
@org_id_option()
|
|
154
|
-
@click.option(
|
|
155
|
-
"--range", "range_", type=int, default=None,
|
|
156
|
-
help="Server-defined range bucket (numeric)",
|
|
157
|
-
)
|
|
158
173
|
@click.option("--page-size", "-n", default=20, help="Items per page")
|
|
159
174
|
@click.option("--page", "-p", default=1, help="Page number")
|
|
160
175
|
@click.option(
|
|
161
|
-
"--
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"--take-count/--no-take-count", "take_total_count", default=None,
|
|
166
|
-
help="Include total count in response",
|
|
176
|
+
"--resigned/--no-resigned",
|
|
177
|
+
"resigned",
|
|
178
|
+
default=False,
|
|
179
|
+
help="Search resigned members instead of active ones",
|
|
167
180
|
)
|
|
168
181
|
@pass_context
|
|
169
|
-
def contact_search(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
def contact_search(ctx, keywords, org_id, page_size, page, resigned):
|
|
183
|
+
"""Search members of an organization by keyword.
|
|
184
|
+
|
|
185
|
+
Defaults to active members. Pass --resigned to search former
|
|
186
|
+
members. Uses the current organization when --org-id is omitted.
|
|
187
|
+
"""
|
|
173
188
|
try:
|
|
174
189
|
session = ctx.get_session()
|
|
175
190
|
pid = require_api_project_id(session, org_id)
|
|
176
|
-
|
|
191
|
+
fn = contact_mod.search_resigned_users if resigned else contact_mod.search_contacts
|
|
192
|
+
raw = fn(
|
|
177
193
|
session,
|
|
178
194
|
keywords,
|
|
179
195
|
project_id=pid,
|
|
180
|
-
range_=range_,
|
|
181
196
|
page_index=page,
|
|
182
197
|
page_size=page_size,
|
|
183
|
-
is_filter_other=is_filter_other,
|
|
184
|
-
take_total_count=take_total_count,
|
|
185
198
|
)
|
|
186
199
|
summary = _simplify_search_result(raw)
|
|
187
|
-
|
|
200
|
+
header = "Resigned matches" if resigned else "Matches"
|
|
201
|
+
ctx.output(summary, lambda d: _render_users(d, header))
|
|
188
202
|
except Exception as e:
|
|
189
203
|
ctx.handle_error(e)
|
|
190
204
|
|
|
@@ -34,8 +34,8 @@ def _simplify_department(d: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
|
34
34
|
"id": d.get("departmentId") or d.get("id"),
|
|
35
35
|
"name": d.get("departmentName") or d.get("name"),
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# userCount from these list/tree endpoints is unreliable (always 0) —
|
|
38
|
+
# use `department members` for a real count. Intentionally omitted.
|
|
39
39
|
if d.get("haveSubDepartment") is not None:
|
|
40
40
|
out["has_children"] = bool(d.get("haveSubDepartment"))
|
|
41
41
|
if d.get("disabled"):
|
|
@@ -75,13 +75,23 @@ def _simplify_department_members(raw: dict[str, Any]) -> dict[str, Any]:
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _simplify_dept_search_node(node: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
79
|
+
# SearchDeptAndUsers returns departments as a recursive tree under
|
|
80
|
+
# the `subs` field (distinct from GetProjectSubDepartmentTree which
|
|
81
|
+
# uses `subDepartments`).
|
|
82
|
+
base = _simplify_department(node)
|
|
83
|
+
if base is None or not isinstance(node, dict):
|
|
84
|
+
return base
|
|
85
|
+
subs = node.get("subs") or []
|
|
86
|
+
children = [c for c in (_simplify_dept_search_node(s) for s in subs) if c]
|
|
87
|
+
if children:
|
|
88
|
+
base["children"] = children
|
|
89
|
+
return base
|
|
90
|
+
|
|
91
|
+
|
|
78
92
|
def _simplify_dept_search(raw: dict[str, Any]) -> dict[str, Any]:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
"users": [u for u in users if u],
|
|
83
|
-
"departments": [d for d in depts if d],
|
|
84
|
-
}
|
|
93
|
+
depts = [_simplify_dept_search_node(d) for d in (raw.get("departments") or [])]
|
|
94
|
+
return {"departments": [d for d in depts if d]}
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
# ── Human-readable renderers ───────────────────────────────────────────────
|
|
@@ -94,8 +104,6 @@ def _render_departments(depts: list[dict[str, Any]], header: str = "Departments"
|
|
|
94
104
|
click.echo(f"{header} ({len(depts)}):")
|
|
95
105
|
for d in depts:
|
|
96
106
|
extras = []
|
|
97
|
-
if d.get("user_count") is not None:
|
|
98
|
-
extras.append(f"{d['user_count']} members")
|
|
99
107
|
if d.get("has_children"):
|
|
100
108
|
extras.append("has sub-departments")
|
|
101
109
|
if d.get("disabled"):
|
|
@@ -111,8 +119,7 @@ def _render_tree(nodes: list[dict[str, Any]], depth: int = 0) -> None:
|
|
|
111
119
|
return
|
|
112
120
|
for n in nodes:
|
|
113
121
|
indent = " " * depth
|
|
114
|
-
|
|
115
|
-
click.echo(f"{indent}• {n.get('name') or '?'}{count}")
|
|
122
|
+
click.echo(f"{indent}• {n.get('name') or '?'}")
|
|
116
123
|
click.echo(f"{indent} id: {n.get('id')}")
|
|
117
124
|
if n.get("children"):
|
|
118
125
|
_render_tree(n["children"], depth + 1)
|
|
@@ -123,8 +130,6 @@ def _render_department_info(info: dict[str, Any]) -> None:
|
|
|
123
130
|
click.echo(f"ID: {info.get('id')}")
|
|
124
131
|
if info.get("parent_name") or info.get("parent_id"):
|
|
125
132
|
click.echo(f"Parent: {info.get('parent_name') or ''} ({info.get('parent_id') or ''})")
|
|
126
|
-
if info.get("user_count") is not None:
|
|
127
|
-
click.echo(f"Members: {info['user_count']}")
|
|
128
133
|
if info.get("disabled"):
|
|
129
134
|
click.echo("Status: disabled")
|
|
130
135
|
chargers = info.get("charge_users") or []
|
|
@@ -136,14 +141,11 @@ def _render_department_info(info: dict[str, Any]) -> None:
|
|
|
136
141
|
|
|
137
142
|
def _render_dept_search(summary: dict[str, Any]) -> None:
|
|
138
143
|
depts = summary.get("departments") or []
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
click.echo("No matches.")
|
|
144
|
+
if not depts:
|
|
145
|
+
click.echo("No matching departments.")
|
|
142
146
|
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if users:
|
|
146
|
-
_render_users({"users": users}, "Matching users")
|
|
147
|
+
click.echo(f"Matching departments ({len(depts)}):")
|
|
148
|
+
_render_tree(depts)
|
|
147
149
|
|
|
148
150
|
|
|
149
151
|
# ── Commands ───────────────────────────────────────────────────────────────
|
|
@@ -249,11 +251,12 @@ def department_members(
|
|
|
249
251
|
"--include-disabled/--no-include-disabled",
|
|
250
252
|
"include_disabled",
|
|
251
253
|
default=None,
|
|
252
|
-
help="Include disabled departments
|
|
254
|
+
help="Include disabled departments",
|
|
253
255
|
)
|
|
254
256
|
@pass_context
|
|
255
257
|
def department_search(ctx, keywords, org_id, include_disabled):
|
|
256
|
-
"""Search departments
|
|
258
|
+
"""Search departments by keyword. Results render as a tree showing
|
|
259
|
+
each match with its sub-departments."""
|
|
257
260
|
try:
|
|
258
261
|
session = ctx.get_session()
|
|
259
262
|
pid = require_api_project_id(session, org_id)
|
|
@@ -431,6 +431,12 @@ def _normalize_scope(scope: str) -> str:
|
|
|
431
431
|
@click.option("--start-date", default="", help="Only posts on or after this date (YYYY-MM-DD)")
|
|
432
432
|
@click.option("--end-date", default="", help="Only posts on or before this date (YYYY-MM-DD)")
|
|
433
433
|
@click.option("--post-type", default=-1, type=int, show_default=True, help="Filter by post type; -1 means all")
|
|
434
|
+
@click.option(
|
|
435
|
+
"--topic-id",
|
|
436
|
+
"topic_id",
|
|
437
|
+
default="",
|
|
438
|
+
help="Filter by topic tag id (see `hap post topics`)",
|
|
439
|
+
)
|
|
434
440
|
@click.option("-p", "--page", default=1, show_default=True, help="Page number (1-based)")
|
|
435
441
|
@click.option(
|
|
436
442
|
"--cursor",
|
|
@@ -449,6 +455,7 @@ def post_list(
|
|
|
449
455
|
start_date,
|
|
450
456
|
end_date,
|
|
451
457
|
post_type,
|
|
458
|
+
topic_id,
|
|
452
459
|
page,
|
|
453
460
|
last_post_auto_id,
|
|
454
461
|
):
|
|
@@ -487,6 +494,7 @@ def post_list(
|
|
|
487
494
|
start_date=start_date,
|
|
488
495
|
end_date=end_date,
|
|
489
496
|
post_type=post_type,
|
|
497
|
+
category_id=topic_id,
|
|
490
498
|
page_index=page,
|
|
491
499
|
last_post_auto_id=last_post_auto_id,
|
|
492
500
|
)
|
|
@@ -512,6 +520,12 @@ def post_list(
|
|
|
512
520
|
@click.option("--post-type", default=-1, type=int, show_default=True, help="Filter by post type; -1 means all")
|
|
513
521
|
@click.option("--account-id", default="", help="Filter by author member id")
|
|
514
522
|
@click.option("--group-id", default="", help="Filter by group id")
|
|
523
|
+
@click.option(
|
|
524
|
+
"--topic-id",
|
|
525
|
+
"topic_id",
|
|
526
|
+
default="",
|
|
527
|
+
help="Filter by topic tag id (see `hap post topics`)",
|
|
528
|
+
)
|
|
515
529
|
@click.option("-p", "--page", default=1, show_default=True, help="Page number (1-based)")
|
|
516
530
|
@click.option(
|
|
517
531
|
"--cursor",
|
|
@@ -530,6 +544,7 @@ def post_search(
|
|
|
530
544
|
post_type,
|
|
531
545
|
account_id,
|
|
532
546
|
group_id,
|
|
547
|
+
topic_id,
|
|
533
548
|
page,
|
|
534
549
|
last_post_auto_id,
|
|
535
550
|
):
|
|
@@ -557,6 +572,7 @@ def post_search(
|
|
|
557
572
|
account_id=account_id,
|
|
558
573
|
group_id=group_id,
|
|
559
574
|
post_type=post_type,
|
|
575
|
+
category_id=topic_id,
|
|
560
576
|
page_index=page,
|
|
561
577
|
last_post_auto_id=last_post_auto_id,
|
|
562
578
|
)
|
|
@@ -870,3 +886,36 @@ def post_unpin(ctx, post_id):
|
|
|
870
886
|
))
|
|
871
887
|
except Exception as e:
|
|
872
888
|
ctx.handle_error(e)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _simplify_topic(t: dict) -> dict:
|
|
892
|
+
return {"id": t.get("id"), "name": t.get("value")}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _render_topics(summary: dict) -> None:
|
|
896
|
+
topics = summary.get("topics") or []
|
|
897
|
+
if not topics:
|
|
898
|
+
click.echo("No topics found.")
|
|
899
|
+
return
|
|
900
|
+
click.echo(f"Topics ({len(topics)}):")
|
|
901
|
+
for t in topics:
|
|
902
|
+
click.echo(f" #{t.get('name')}# id: {t.get('id')}")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
@post.command("topics")
|
|
906
|
+
@click.option("--keywords", "-k", default="", help="Filter by keyword")
|
|
907
|
+
@pass_context
|
|
908
|
+
def post_topics(ctx, keywords):
|
|
909
|
+
"""List topic tags (autocomplete suggestions).
|
|
910
|
+
|
|
911
|
+
Use a topic inside a post by wrapping its name in hashes
|
|
912
|
+
(e.g. `#周报#`) in the post message. The tag links automatically
|
|
913
|
+
on publish, and unknown names are created on the fly.
|
|
914
|
+
"""
|
|
915
|
+
try:
|
|
916
|
+
session = ctx.get_session()
|
|
917
|
+
raw = post_mod.get_topics(session, keywords=keywords)
|
|
918
|
+
topics = [_simplify_topic(t) for t in (raw or []) if isinstance(t, dict)]
|
|
919
|
+
ctx.output({"topics": topics}, _render_topics)
|
|
920
|
+
except Exception as e:
|
|
921
|
+
ctx.handle_error(e)
|
|
@@ -26,37 +26,55 @@ from hap_cli.core.session import Session
|
|
|
26
26
|
def search_contacts(
|
|
27
27
|
session: Session,
|
|
28
28
|
keywords: str,
|
|
29
|
-
project_id: str
|
|
30
|
-
range_: Optional[int] = None,
|
|
29
|
+
project_id: str,
|
|
31
30
|
page_index: int = 1,
|
|
32
31
|
page_size: int = 20,
|
|
33
|
-
is_filter_other: Optional[bool] = None,
|
|
34
|
-
take_total_count: Optional[bool] = None,
|
|
35
32
|
) -> dict[str, Any]:
|
|
36
|
-
"""Search
|
|
37
|
-
|
|
38
|
-
Wraps `
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
"""Search active members of an organization by keyword.
|
|
34
|
+
|
|
35
|
+
Wraps `User.GetContactUserList`. The server response carries both
|
|
36
|
+
``oftenUsers`` (frequently-contacted shortcut) and ``users`` (full
|
|
37
|
+
matches); only the latter is useful for a general search, so this
|
|
38
|
+
function returns ``raw["users"]`` with shape
|
|
39
|
+
``{"list": [...], "allCount": N}`` — the same envelope produced by
|
|
40
|
+
:func:`search_resigned_users` so both can share simplifiers.
|
|
41
|
+
|
|
42
|
+
``dataRange`` is fixed at 2 (whole organization) because the
|
|
43
|
+
alternate buckets are not exposed via the CLI.
|
|
45
44
|
"""
|
|
46
45
|
data: dict[str, Any] = {
|
|
47
46
|
"keywords": keywords,
|
|
47
|
+
"projectId": project_id,
|
|
48
|
+
"dataRange": 2,
|
|
48
49
|
"pageIndex": page_index,
|
|
49
50
|
"pageSize": page_size,
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
raw = session.api_call("User", "GetContactUserList", data) or {}
|
|
53
|
+
return raw.get("users") or {"list": [], "allCount": 0}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def search_resigned_users(
|
|
57
|
+
session: Session,
|
|
58
|
+
keywords: str,
|
|
59
|
+
project_id: str,
|
|
60
|
+
page_index: int = 1,
|
|
61
|
+
page_size: int = 20,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Search resigned members of an organization by keyword.
|
|
64
|
+
|
|
65
|
+
Wraps `User.GetProjectResignedUserList`. Returns the raw
|
|
66
|
+
``{"list": [...], "allCount": N}`` envelope — matches the shape
|
|
67
|
+
produced by :func:`search_contacts` on purpose so callers can feed
|
|
68
|
+
both through one simplifier.
|
|
69
|
+
"""
|
|
70
|
+
data: dict[str, Any] = {
|
|
71
|
+
"projectId": project_id,
|
|
72
|
+
"pageIndex": page_index,
|
|
73
|
+
"pageSize": page_size,
|
|
74
|
+
}
|
|
75
|
+
if keywords:
|
|
76
|
+
data["keywords"] = keywords
|
|
77
|
+
return session.api_call("User", "GetProjectResignedUserList", data)
|
|
60
78
|
|
|
61
79
|
|
|
62
80
|
def get_friends(
|
|
@@ -330,3 +330,15 @@ def add_top_post(
|
|
|
330
330
|
def remove_top_post(session: Session, post_id: str) -> dict[str, Any]:
|
|
331
331
|
"""Unpin via ``Post.RemoveTopPost``."""
|
|
332
332
|
return session.api_call("Post", "RemoveTopPost", {"postId": post_id})
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_topics(session: Session, keywords: str = "") -> list[dict[str, Any]]:
|
|
336
|
+
"""Autocomplete topic tags via ``Category.AutoCompleteCategory``.
|
|
337
|
+
|
|
338
|
+
Returns ``[{id, value}]``. Callers don't need the id to use a tag
|
|
339
|
+
when posting — the server recognises ``#value#`` in the post body
|
|
340
|
+
and auto-links (or auto-creates) the matching topic.
|
|
341
|
+
"""
|
|
342
|
+
return session.api_call(
|
|
343
|
+
"Category", "AutoCompleteCategory", {"keywords": keywords or ""}
|
|
344
|
+
)
|
|
@@ -190,8 +190,8 @@ hap department search KEYWORDS --org-id ORG_ID [--include-disabled]
|
|
|
190
190
|
|
|
191
191
|
### post - Feed & Post Management
|
|
192
192
|
```bash
|
|
193
|
-
hap post list [--org-id ORG_ID] [--scope org|user|fav|group|groups|myself] [--cursor AUTO_ID] [-p N]
|
|
194
|
-
hap post search --keywords TEXT [--org-id ORG_ID] [--start-date D] [--end-date D]
|
|
193
|
+
hap post list [--org-id ORG_ID] [--scope org|user|fav|group|groups|myself] [--topic-id TOPIC_ID] [--cursor AUTO_ID] [-p N]
|
|
194
|
+
hap post search --keywords TEXT [--org-id ORG_ID] [--start-date D] [--end-date D] [--topic-id TOPIC_ID]
|
|
195
195
|
hap post get POST_ID
|
|
196
196
|
hap post create --org-id ORG_ID --message TEXT [--share-group GID]... [--share-org OID]...
|
|
197
197
|
hap post update POST_ID --message TEXT [--old-message TEXT]
|
|
@@ -205,6 +205,7 @@ hap post favorite POST_ID [--remove]
|
|
|
205
205
|
hap post pin POST_ID [--hours N]
|
|
206
206
|
hap post unpin POST_ID
|
|
207
207
|
hap post pinned [--org-id ORG_ID]
|
|
208
|
+
hap post topics [--keywords K] # Hint: use #NAME# in message to tag
|
|
208
209
|
```
|
|
209
210
|
|
|
210
211
|
### calendar - Calendar & Schedule
|
|
@@ -1621,44 +1621,56 @@ class TestPage:
|
|
|
1621
1621
|
class TestContact:
|
|
1622
1622
|
@patch("hap_cli.core.session.requests.post")
|
|
1623
1623
|
def test_search_contacts_defaults(self, mock_post, mock_session):
|
|
1624
|
-
"""`
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
mock_post.return_value = _mock_response([{"accountId": "u1", "fullname": "Alice"}])
|
|
1624
|
+
"""`User.GetContactUserList` — defaults + `users` sub-field extraction."""
|
|
1625
|
+
mock_post.return_value = _mock_response({
|
|
1626
|
+
"oftenUsers": {"list": [{"accountId": "skip"}]},
|
|
1627
|
+
"users": {"list": [{"accountId": "u1", "fullname": "Alice"}], "allCount": 1},
|
|
1628
|
+
})
|
|
1630
1629
|
from hap_cli.core import contact as contact_mod
|
|
1631
1630
|
result = contact_mod.search_contacts(mock_session, "Alice", project_id="proj1")
|
|
1632
|
-
|
|
1631
|
+
# Only the `users` envelope is returned; `oftenUsers` is dropped.
|
|
1632
|
+
assert result == {"list": [{"accountId": "u1", "fullname": "Alice"}], "allCount": 1}
|
|
1633
1633
|
args = mock_post.call_args
|
|
1634
|
-
assert "
|
|
1634
|
+
assert "User/GetContactUserList" in args[0][0]
|
|
1635
1635
|
body = args[1]["json"]
|
|
1636
1636
|
assert body["keywords"] == "Alice"
|
|
1637
1637
|
assert body["projectId"] == "proj1"
|
|
1638
|
-
|
|
1638
|
+
assert body["dataRange"] == 2
|
|
1639
1639
|
assert body["pageIndex"] == 1
|
|
1640
1640
|
assert body["pageSize"] == 20
|
|
1641
1641
|
|
|
1642
1642
|
@patch("hap_cli.core.session.requests.post")
|
|
1643
|
-
def
|
|
1644
|
-
|
|
1643
|
+
def test_search_contacts_empty_envelope(self, mock_post, mock_session):
|
|
1644
|
+
"""Missing `users` sub-field degrades gracefully to an empty envelope."""
|
|
1645
|
+
mock_post.return_value = _mock_response({})
|
|
1645
1646
|
from hap_cli.core import contact as contact_mod
|
|
1646
|
-
contact_mod.search_contacts(
|
|
1647
|
-
mock_session,
|
|
1648
|
-
keywords="Zhang",
|
|
1649
|
-
project_id="proj1",
|
|
1650
|
-
range_=10,
|
|
1651
|
-
page_index=2,
|
|
1652
|
-
page_size=50,
|
|
1653
|
-
is_filter_other=True,
|
|
1654
|
-
take_total_count=True,
|
|
1647
|
+
result = contact_mod.search_contacts(
|
|
1648
|
+
mock_session, keywords="Zhang", project_id="proj1", page_index=2, page_size=50,
|
|
1655
1649
|
)
|
|
1650
|
+
assert result == {"list": [], "allCount": 0}
|
|
1656
1651
|
body = mock_post.call_args[1]["json"]
|
|
1657
|
-
assert body["range"] == 10
|
|
1658
1652
|
assert body["pageIndex"] == 2
|
|
1659
1653
|
assert body["pageSize"] == 50
|
|
1660
|
-
|
|
1661
|
-
|
|
1654
|
+
|
|
1655
|
+
@patch("hap_cli.core.session.requests.post")
|
|
1656
|
+
def test_search_resigned_users(self, mock_post, mock_session):
|
|
1657
|
+
"""`User.GetProjectResignedUserList` — shape matches active search."""
|
|
1658
|
+
mock_post.return_value = _mock_response({
|
|
1659
|
+
"list": [{"accountId": "r1", "fullname": "Gone"}],
|
|
1660
|
+
"allCount": 1,
|
|
1661
|
+
})
|
|
1662
|
+
from hap_cli.core import contact as contact_mod
|
|
1663
|
+
result = contact_mod.search_resigned_users(
|
|
1664
|
+
mock_session, keywords="Gone", project_id="proj1", page_index=2, page_size=50,
|
|
1665
|
+
)
|
|
1666
|
+
assert result == {"list": [{"accountId": "r1", "fullname": "Gone"}], "allCount": 1}
|
|
1667
|
+
args = mock_post.call_args
|
|
1668
|
+
assert "User/GetProjectResignedUserList" in args[0][0]
|
|
1669
|
+
body = args[1]["json"]
|
|
1670
|
+
assert body["keywords"] == "Gone"
|
|
1671
|
+
assert body["projectId"] == "proj1"
|
|
1672
|
+
assert body["pageIndex"] == 2
|
|
1673
|
+
assert body["pageSize"] == 50
|
|
1662
1674
|
|
|
1663
1675
|
@patch("hap_cli.core.session.requests.post")
|
|
1664
1676
|
def test_get_friends_full_fields(self, mock_post, mock_session):
|
|
@@ -60,7 +60,7 @@ def _dummy_account_id(integration_session, project_id):
|
|
|
60
60
|
res = contact.search_contacts(
|
|
61
61
|
integration_session, project_id=project_id, keywords="a", page_size=1
|
|
62
62
|
)
|
|
63
|
-
users = (res or {}).get("
|
|
63
|
+
users = (res or {}).get("list") or []
|
|
64
64
|
if not users:
|
|
65
65
|
pytest.skip("project has no users matching 'a' — cannot run delegation tests")
|
|
66
66
|
return users[0].get("accountId")
|
|
@@ -39,19 +39,26 @@ class TestChat:
|
|
|
39
39
|
class TestContact:
|
|
40
40
|
|
|
41
41
|
def test_01_search(self, integration_session, project_id):
|
|
42
|
-
"""
|
|
43
|
-
named ``SearchAddressbookAndDepartment`` and also emits an
|
|
44
|
-
always-empty ``departmentResult`` alongside it — the CLI only
|
|
45
|
-
surfaces the user matches, but both fields remain in the raw
|
|
46
|
-
envelope, so we assert on ``userResult`` only."""
|
|
42
|
+
"""`User.GetContactUserList` → unified ``{list, allCount}`` envelope."""
|
|
47
43
|
result = contact.search_contacts(
|
|
48
44
|
integration_session,
|
|
49
45
|
project_id=project_id,
|
|
50
46
|
keywords="a",
|
|
51
47
|
page_size=5,
|
|
52
48
|
)
|
|
53
|
-
assert_dict_like(result, ["
|
|
54
|
-
assert isinstance(result["
|
|
49
|
+
assert_dict_like(result, ["list"])
|
|
50
|
+
assert isinstance(result["list"], list)
|
|
51
|
+
|
|
52
|
+
def test_01b_search_resigned(self, integration_session, project_id):
|
|
53
|
+
"""`User.GetProjectResignedUserList` — same envelope as active search."""
|
|
54
|
+
result = contact.search_resigned_users(
|
|
55
|
+
integration_session,
|
|
56
|
+
project_id=project_id,
|
|
57
|
+
keywords="",
|
|
58
|
+
page_size=5,
|
|
59
|
+
)
|
|
60
|
+
assert_dict_like(result, ["list"])
|
|
61
|
+
assert isinstance(result["list"], list)
|
|
55
62
|
|
|
56
63
|
def test_02_resolve(self, integration_session):
|
|
57
64
|
"""AddressBook.GetUserAddressbookByKeywords returns a paged dict."""
|
|
@@ -146,5 +153,39 @@ class TestPost:
|
|
|
146
153
|
f"GetTopPosts expected list, got {type(result).__name__}"
|
|
147
154
|
)
|
|
148
155
|
|
|
156
|
+
def test_04_topics(self, integration_session):
|
|
157
|
+
"""Category.AutoCompleteCategory returns ``[{id, value}]``."""
|
|
158
|
+
result = post.get_topics(integration_session)
|
|
159
|
+
assert isinstance(result, list)
|
|
160
|
+
for t in result:
|
|
161
|
+
assert "id" in t and "value" in t
|
|
162
|
+
|
|
163
|
+
def test_05_topics_keyword_filter(self, integration_session):
|
|
164
|
+
"""Keyword filter narrows the suggestions."""
|
|
165
|
+
result = post.get_topics(integration_session, keywords="周")
|
|
166
|
+
assert isinstance(result, list)
|
|
167
|
+
# Every hit should match the keyword substring
|
|
168
|
+
for t in result:
|
|
169
|
+
assert "周" in (t.get("value") or "")
|
|
170
|
+
|
|
171
|
+
def test_06_list_by_topic(self, integration_session):
|
|
172
|
+
"""`catId` filter: every returned post carries that topic."""
|
|
173
|
+
topics = post.get_topics(integration_session, keywords="周")
|
|
174
|
+
if not topics:
|
|
175
|
+
pytest.skip("no topic tags available on this server")
|
|
176
|
+
topic_id = topics[0]["id"]
|
|
177
|
+
result = post.get_post_list(
|
|
178
|
+
integration_session,
|
|
179
|
+
list_type="project",
|
|
180
|
+
category_id=topic_id,
|
|
181
|
+
)
|
|
182
|
+
assert_dict_like(result, ["postList"])
|
|
183
|
+
for p in result["postList"]:
|
|
184
|
+
cats = p.get("categories") or []
|
|
185
|
+
ids = {c.get("catID") or c.get("catId") or c.get("id") for c in cats}
|
|
186
|
+
assert topic_id in ids, (
|
|
187
|
+
f"post {p.get('postID')} missing topic {topic_id}; got {ids}"
|
|
188
|
+
)
|
|
189
|
+
|
|
149
190
|
|
|
150
191
|
# Calendar coverage lives in ``test_integration_calendar.py``.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hap-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.7
|
|
4
4
|
Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
|
|
5
5
|
Author: hap-cli
|
|
6
6
|
License: Apache-2.0
|
|
@@ -333,8 +333,8 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
333
333
|
|
|
334
334
|
| Command | Description |
|
|
335
335
|
|---|---|
|
|
336
|
-
| `post list` | List feed posts |
|
|
337
|
-
| `post search` | Search posts by keyword and date range |
|
|
336
|
+
| `post list` | List feed posts (`--topic-id` filters by topic tag) |
|
|
337
|
+
| `post search` | Search posts by keyword and date range (`--topic-id` filters by topic tag) |
|
|
338
338
|
| `post get` | Show a post with its comment thread |
|
|
339
339
|
| `post create` | Create a new post |
|
|
340
340
|
| `post update` | Edit a post's content |
|
|
@@ -348,6 +348,7 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
348
348
|
| `post pin` | Pin a post (`--hours` for duration) |
|
|
349
349
|
| `post unpin` | Unpin a post |
|
|
350
350
|
| `post pinned` | List pinned posts |
|
|
351
|
+
| `post topics` | List topic tags (autocomplete) |
|
|
351
352
|
|
|
352
353
|
### calendar — Calendar Management
|
|
353
354
|
|
|
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
|
|
|
4
4
|
|
|
5
5
|
setup(
|
|
6
6
|
name="hap-cli",
|
|
7
|
-
version="0.6.
|
|
7
|
+
version="0.6.7",
|
|
8
8
|
description="CLI harness for MingDAO HAP - Enterprise no-code platform",
|
|
9
9
|
long_description=open("hap_cli/README.md").read(),
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|