ktr-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kantree_cli/__init__.py +3 -0
- kantree_cli/cli.py +7922 -0
- kantree_cli/core/__init__.py +1 -0
- kantree_cli/core/client.py +230 -0
- kantree_cli/core/config.py +226 -0
- kantree_cli/core/context.py +141 -0
- kantree_cli/core/errors.py +32 -0
- kantree_cli/core/output.py +64 -0
- kantree_cli/core/response.py +86 -0
- kantree_cli/services/__init__.py +1 -0
- kantree_cli/services/cards.py +726 -0
- kantree_cli/services/importers.py +304 -0
- kantree_cli/services/kql.py +43 -0
- kantree_cli/services/resolver.py +236 -0
- kantree_cli/services/search.py +67 -0
- kantree_cli/services/views.py +124 -0
- kantree_cli/services/webhooks.py +120 -0
- kantree_cli/services/workspaces.py +1300 -0
- ktr_cli-0.1.0.dist-info/METADATA +173 -0
- ktr_cli-0.1.0.dist-info/RECORD +22 -0
- ktr_cli-0.1.0.dist-info/WHEEL +4 -0
- ktr_cli-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from kantree_cli.core.client import KantreeClient
|
|
5
|
+
from kantree_cli.core.errors import AmbiguityError, NotFoundError, ValidationError
|
|
6
|
+
from kantree_cli.core.response import (
|
|
7
|
+
extract_object,
|
|
8
|
+
extract_object_list,
|
|
9
|
+
next_page_header_value,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
JsonObject = dict[str, Any]
|
|
13
|
+
CARD_STATES = (
|
|
14
|
+
"undecided",
|
|
15
|
+
"accepted",
|
|
16
|
+
"waiting",
|
|
17
|
+
"in_progress",
|
|
18
|
+
"completed",
|
|
19
|
+
"closed",
|
|
20
|
+
"dropped",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class ResolvedField:
|
|
26
|
+
id: int
|
|
27
|
+
name: str
|
|
28
|
+
definition: JsonObject
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ResolvedGroup:
|
|
33
|
+
id: int
|
|
34
|
+
name: str
|
|
35
|
+
definition: JsonObject
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def list_cards(
|
|
39
|
+
client: KantreeClient,
|
|
40
|
+
workspace_id: int,
|
|
41
|
+
*,
|
|
42
|
+
filter_text: str | None = None,
|
|
43
|
+
archived: bool = False,
|
|
44
|
+
limit: int | None = None,
|
|
45
|
+
page: int | None = None,
|
|
46
|
+
per_page: int | None = None,
|
|
47
|
+
all_pages: bool = False,
|
|
48
|
+
) -> list[JsonObject]:
|
|
49
|
+
if limit is not None and limit < 1:
|
|
50
|
+
raise ValidationError("`--limit` expects a positive integer.")
|
|
51
|
+
|
|
52
|
+
params: dict[str, str] = {}
|
|
53
|
+
if filter_text is not None:
|
|
54
|
+
normalized_filter = filter_text.strip()
|
|
55
|
+
if not normalized_filter:
|
|
56
|
+
raise ValidationError("`--filter` expects a non-empty value.")
|
|
57
|
+
params["filter"] = normalized_filter
|
|
58
|
+
|
|
59
|
+
if archived:
|
|
60
|
+
params["with_archived"] = "true"
|
|
61
|
+
if all_pages:
|
|
62
|
+
if page is not None:
|
|
63
|
+
raise ValidationError("`--all` cannot be combined with `--page`.")
|
|
64
|
+
|
|
65
|
+
merged_cards: list[JsonObject] = []
|
|
66
|
+
next_page = 1
|
|
67
|
+
while True:
|
|
68
|
+
request_params = dict(params)
|
|
69
|
+
request_params["page"] = str(next_page)
|
|
70
|
+
if per_page is not None:
|
|
71
|
+
request_params["per_page"] = str(per_page)
|
|
72
|
+
|
|
73
|
+
response = client.request(
|
|
74
|
+
method="GET",
|
|
75
|
+
path=f"/projects/{workspace_id}/cards",
|
|
76
|
+
params=request_params,
|
|
77
|
+
)
|
|
78
|
+
page_cards = extract_object_list(
|
|
79
|
+
response.data,
|
|
80
|
+
context=f"`GET /projects/{workspace_id}/cards`",
|
|
81
|
+
preferred_keys=("cards", "data", "items", "results"),
|
|
82
|
+
)
|
|
83
|
+
merged_cards.extend(page_cards)
|
|
84
|
+
|
|
85
|
+
if limit is not None and len(merged_cards) >= limit:
|
|
86
|
+
return merged_cards[:limit]
|
|
87
|
+
|
|
88
|
+
parsed_next_page = next_page_header_value(response.headers)
|
|
89
|
+
if parsed_next_page is None:
|
|
90
|
+
break
|
|
91
|
+
next_page = parsed_next_page
|
|
92
|
+
|
|
93
|
+
return merged_cards
|
|
94
|
+
|
|
95
|
+
if page is not None:
|
|
96
|
+
params["page"] = str(page)
|
|
97
|
+
if per_page is not None:
|
|
98
|
+
params["per_page"] = str(per_page)
|
|
99
|
+
|
|
100
|
+
response = client.request(
|
|
101
|
+
method="GET",
|
|
102
|
+
path=f"/projects/{workspace_id}/cards",
|
|
103
|
+
params=params or None,
|
|
104
|
+
)
|
|
105
|
+
cards = extract_object_list(
|
|
106
|
+
response.data,
|
|
107
|
+
context=f"`GET /projects/{workspace_id}/cards`",
|
|
108
|
+
preferred_keys=("cards", "data", "items", "results"),
|
|
109
|
+
)
|
|
110
|
+
if limit is not None:
|
|
111
|
+
return cards[:limit]
|
|
112
|
+
return cards
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_card(client: KantreeClient, card_id: int) -> JsonObject:
|
|
116
|
+
payload = client.get_json(f"/cards/{card_id}")
|
|
117
|
+
return extract_object(
|
|
118
|
+
payload,
|
|
119
|
+
context=f"`GET /cards/{card_id}`",
|
|
120
|
+
preferred_keys=("card", "data", "item", "result"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_workspace_card_by_ref(
|
|
125
|
+
client: KantreeClient,
|
|
126
|
+
*,
|
|
127
|
+
workspace_id: int,
|
|
128
|
+
card_ref: int,
|
|
129
|
+
) -> JsonObject:
|
|
130
|
+
payload = client.get_json(f"/projects/{workspace_id}/cards/{card_ref}")
|
|
131
|
+
return extract_object(
|
|
132
|
+
payload,
|
|
133
|
+
context=f"`GET /projects/{workspace_id}/cards/{card_ref}`",
|
|
134
|
+
preferred_keys=("card", "data", "item", "result"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def list_card_children(
|
|
139
|
+
client: KantreeClient, card_id: int, *, archived: bool = False
|
|
140
|
+
) -> list[JsonObject]:
|
|
141
|
+
endpoint = f"/cards/{card_id}/children/archived" if archived else f"/cards/{card_id}/children"
|
|
142
|
+
payload = client.get_json(endpoint)
|
|
143
|
+
return extract_object_list(
|
|
144
|
+
payload,
|
|
145
|
+
context=f"`GET {endpoint}`",
|
|
146
|
+
preferred_keys=("cards", "children", "data", "items", "results"),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def list_card_stream(client: KantreeClient, card_id: int) -> list[JsonObject]:
|
|
151
|
+
payload = client.get_json(f"/cards/{card_id}/stream")
|
|
152
|
+
return extract_object_list(
|
|
153
|
+
payload,
|
|
154
|
+
context=f"`GET /cards/{card_id}/stream`",
|
|
155
|
+
preferred_keys=("stream", "events", "data", "items", "results"),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def list_card_reminders(client: KantreeClient, card_id: int) -> list[JsonObject]:
|
|
160
|
+
payload = client.get_json(f"/cards/{card_id}/reminders")
|
|
161
|
+
return extract_object_list(
|
|
162
|
+
payload,
|
|
163
|
+
context=f"`GET /cards/{card_id}/reminders`",
|
|
164
|
+
preferred_keys=("reminders", "data", "items", "results"),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_card_reminder(
|
|
169
|
+
client: KantreeClient,
|
|
170
|
+
*,
|
|
171
|
+
card_id: int,
|
|
172
|
+
targets: list[str],
|
|
173
|
+
reminding_date: str,
|
|
174
|
+
) -> JsonObject:
|
|
175
|
+
response = client.request(
|
|
176
|
+
method="POST",
|
|
177
|
+
path=f"/cards/{card_id}/reminders",
|
|
178
|
+
json_body={"targets": targets, "reminding_date": reminding_date},
|
|
179
|
+
)
|
|
180
|
+
return extract_object(
|
|
181
|
+
response.data,
|
|
182
|
+
context=f"`POST /cards/{card_id}/reminders`",
|
|
183
|
+
preferred_keys=("reminder", "data", "item", "result"),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def delete_card_reminder(
|
|
188
|
+
client: KantreeClient,
|
|
189
|
+
*,
|
|
190
|
+
card_id: int,
|
|
191
|
+
reminder_id: int,
|
|
192
|
+
) -> Any:
|
|
193
|
+
response = client.request(
|
|
194
|
+
method="DELETE",
|
|
195
|
+
path=f"/cards/{card_id}/reminders/{reminder_id}",
|
|
196
|
+
)
|
|
197
|
+
return response.data
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def comment_card(
|
|
201
|
+
client: KantreeClient,
|
|
202
|
+
*,
|
|
203
|
+
card_id: int,
|
|
204
|
+
message: str,
|
|
205
|
+
) -> Any:
|
|
206
|
+
response = client.request(
|
|
207
|
+
method="POST",
|
|
208
|
+
path=f"/cards/{card_id}/comments",
|
|
209
|
+
json_body={"message": message},
|
|
210
|
+
)
|
|
211
|
+
return response.data
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def archive_card(client: KantreeClient, *, card_id: int) -> Any:
|
|
215
|
+
response = client.request(
|
|
216
|
+
method="POST",
|
|
217
|
+
path=f"/cards/{card_id}/archive",
|
|
218
|
+
)
|
|
219
|
+
return response.data
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def restore_card(client: KantreeClient, *, card_id: int) -> Any:
|
|
223
|
+
response = client.request(
|
|
224
|
+
method="DELETE",
|
|
225
|
+
path=f"/cards/{card_id}/archive",
|
|
226
|
+
)
|
|
227
|
+
return response.data
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def delete_card(client: KantreeClient, *, card_id: int) -> Any:
|
|
231
|
+
response = client.request(
|
|
232
|
+
method="DELETE",
|
|
233
|
+
path=f"/cards/{card_id}",
|
|
234
|
+
)
|
|
235
|
+
return response.data
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def create_card(
|
|
239
|
+
client: KantreeClient,
|
|
240
|
+
*,
|
|
241
|
+
parent_id: int,
|
|
242
|
+
title: str,
|
|
243
|
+
group_id: int | None = None,
|
|
244
|
+
model_id: int | None = None,
|
|
245
|
+
attributes: dict[int, object] | None = None,
|
|
246
|
+
) -> JsonObject:
|
|
247
|
+
json_body: dict[str, object] = {"title": title}
|
|
248
|
+
if group_id is not None:
|
|
249
|
+
json_body["group_id"] = group_id
|
|
250
|
+
if model_id is not None:
|
|
251
|
+
json_body["model_id"] = model_id
|
|
252
|
+
if attributes is not None:
|
|
253
|
+
json_body["attributes"] = {str(field_id): value for field_id, value in attributes.items()}
|
|
254
|
+
response = client.request(
|
|
255
|
+
method="POST",
|
|
256
|
+
path=f"/cards/{parent_id}/children",
|
|
257
|
+
json_body=json_body,
|
|
258
|
+
)
|
|
259
|
+
return extract_object(
|
|
260
|
+
response.data,
|
|
261
|
+
context=f"`POST /cards/{parent_id}/children`",
|
|
262
|
+
preferred_keys=("card", "data", "item", "result"),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def update_card(
|
|
267
|
+
client: KantreeClient,
|
|
268
|
+
card_id: int,
|
|
269
|
+
*,
|
|
270
|
+
title: str | None = None,
|
|
271
|
+
state: str | None = None,
|
|
272
|
+
default_child_card_model_id: int | None = None,
|
|
273
|
+
image_cover_url: str | None = None,
|
|
274
|
+
) -> Any:
|
|
275
|
+
payload: dict[str, object] = {}
|
|
276
|
+
if title is not None:
|
|
277
|
+
payload["title"] = title
|
|
278
|
+
if state is not None:
|
|
279
|
+
payload["state"] = state
|
|
280
|
+
if default_child_card_model_id is not None:
|
|
281
|
+
payload["default_child_card_model_id"] = default_child_card_model_id
|
|
282
|
+
if image_cover_url is not None:
|
|
283
|
+
payload["image_cover_url"] = image_cover_url
|
|
284
|
+
if not payload:
|
|
285
|
+
raise ValidationError("Card update requires at least one field.")
|
|
286
|
+
response = client.request(
|
|
287
|
+
method="PUT",
|
|
288
|
+
path=f"/cards/{card_id}",
|
|
289
|
+
json_body=payload,
|
|
290
|
+
)
|
|
291
|
+
return response.data
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def set_card_attributes(
|
|
295
|
+
client: KantreeClient,
|
|
296
|
+
card_id: int,
|
|
297
|
+
*,
|
|
298
|
+
attributes: dict[int, object],
|
|
299
|
+
) -> Any:
|
|
300
|
+
if not attributes:
|
|
301
|
+
raise ValidationError("Card attribute update requires at least one field.")
|
|
302
|
+
encoded = {str(field_id): value for field_id, value in attributes.items()}
|
|
303
|
+
response = client.request(
|
|
304
|
+
method="POST",
|
|
305
|
+
path=f"/cards/{card_id}/attributes",
|
|
306
|
+
json_body={"attributes": encoded},
|
|
307
|
+
)
|
|
308
|
+
return response.data
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def append_card_attribute(
|
|
312
|
+
client: KantreeClient,
|
|
313
|
+
*,
|
|
314
|
+
card_id: int,
|
|
315
|
+
field_id: int,
|
|
316
|
+
value: object,
|
|
317
|
+
) -> Any:
|
|
318
|
+
response = client.request(
|
|
319
|
+
method="POST",
|
|
320
|
+
path=f"/cards/{card_id}/attributes/{field_id}/append",
|
|
321
|
+
json_body={"value": value},
|
|
322
|
+
)
|
|
323
|
+
return response.data
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def pop_card_attribute(
|
|
327
|
+
client: KantreeClient,
|
|
328
|
+
*,
|
|
329
|
+
card_id: int,
|
|
330
|
+
field_id: int,
|
|
331
|
+
value: object,
|
|
332
|
+
) -> Any:
|
|
333
|
+
response = client.request(
|
|
334
|
+
method="POST",
|
|
335
|
+
path=f"/cards/{card_id}/attributes/{field_id}/pop",
|
|
336
|
+
json_body={"value": value},
|
|
337
|
+
)
|
|
338
|
+
return response.data
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def clear_card_attribute(
|
|
342
|
+
client: KantreeClient,
|
|
343
|
+
*,
|
|
344
|
+
card_id: int,
|
|
345
|
+
field_id: int,
|
|
346
|
+
) -> Any:
|
|
347
|
+
response = client.request(
|
|
348
|
+
method="DELETE",
|
|
349
|
+
path=f"/cards/{card_id}/attributes/{field_id}",
|
|
350
|
+
)
|
|
351
|
+
return response.data
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def set_card_model(
|
|
355
|
+
client: KantreeClient,
|
|
356
|
+
*,
|
|
357
|
+
card_id: int,
|
|
358
|
+
model_id: int | None,
|
|
359
|
+
) -> Any:
|
|
360
|
+
response = client.request(
|
|
361
|
+
method="POST",
|
|
362
|
+
path=f"/cards/{card_id}/model",
|
|
363
|
+
json_body={"model_id": model_id},
|
|
364
|
+
)
|
|
365
|
+
return response.data
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def relocate_card(
|
|
369
|
+
client: KantreeClient,
|
|
370
|
+
*,
|
|
371
|
+
card_id: int,
|
|
372
|
+
group_id: int,
|
|
373
|
+
parent_id: int | None = None,
|
|
374
|
+
position: int | None = None,
|
|
375
|
+
) -> Any:
|
|
376
|
+
payload: dict[str, object] = {"group_id": group_id}
|
|
377
|
+
if parent_id is not None:
|
|
378
|
+
payload["parent_id"] = parent_id
|
|
379
|
+
if position is not None:
|
|
380
|
+
payload["position"] = position
|
|
381
|
+
response = client.request(
|
|
382
|
+
method="POST",
|
|
383
|
+
path=f"/cards/{card_id}/relocate",
|
|
384
|
+
json_body=payload,
|
|
385
|
+
)
|
|
386
|
+
return response.data
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def add_card_to_group(
|
|
390
|
+
client: KantreeClient,
|
|
391
|
+
*,
|
|
392
|
+
card_id: int,
|
|
393
|
+
group_id: int,
|
|
394
|
+
) -> Any:
|
|
395
|
+
response = client.request(
|
|
396
|
+
method="POST",
|
|
397
|
+
path=f"/cards/{card_id}/add-to-group",
|
|
398
|
+
json_body={"group_id": group_id},
|
|
399
|
+
)
|
|
400
|
+
return response.data
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def remove_card_from_group(
|
|
404
|
+
client: KantreeClient,
|
|
405
|
+
*,
|
|
406
|
+
card_id: int,
|
|
407
|
+
group_id: int,
|
|
408
|
+
) -> Any:
|
|
409
|
+
response = client.request(
|
|
410
|
+
method="POST",
|
|
411
|
+
path=f"/cards/{card_id}/remove-from-group",
|
|
412
|
+
json_body={"group_id": group_id},
|
|
413
|
+
)
|
|
414
|
+
return response.data
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def clear_card_group_type(
|
|
418
|
+
client: KantreeClient,
|
|
419
|
+
*,
|
|
420
|
+
card_id: int,
|
|
421
|
+
type_id: int,
|
|
422
|
+
) -> Any:
|
|
423
|
+
response = client.request(
|
|
424
|
+
method="POST",
|
|
425
|
+
path=f"/cards/{card_id}/remove-from-group-type",
|
|
426
|
+
json_body={"type_id": type_id},
|
|
427
|
+
)
|
|
428
|
+
return response.data
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def copy_card(
|
|
432
|
+
client: KantreeClient,
|
|
433
|
+
*,
|
|
434
|
+
card_id: int,
|
|
435
|
+
parent_id: int | None = None,
|
|
436
|
+
group_id: int | None = None,
|
|
437
|
+
position: int | None = None,
|
|
438
|
+
) -> Any:
|
|
439
|
+
payload: dict[str, object] = {}
|
|
440
|
+
if parent_id is not None:
|
|
441
|
+
payload["parent_id"] = parent_id
|
|
442
|
+
if group_id is not None:
|
|
443
|
+
payload["group_id"] = group_id
|
|
444
|
+
if position is not None:
|
|
445
|
+
payload["position"] = position
|
|
446
|
+
response = client.request(
|
|
447
|
+
method="POST", path=f"/cards/{card_id}/copy", json_body=payload or None
|
|
448
|
+
)
|
|
449
|
+
return response.data
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def list_card_states(client: KantreeClient, *, card_id: int) -> list[JsonObject]:
|
|
453
|
+
payload = client.get_json(f"/cards/{card_id}/states")
|
|
454
|
+
return extract_object_list(
|
|
455
|
+
payload,
|
|
456
|
+
context=f"`GET /cards/{card_id}/states`",
|
|
457
|
+
preferred_keys=("states", "data", "items", "results"),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def get_card_hierarchy(client: KantreeClient, *, card_id: int) -> Any:
|
|
462
|
+
return client.get_json(f"/cards/{card_id}/hierarchy")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def list_card_descendants(
|
|
466
|
+
client: KantreeClient,
|
|
467
|
+
*,
|
|
468
|
+
card_id: int,
|
|
469
|
+
reversed_order: bool = False,
|
|
470
|
+
) -> list[JsonObject]:
|
|
471
|
+
params: dict[str, str] = {}
|
|
472
|
+
if reversed_order:
|
|
473
|
+
params["reverse"] = "true"
|
|
474
|
+
response = client.request(
|
|
475
|
+
method="GET",
|
|
476
|
+
path=f"/cards/{card_id}/descendants",
|
|
477
|
+
params=params or None,
|
|
478
|
+
)
|
|
479
|
+
return extract_object_list(
|
|
480
|
+
response.data,
|
|
481
|
+
context=f"`GET /cards/{card_id}/descendants`",
|
|
482
|
+
preferred_keys=("cards", "descendants", "data", "items", "results"),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def batch_operation(
|
|
487
|
+
client: KantreeClient,
|
|
488
|
+
*,
|
|
489
|
+
workspace_id: int,
|
|
490
|
+
operation: str,
|
|
491
|
+
params: dict[str, object],
|
|
492
|
+
card_ids: list[int] | None = None,
|
|
493
|
+
filter_text: str | None = None,
|
|
494
|
+
parent_id: int | None = None,
|
|
495
|
+
group_id: int | None = None,
|
|
496
|
+
with_archived: bool = False,
|
|
497
|
+
) -> Any:
|
|
498
|
+
"""Execute a batch operation server-side."""
|
|
499
|
+
payload: dict[str, object] = {"operation": operation, "params": params}
|
|
500
|
+
if card_ids is not None:
|
|
501
|
+
payload["card_ids"] = card_ids
|
|
502
|
+
if filter_text is not None:
|
|
503
|
+
payload["filter"] = filter_text
|
|
504
|
+
if parent_id is not None:
|
|
505
|
+
payload["parent_id"] = parent_id
|
|
506
|
+
if group_id is not None:
|
|
507
|
+
payload["group_id"] = group_id
|
|
508
|
+
if with_archived:
|
|
509
|
+
payload["with_archived"] = True
|
|
510
|
+
response = client.request(
|
|
511
|
+
method="POST",
|
|
512
|
+
path=f"/projects/{workspace_id}/batch",
|
|
513
|
+
json_body=payload,
|
|
514
|
+
)
|
|
515
|
+
return response.data
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_async_job(client: KantreeClient, job_id: int | str) -> Any:
|
|
519
|
+
response = client.request(method="GET", path=f"/async/{job_id}")
|
|
520
|
+
return response.data
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def batch_dates(
|
|
524
|
+
client: KantreeClient,
|
|
525
|
+
*,
|
|
526
|
+
workspace_id: int,
|
|
527
|
+
cards: dict[str, list[dict[str, Any]]],
|
|
528
|
+
) -> Any:
|
|
529
|
+
response = client.request(
|
|
530
|
+
method="POST",
|
|
531
|
+
path=f"/projects/{workspace_id}/batch/dates",
|
|
532
|
+
json_body={"cards": cards},
|
|
533
|
+
)
|
|
534
|
+
return response.data
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def favorite_card(client: KantreeClient, *, card_id: int) -> Any:
|
|
538
|
+
response = client.request(method="POST", path=f"/cards/{card_id}/favorite")
|
|
539
|
+
return response.data
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def unfavorite_card(client: KantreeClient, *, card_id: int) -> Any:
|
|
543
|
+
response = client.request(method="DELETE", path=f"/cards/{card_id}/favorite")
|
|
544
|
+
return response.data
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def resolve_field_selector(fields: list[JsonObject], selector: str) -> ResolvedField:
|
|
548
|
+
normalized_selector = selector.strip()
|
|
549
|
+
if not normalized_selector:
|
|
550
|
+
raise ValidationError("Field selector must not be empty.")
|
|
551
|
+
|
|
552
|
+
parsed_id = _parse_int(normalized_selector)
|
|
553
|
+
if parsed_id is not None:
|
|
554
|
+
matches = [field for field in fields if field_id(field, context="Field list") == parsed_id]
|
|
555
|
+
if not matches:
|
|
556
|
+
raise NotFoundError(f"Field id `{parsed_id}` was not found in this workspace.")
|
|
557
|
+
return _single_field_match(matches, selector=f"id `{parsed_id}`")
|
|
558
|
+
|
|
559
|
+
matches = [
|
|
560
|
+
field for field in fields if field_name(field, context="Field list") == normalized_selector
|
|
561
|
+
]
|
|
562
|
+
if not matches:
|
|
563
|
+
raise NotFoundError(f"Field `{normalized_selector}` was not found in this workspace.")
|
|
564
|
+
return _single_field_match(matches, selector=f"`{normalized_selector}`")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def resolve_group_selector(groups: list[JsonObject], selector: str) -> ResolvedGroup:
|
|
568
|
+
normalized_selector = selector.strip()
|
|
569
|
+
if not normalized_selector:
|
|
570
|
+
raise ValidationError("Group selector must not be empty.")
|
|
571
|
+
|
|
572
|
+
parsed_id = _parse_int(normalized_selector)
|
|
573
|
+
if parsed_id is not None:
|
|
574
|
+
if parsed_id < 1:
|
|
575
|
+
raise ValidationError(
|
|
576
|
+
f"Group selector expects a positive integer id, got `{normalized_selector}`."
|
|
577
|
+
)
|
|
578
|
+
return ResolvedGroup(
|
|
579
|
+
id=parsed_id,
|
|
580
|
+
name=normalized_selector,
|
|
581
|
+
definition={"id": parsed_id},
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
matches = [
|
|
585
|
+
group for group in groups if group_name(group, context="Group list") == normalized_selector
|
|
586
|
+
]
|
|
587
|
+
if not matches:
|
|
588
|
+
raise NotFoundError(f"Group `{normalized_selector}` was not found under this parent.")
|
|
589
|
+
if len(matches) > 1:
|
|
590
|
+
ids = ", ".join(str(group_id(match, context="Group lookup")) for match in matches)
|
|
591
|
+
raise AmbiguityError(
|
|
592
|
+
f"Group `{normalized_selector}` is ambiguous under this parent "
|
|
593
|
+
f"({len(matches)} matches: {ids}). Use a group id."
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
match = matches[0]
|
|
597
|
+
return ResolvedGroup(
|
|
598
|
+
id=group_id(match, context="Group lookup"),
|
|
599
|
+
name=group_name(match, context="Group lookup"),
|
|
600
|
+
definition=match,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def field_id(field: JsonObject, *, context: str) -> int:
|
|
605
|
+
parsed = _parse_int(field.get("id"))
|
|
606
|
+
if parsed is None:
|
|
607
|
+
raise ValidationError(f"{context} does not contain a usable integer `id`.")
|
|
608
|
+
return parsed
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def field_name(field: JsonObject, *, context: str) -> str:
|
|
612
|
+
for key in ("name", "title"):
|
|
613
|
+
value = field.get(key)
|
|
614
|
+
if isinstance(value, str):
|
|
615
|
+
normalized = value.strip()
|
|
616
|
+
if normalized:
|
|
617
|
+
return normalized
|
|
618
|
+
raise ValidationError(f"{context} does not contain a usable field name.")
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def field_is_group_type(field: JsonObject) -> bool:
|
|
622
|
+
for raw_token in _field_type_tokens(field):
|
|
623
|
+
normalized = raw_token.strip().lower().replace("-", "_").replace(" ", "_")
|
|
624
|
+
if normalized == "group_type":
|
|
625
|
+
return True
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def group_id(group: JsonObject, *, context: str) -> int:
|
|
630
|
+
parsed = _parse_int(group.get("id"))
|
|
631
|
+
if parsed is None:
|
|
632
|
+
raise ValidationError(f"{context} does not contain a usable integer `id`.")
|
|
633
|
+
return parsed
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def group_name(group: JsonObject, *, context: str) -> str:
|
|
637
|
+
for key in ("title", "name"):
|
|
638
|
+
value = group.get(key)
|
|
639
|
+
if isinstance(value, str):
|
|
640
|
+
normalized = value.strip()
|
|
641
|
+
if normalized:
|
|
642
|
+
return normalized
|
|
643
|
+
raise ValidationError(f"{context} does not contain a usable group name.")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def card_parent_id(card: JsonObject, *, context: str) -> int:
|
|
647
|
+
parsed_parent = _parse_int(card.get("parent_id"))
|
|
648
|
+
if parsed_parent is not None:
|
|
649
|
+
return parsed_parent
|
|
650
|
+
|
|
651
|
+
parent = card.get("parent")
|
|
652
|
+
if isinstance(parent, dict):
|
|
653
|
+
parsed_parent = _parse_int(parent.get("id"))
|
|
654
|
+
if parsed_parent is not None:
|
|
655
|
+
return parsed_parent
|
|
656
|
+
|
|
657
|
+
raise ValidationError(f"{context} does not contain a usable `parent_id`.")
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def card_workspace_id(card: JsonObject, *, context: str) -> int:
|
|
661
|
+
for key in ("project_id", "workspace_id"):
|
|
662
|
+
parsed = _parse_int(card.get(key))
|
|
663
|
+
if parsed is not None:
|
|
664
|
+
return parsed
|
|
665
|
+
|
|
666
|
+
for key in ("project", "workspace"):
|
|
667
|
+
nested = card.get(key)
|
|
668
|
+
if isinstance(nested, dict):
|
|
669
|
+
parsed = _parse_int(nested.get("id"))
|
|
670
|
+
if parsed is not None:
|
|
671
|
+
return parsed
|
|
672
|
+
|
|
673
|
+
raise ValidationError(f"{context} does not contain a usable workspace id.")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def card_id(card: JsonObject, *, context: str) -> int:
|
|
677
|
+
parsed = _parse_int(card.get("id"))
|
|
678
|
+
if parsed is None:
|
|
679
|
+
raise ValidationError(f"{context} does not contain a usable integer `id`.")
|
|
680
|
+
return parsed
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _single_field_match(matches: list[JsonObject], *, selector: str) -> ResolvedField:
|
|
684
|
+
if len(matches) > 1:
|
|
685
|
+
ids = ", ".join(str(field_id(match, context="Field lookup")) for match in matches)
|
|
686
|
+
raise AmbiguityError(
|
|
687
|
+
f"Field {selector} is ambiguous in this workspace "
|
|
688
|
+
f"({len(matches)} matches: {ids}). Use a field id."
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
match = matches[0]
|
|
692
|
+
return ResolvedField(
|
|
693
|
+
id=field_id(match, context="Field lookup"),
|
|
694
|
+
name=field_name(match, context="Field lookup"),
|
|
695
|
+
definition=match,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _field_type_tokens(field: JsonObject) -> list[str]:
|
|
700
|
+
tokens: list[str] = []
|
|
701
|
+
for key in ("type", "attribute_type", "field_type", "kind", "attribute_type_name", "type_name"):
|
|
702
|
+
value = field.get(key)
|
|
703
|
+
if isinstance(value, str):
|
|
704
|
+
tokens.append(value)
|
|
705
|
+
elif isinstance(value, dict):
|
|
706
|
+
for nested_key in ("name", "slug", "key", "code", "type"):
|
|
707
|
+
nested_value = value.get(nested_key)
|
|
708
|
+
if isinstance(nested_value, str):
|
|
709
|
+
tokens.append(nested_value)
|
|
710
|
+
return tokens
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _parse_int(value: Any) -> int | None:
|
|
714
|
+
if isinstance(value, bool):
|
|
715
|
+
return None
|
|
716
|
+
if isinstance(value, int):
|
|
717
|
+
return value
|
|
718
|
+
if isinstance(value, str):
|
|
719
|
+
stripped = value.strip()
|
|
720
|
+
if not stripped:
|
|
721
|
+
return None
|
|
722
|
+
try:
|
|
723
|
+
return int(stripped)
|
|
724
|
+
except ValueError:
|
|
725
|
+
return None
|
|
726
|
+
return None
|