tapdctl 0.1.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.
@@ -0,0 +1,9 @@
1
+ .env
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ .ruff_cache/
8
+ .playwright-mcp/
9
+ .claude/
tapdctl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: tapdctl
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: httpx>=0.28.0
6
+ Requires-Dist: typer>=0.16.0
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "tapdctl"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "httpx>=0.28.0",
7
+ "typer>=0.16.0",
8
+ ]
9
+
10
+ [project.scripts]
11
+ tapdctl = "tapdctl.main:app"
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["apolloconfigctl"]
19
+
File without changes
@@ -0,0 +1,457 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ from typing import Optional
5
+
6
+ import httpx
7
+ import typer
8
+
9
+ app = typer.Typer(help="tapdctl — TAPD MCP Gateway CLI", add_completion=False)
10
+
11
+ _DEFAULT_API_URL = os.getenv("TAPD_API_URL", "http://localhost:8000")
12
+
13
+
14
+ def _api(api_url: str) -> str:
15
+ return api_url.rstrip("/") + "/api"
16
+
17
+
18
+ def _get(api_url: str, path: str, params: dict) -> object:
19
+ params = {k: v for k, v in params.items() if v is not None}
20
+ resp = httpx.get(f"{_api(api_url)}/{path}", params=params, timeout=30)
21
+ resp.raise_for_status()
22
+ return resp.json()
23
+
24
+
25
+ def _post(api_url: str, path: str, body: dict) -> object:
26
+ body = {k: v for k, v in body.items() if v is not None}
27
+ resp = httpx.post(f"{_api(api_url)}/{path}", json=body, timeout=30)
28
+ resp.raise_for_status()
29
+ return resp.json()
30
+
31
+
32
+ def _put(api_url: str, path: str, body: dict) -> object:
33
+ body = {k: v for k, v in body.items() if v is not None}
34
+ resp = httpx.put(f"{_api(api_url)}/{path}", json=body, timeout=30)
35
+ resp.raise_for_status()
36
+ return resp.json()
37
+
38
+
39
+ def _out(data: object) -> None:
40
+ typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
41
+
42
+
43
+ # ── Stories ────────────────────────────────────────────────────────────────────
44
+
45
+ @app.command("get-stories")
46
+ def get_stories(
47
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
48
+ workspace_id: Optional[str] = typer.Option(None, help="项目ID"),
49
+ iteration_id: Optional[str] = typer.Option(None, help="迭代ID"),
50
+ status: Optional[str] = typer.Option(None, help="需求状态"),
51
+ owner: Optional[str] = typer.Option(None, help="处理人"),
52
+ created_from: Optional[str] = typer.Option(None, help="创建时间起始 YYYY-MM-DD HH:MM:SS"),
53
+ created_to: Optional[str] = typer.Option(None, help="创建时间结束"),
54
+ modified_from: Optional[str] = typer.Option(None, help="修改时间起始"),
55
+ modified_to: Optional[str] = typer.Option(None, help="修改时间结束"),
56
+ limit: int = typer.Option(30, help="每页数量"),
57
+ page: int = typer.Option(1, help="页码"),
58
+ fields: Optional[str] = typer.Option(None, help="返回字段,逗号分隔"),
59
+ with_status_label: bool = typer.Option(False, help="返回中文状态名"),
60
+ ):
61
+ """获取需求列表。"""
62
+ _out(_get(api_url, "getStories", dict(
63
+ workspace_id=workspace_id, iteration_id=iteration_id, status=status,
64
+ owner=owner, created_from=created_from, created_to=created_to,
65
+ modified_from=modified_from, modified_to=modified_to,
66
+ limit=limit, page=page, fields=fields,
67
+ with_status_label=with_status_label or None,
68
+ )))
69
+
70
+
71
+ @app.command("get-stories-count")
72
+ def get_stories_count(
73
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
74
+ workspace_id: Optional[str] = typer.Option(None),
75
+ iteration_id: Optional[str] = typer.Option(None),
76
+ status: Optional[str] = typer.Option(None),
77
+ owner: Optional[str] = typer.Option(None),
78
+ created_from: Optional[str] = typer.Option(None),
79
+ created_to: Optional[str] = typer.Option(None),
80
+ ):
81
+ """获取需求总数。"""
82
+ _out(_get(api_url, "getStoriesCount", dict(
83
+ workspace_id=workspace_id, iteration_id=iteration_id,
84
+ status=status, owner=owner,
85
+ created_from=created_from, created_to=created_to,
86
+ )))
87
+
88
+
89
+ @app.command("get-story-changes")
90
+ def get_story_changes(
91
+ story_id: str = typer.Argument(help="需求ID"),
92
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
93
+ workspace_id: Optional[str] = typer.Option(None),
94
+ limit: int = typer.Option(30),
95
+ page: int = typer.Option(1),
96
+ ):
97
+ """获取需求变更历史。"""
98
+ _out(_get(api_url, "getStoryChanges", dict(
99
+ story_id=story_id, workspace_id=workspace_id, limit=limit, page=page,
100
+ )))
101
+
102
+
103
+ @app.command("get-story-custom-fields")
104
+ def get_story_custom_fields(
105
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
106
+ workspace_id: Optional[str] = typer.Option(None),
107
+ ):
108
+ """获取需求自定义字段配置(custom_field_* 含义对照)。"""
109
+ _out(_get(api_url, "getStoryCustomFields", dict(workspace_id=workspace_id)))
110
+
111
+
112
+ # ── Bugs ───────────────────────────────────────────────────────────────────────
113
+
114
+ @app.command("get-bugs")
115
+ def get_bugs(
116
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
117
+ workspace_id: Optional[str] = typer.Option(None),
118
+ iteration_id: Optional[str] = typer.Option(None),
119
+ status: Optional[str] = typer.Option(None, help="缺陷状态"),
120
+ severity: Optional[str] = typer.Option(None, help="严重程度"),
121
+ current_owner: Optional[str] = typer.Option(None, help="处理人"),
122
+ reporter: Optional[str] = typer.Option(None, help="创建人"),
123
+ de: Optional[str] = typer.Option(None, help="开发人员"),
124
+ te: Optional[str] = typer.Option(None, help="测试人员"),
125
+ created_from: Optional[str] = typer.Option(None),
126
+ created_to: Optional[str] = typer.Option(None),
127
+ modified_from: Optional[str] = typer.Option(None),
128
+ modified_to: Optional[str] = typer.Option(None),
129
+ limit: int = typer.Option(30),
130
+ page: int = typer.Option(1),
131
+ fields: Optional[str] = typer.Option(None),
132
+ with_status_label: bool = typer.Option(False),
133
+ ):
134
+ """获取缺陷列表。"""
135
+ _out(_get(api_url, "getBugs", dict(
136
+ workspace_id=workspace_id, iteration_id=iteration_id, status=status,
137
+ severity=severity, current_owner=current_owner, reporter=reporter,
138
+ de=de, te=te, created_from=created_from, created_to=created_to,
139
+ modified_from=modified_from, modified_to=modified_to,
140
+ limit=limit, page=page, fields=fields,
141
+ with_status_label=with_status_label or None,
142
+ )))
143
+
144
+
145
+ @app.command("get-bugs-count")
146
+ def get_bugs_count(
147
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
148
+ workspace_id: Optional[str] = typer.Option(None),
149
+ iteration_id: Optional[str] = typer.Option(None),
150
+ status: Optional[str] = typer.Option(None),
151
+ severity: Optional[str] = typer.Option(None),
152
+ current_owner: Optional[str] = typer.Option(None),
153
+ created_from: Optional[str] = typer.Option(None),
154
+ created_to: Optional[str] = typer.Option(None),
155
+ ):
156
+ """获取缺陷总数。"""
157
+ _out(_get(api_url, "getBugsCount", dict(
158
+ workspace_id=workspace_id, iteration_id=iteration_id,
159
+ status=status, severity=severity, current_owner=current_owner,
160
+ created_from=created_from, created_to=created_to,
161
+ )))
162
+
163
+
164
+ @app.command("get-bug-changes")
165
+ def get_bug_changes(
166
+ bug_id: str = typer.Argument(help="缺陷ID"),
167
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
168
+ workspace_id: Optional[str] = typer.Option(None),
169
+ limit: int = typer.Option(30),
170
+ page: int = typer.Option(1),
171
+ ):
172
+ """获取缺陷变更历史。"""
173
+ _out(_get(api_url, "getBugChanges", dict(
174
+ bug_id=bug_id, workspace_id=workspace_id, limit=limit, page=page,
175
+ )))
176
+
177
+
178
+ # ── Iterations ─────────────────────────────────────────────────────────────────
179
+
180
+ @app.command("get-iterations")
181
+ def get_iterations(
182
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
183
+ workspace_id: Optional[str] = typer.Option(None),
184
+ status: Optional[str] = typer.Option(None, help="迭代状态,如 open、done"),
185
+ limit: int = typer.Option(30),
186
+ page: int = typer.Option(1),
187
+ ):
188
+ """获取迭代列表。"""
189
+ _out(_get(api_url, "getIterations", dict(
190
+ workspace_id=workspace_id, status=status, limit=limit, page=page,
191
+ )))
192
+
193
+
194
+ @app.command("get-iterations-count")
195
+ def get_iterations_count(
196
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
197
+ workspace_id: Optional[str] = typer.Option(None),
198
+ status: Optional[str] = typer.Option(None),
199
+ ):
200
+ """获取迭代总数。"""
201
+ _out(_get(api_url, "getIterationsCount", dict(workspace_id=workspace_id, status=status)))
202
+
203
+
204
+ # ── Tasks ──────────────────────────────────────────────────────────────────────
205
+
206
+ @app.command("get-tasks")
207
+ def get_tasks(
208
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
209
+ workspace_id: Optional[str] = typer.Option(None),
210
+ iteration_id: Optional[str] = typer.Option(None),
211
+ story_id: Optional[str] = typer.Option(None, help="关联需求ID"),
212
+ owner: Optional[str] = typer.Option(None),
213
+ status: Optional[str] = typer.Option(None),
214
+ limit: int = typer.Option(30),
215
+ page: int = typer.Option(1),
216
+ fields: Optional[str] = typer.Option(None),
217
+ ):
218
+ """获取任务列表。"""
219
+ _out(_get(api_url, "getTasks", dict(
220
+ workspace_id=workspace_id, iteration_id=iteration_id, story_id=story_id,
221
+ owner=owner, status=status, limit=limit, page=page, fields=fields,
222
+ )))
223
+
224
+
225
+ # ── Measure ────────────────────────────────────────────────────────────────────
226
+
227
+ @app.command("get-life-times")
228
+ def get_life_times(
229
+ entity_id: str = typer.Argument(help="业务对象ID"),
230
+ entity_type: str = typer.Argument(help="对象类型:story、bug 或 task"),
231
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
232
+ workspace_id: Optional[str] = typer.Option(None),
233
+ limit: int = typer.Option(200),
234
+ page: int = typer.Option(1),
235
+ ):
236
+ """获取工作项状态流转时间(各阶段耗时数据)。"""
237
+ _out(_get(api_url, "getLifeTimes", dict(
238
+ entity_id=entity_id, entity_type=entity_type,
239
+ workspace_id=workspace_id, limit=limit, page=page,
240
+ )))
241
+
242
+
243
+ @app.command("get-workflow-status-map")
244
+ def get_workflow_status_map(
245
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
246
+ entity_type: str = typer.Option("story", help="对象类型:story、bug 或 task"),
247
+ workspace_id: Optional[str] = typer.Option(None),
248
+ ):
249
+ """获取工作流状态的中英文名称映射。"""
250
+ _out(_get(api_url, "getWorkflowStatusMap", dict(entity_type=entity_type, workspace_id=workspace_id)))
251
+
252
+
253
+ # ── Timesheets ─────────────────────────────────────────────────────────────────
254
+
255
+ @app.command("get-timesheets")
256
+ def get_timesheets(
257
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
258
+ workspace_id: Optional[str] = typer.Option(None),
259
+ entity_type: Optional[str] = typer.Option(None, help="story、bug 或 task"),
260
+ entity_id: Optional[str] = typer.Option(None),
261
+ owner: Optional[str] = typer.Option(None),
262
+ spent_date_from: Optional[str] = typer.Option(None, help="工时日期起始 YYYY-MM-DD"),
263
+ spent_date_to: Optional[str] = typer.Option(None),
264
+ limit: int = typer.Option(30),
265
+ page: int = typer.Option(1),
266
+ ):
267
+ """获取工时花费记录。"""
268
+ _out(_get(api_url, "getTimesheets", dict(
269
+ workspace_id=workspace_id, entity_type=entity_type, entity_id=entity_id,
270
+ owner=owner, spent_date_from=spent_date_from, spent_date_to=spent_date_to,
271
+ limit=limit, page=page,
272
+ )))
273
+
274
+
275
+ # ── Comments ───────────────────────────────────────────────────────────────────
276
+
277
+ @app.command("get-comments")
278
+ def get_comments(
279
+ entity_type: str = typer.Argument(help="story、bug 或 task"),
280
+ entity_id: str = typer.Argument(help="对象ID"),
281
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
282
+ workspace_id: Optional[str] = typer.Option(None),
283
+ limit: int = typer.Option(30),
284
+ page: int = typer.Option(1),
285
+ ):
286
+ """获取工作项评论。"""
287
+ _out(_get(api_url, "getComments", dict(
288
+ entity_type=entity_type, entity_id=entity_id,
289
+ workspace_id=workspace_id, limit=limit, page=page,
290
+ )))
291
+
292
+
293
+ # ── Workspace ──────────────────────────────────────────────────────────────────
294
+
295
+ @app.command("get-workspace-info")
296
+ def get_workspace_info(
297
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
298
+ workspace_id: Optional[str] = typer.Option(None),
299
+ ):
300
+ """获取项目基本信息。"""
301
+ _out(_get(api_url, "getWorkspaceInfo", dict(workspace_id=workspace_id)))
302
+
303
+
304
+ @app.command("get-workspace-members")
305
+ def get_workspace_members(
306
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
307
+ workspace_id: Optional[str] = typer.Option(None),
308
+ ):
309
+ """获取项目成员列表。"""
310
+ _out(_get(api_url, "getWorkspaceMembers", dict(workspace_id=workspace_id)))
311
+
312
+
313
+ # ── Write commands ─────────────────────────────────────────────────────────────
314
+
315
+ @app.command("create-story")
316
+ def create_story(
317
+ name: str = typer.Argument(help="需求标题"),
318
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
319
+ workspace_id: Optional[str] = typer.Option(None),
320
+ owner: Optional[str] = typer.Option(None),
321
+ status: Optional[str] = typer.Option(None),
322
+ iteration_id: Optional[str] = typer.Option(None),
323
+ priority: Optional[str] = typer.Option(None),
324
+ description: Optional[str] = typer.Option(None),
325
+ ):
326
+ """创建需求。"""
327
+ _out(_post(api_url, "createStory", dict(
328
+ name=name, workspace_id=workspace_id, owner=owner, status=status,
329
+ iteration_id=iteration_id, priority=priority, description=description,
330
+ )))
331
+
332
+
333
+ @app.command("update-story")
334
+ def update_story(
335
+ story_id: str = typer.Argument(help="需求ID"),
336
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
337
+ workspace_id: Optional[str] = typer.Option(None),
338
+ name: Optional[str] = typer.Option(None),
339
+ owner: Optional[str] = typer.Option(None),
340
+ status: Optional[str] = typer.Option(None),
341
+ iteration_id: Optional[str] = typer.Option(None),
342
+ ):
343
+ """更新需求。"""
344
+ _out(_put(api_url, "updateStory", dict(
345
+ id=story_id, workspace_id=workspace_id,
346
+ name=name, owner=owner, status=status, iteration_id=iteration_id,
347
+ )))
348
+
349
+
350
+ @app.command("create-bug")
351
+ def create_bug(
352
+ title: str = typer.Argument(help="缺陷标题"),
353
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
354
+ workspace_id: Optional[str] = typer.Option(None),
355
+ priority: Optional[str] = typer.Option(None),
356
+ severity: Optional[str] = typer.Option(None),
357
+ current_owner: Optional[str] = typer.Option(None),
358
+ de: Optional[str] = typer.Option(None, help="开发人员"),
359
+ te: Optional[str] = typer.Option(None, help="测试人员"),
360
+ iteration_id: Optional[str] = typer.Option(None),
361
+ description: Optional[str] = typer.Option(None),
362
+ ):
363
+ """创建缺陷。"""
364
+ _out(_post(api_url, "createBug", dict(
365
+ title=title, workspace_id=workspace_id, priority=priority,
366
+ severity=severity, current_owner=current_owner,
367
+ de=de, te=te, iteration_id=iteration_id, description=description,
368
+ )))
369
+
370
+
371
+ @app.command("update-bug")
372
+ def update_bug(
373
+ bug_id: str = typer.Argument(help="缺陷ID"),
374
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
375
+ workspace_id: Optional[str] = typer.Option(None),
376
+ title: Optional[str] = typer.Option(None),
377
+ status: Optional[str] = typer.Option(None),
378
+ current_owner: Optional[str] = typer.Option(None),
379
+ priority: Optional[str] = typer.Option(None),
380
+ severity: Optional[str] = typer.Option(None),
381
+ ):
382
+ """更新缺陷。"""
383
+ _out(_put(api_url, "updateBug", dict(
384
+ id=bug_id, workspace_id=workspace_id, title=title, status=status,
385
+ current_owner=current_owner, priority=priority, severity=severity,
386
+ )))
387
+
388
+
389
+ @app.command("create-task")
390
+ def create_task(
391
+ name: str = typer.Argument(help="任务名称"),
392
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
393
+ workspace_id: Optional[str] = typer.Option(None),
394
+ owner: Optional[str] = typer.Option(None),
395
+ story_id: Optional[str] = typer.Option(None),
396
+ iteration_id: Optional[str] = typer.Option(None),
397
+ description: Optional[str] = typer.Option(None),
398
+ ):
399
+ """创建任务。"""
400
+ _out(_post(api_url, "createTask", dict(
401
+ name=name, workspace_id=workspace_id, owner=owner,
402
+ story_id=story_id, iteration_id=iteration_id, description=description,
403
+ )))
404
+
405
+
406
+ @app.command("update-task")
407
+ def update_task(
408
+ task_id: str = typer.Argument(help="任务ID"),
409
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
410
+ workspace_id: Optional[str] = typer.Option(None),
411
+ name: Optional[str] = typer.Option(None),
412
+ status: Optional[str] = typer.Option(None),
413
+ owner: Optional[str] = typer.Option(None),
414
+ ):
415
+ """更新任务。"""
416
+ _out(_put(api_url, "updateTask", dict(
417
+ id=task_id, workspace_id=workspace_id,
418
+ name=name, status=status, owner=owner,
419
+ )))
420
+
421
+
422
+ @app.command("add-timesheet")
423
+ def add_timesheet(
424
+ entity_type: str = typer.Argument(help="story、bug 或 task"),
425
+ entity_id: str = typer.Argument(help="对象ID"),
426
+ spent_date: str = typer.Argument(help="工时日期 YYYY-MM-DD"),
427
+ timespent: float = typer.Argument(help="工时小时数"),
428
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
429
+ workspace_id: Optional[str] = typer.Option(None),
430
+ owner: Optional[str] = typer.Option(None),
431
+ memo: Optional[str] = typer.Option(None),
432
+ ):
433
+ """记录工时花费。"""
434
+ _out(_post(api_url, "addTimesheet", dict(
435
+ entity_type=entity_type, entity_id=entity_id,
436
+ spent_date=spent_date, timespent=timespent,
437
+ workspace_id=workspace_id, owner=owner, memo=memo,
438
+ )))
439
+
440
+
441
+ @app.command("add-comment")
442
+ def add_comment(
443
+ entity_type: str = typer.Argument(help="story、bug 或 task"),
444
+ entity_id: str = typer.Argument(help="对象ID"),
445
+ description: str = typer.Argument(help="评论内容"),
446
+ api_url: str = typer.Option(_DEFAULT_API_URL, envvar="TAPD_API_URL"),
447
+ workspace_id: Optional[str] = typer.Option(None),
448
+ ):
449
+ """添加评论。"""
450
+ _out(_post(api_url, "addComment", dict(
451
+ entity_type=entity_type, entity_id=entity_id,
452
+ description=description, workspace_id=workspace_id,
453
+ )))
454
+
455
+
456
+ if __name__ == "__main__":
457
+ app()