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,1300 @@
|
|
|
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 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
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class OrganizationContext:
|
|
17
|
+
id: int
|
|
18
|
+
title: str | None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_organizations(client: KantreeClient) -> list[JsonObject]:
|
|
22
|
+
payload = client.get_json("/organizations")
|
|
23
|
+
return extract_object_list(
|
|
24
|
+
payload,
|
|
25
|
+
context="`GET /organizations`",
|
|
26
|
+
preferred_keys=("organizations", "data", "items", "results"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_organization(client: KantreeClient, organization_id: int) -> JsonObject:
|
|
31
|
+
response = client.get_json(f"/organizations/{organization_id}/")
|
|
32
|
+
return extract_object(
|
|
33
|
+
response,
|
|
34
|
+
context=f"`GET /organizations/{organization_id}/`",
|
|
35
|
+
preferred_keys=("organization", "data", "item", "result"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def delete_organization(client: KantreeClient, organization_id: int) -> Any:
|
|
40
|
+
response = client.request(
|
|
41
|
+
method="DELETE",
|
|
42
|
+
path=f"/organizations/{organization_id}/",
|
|
43
|
+
)
|
|
44
|
+
return response.data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_organization(client: KantreeClient, *, title: str) -> JsonObject:
|
|
48
|
+
response = client.request(
|
|
49
|
+
method="POST",
|
|
50
|
+
path="/organizations/",
|
|
51
|
+
json_body={"name": title},
|
|
52
|
+
)
|
|
53
|
+
return extract_object(
|
|
54
|
+
response.data,
|
|
55
|
+
context="`POST /organizations/`",
|
|
56
|
+
preferred_keys=("organization", "data", "item", "result"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def list_teams_for_org(client: KantreeClient, organization_id: int) -> list[JsonObject]:
|
|
61
|
+
payload = client.get_json(f"/organizations/{organization_id}/teams")
|
|
62
|
+
return extract_object_list(
|
|
63
|
+
payload,
|
|
64
|
+
context=f"`GET /organizations/{organization_id}/teams`",
|
|
65
|
+
preferred_keys=("teams", "data", "items", "results"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_team(client: KantreeClient, *, organization_id: int, name: str) -> JsonObject:
|
|
70
|
+
response = client.request(
|
|
71
|
+
method="POST",
|
|
72
|
+
path=f"/organizations/{organization_id}/teams/",
|
|
73
|
+
json_body={"name": name},
|
|
74
|
+
)
|
|
75
|
+
return extract_object(
|
|
76
|
+
response.data,
|
|
77
|
+
context=f"`POST /organizations/{organization_id}/teams/`",
|
|
78
|
+
preferred_keys=("team", "data", "item", "result"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_team(
|
|
83
|
+
client: KantreeClient,
|
|
84
|
+
*,
|
|
85
|
+
organization_id: int,
|
|
86
|
+
team_id: int,
|
|
87
|
+
with_members: bool = False,
|
|
88
|
+
) -> JsonObject:
|
|
89
|
+
params: dict[str, str] = {}
|
|
90
|
+
if with_members:
|
|
91
|
+
params["with_members"] = "true"
|
|
92
|
+
|
|
93
|
+
response = client.request(
|
|
94
|
+
method="GET",
|
|
95
|
+
path=f"/organizations/{organization_id}/teams/{team_id}",
|
|
96
|
+
params=params or None,
|
|
97
|
+
)
|
|
98
|
+
return extract_object(
|
|
99
|
+
response.data,
|
|
100
|
+
context=f"`GET /organizations/{organization_id}/teams/{team_id}`",
|
|
101
|
+
preferred_keys=("team", "data", "item", "result"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def delete_team(
|
|
106
|
+
client: KantreeClient,
|
|
107
|
+
*,
|
|
108
|
+
organization_id: int,
|
|
109
|
+
team_id: int,
|
|
110
|
+
) -> Any:
|
|
111
|
+
response = client.request(
|
|
112
|
+
method="DELETE",
|
|
113
|
+
path=f"/organizations/{organization_id}/teams/{team_id}",
|
|
114
|
+
)
|
|
115
|
+
return response.data
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def list_workspaces_for_org(client: KantreeClient, organization_id: int) -> list[JsonObject]:
|
|
119
|
+
payload = client.get_json(f"/organizations/{organization_id}/projects")
|
|
120
|
+
return extract_object_list(
|
|
121
|
+
payload,
|
|
122
|
+
context=f"`GET /organizations/{organization_id}/projects`",
|
|
123
|
+
preferred_keys=("projects", "data", "items", "results"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def list_team_workspaces(
|
|
128
|
+
client: KantreeClient,
|
|
129
|
+
*,
|
|
130
|
+
organization_id: int,
|
|
131
|
+
team_id: int,
|
|
132
|
+
) -> list[JsonObject]:
|
|
133
|
+
payload = client.get_json(f"/organizations/{organization_id}/teams/{team_id}/projects")
|
|
134
|
+
return extract_object_list(
|
|
135
|
+
payload,
|
|
136
|
+
context=f"`GET /organizations/{organization_id}/teams/{team_id}/projects`",
|
|
137
|
+
preferred_keys=("projects", "data", "items", "results"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def list_workspaces_for_org_with_teams(
|
|
142
|
+
client: KantreeClient,
|
|
143
|
+
organization_id: int,
|
|
144
|
+
) -> list[JsonObject]:
|
|
145
|
+
workspaces = [
|
|
146
|
+
_workspace_with_context(workspace, organization_id=organization_id, team=None)
|
|
147
|
+
for workspace in list_workspaces_for_org(client, organization_id)
|
|
148
|
+
]
|
|
149
|
+
seen_ids = {
|
|
150
|
+
workspace_id
|
|
151
|
+
for workspace_id in (_parse_positive_int(workspace.get("id")) for workspace in workspaces)
|
|
152
|
+
if workspace_id is not None
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for team in list_teams_for_org(client, organization_id):
|
|
156
|
+
team_id = _parse_positive_int(team.get("id"))
|
|
157
|
+
if team_id is None:
|
|
158
|
+
continue
|
|
159
|
+
for workspace in list_team_workspaces(
|
|
160
|
+
client,
|
|
161
|
+
organization_id=organization_id,
|
|
162
|
+
team_id=team_id,
|
|
163
|
+
):
|
|
164
|
+
workspace_id = _parse_positive_int(workspace.get("id"))
|
|
165
|
+
if workspace_id is not None and workspace_id in seen_ids:
|
|
166
|
+
continue
|
|
167
|
+
if workspace_id is not None:
|
|
168
|
+
seen_ids.add(workspace_id)
|
|
169
|
+
workspaces.append(
|
|
170
|
+
_workspace_with_context(workspace, organization_id=organization_id, team=team)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return workspaces
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def list_workspaces(
|
|
177
|
+
client: KantreeClient,
|
|
178
|
+
organization_id: int | None = None,
|
|
179
|
+
*,
|
|
180
|
+
include_team_projects: bool = False,
|
|
181
|
+
) -> list[JsonObject]:
|
|
182
|
+
if organization_id is not None:
|
|
183
|
+
if include_team_projects:
|
|
184
|
+
return list_workspaces_for_org_with_teams(client, organization_id)
|
|
185
|
+
return list_workspaces_for_org(client, organization_id)
|
|
186
|
+
|
|
187
|
+
organizations = list_organizations(client)
|
|
188
|
+
workspaces: list[JsonObject] = []
|
|
189
|
+
for org in organizations:
|
|
190
|
+
org_context = _organization_context(org)
|
|
191
|
+
org_workspaces = (
|
|
192
|
+
list_workspaces_for_org_with_teams(client, org_context.id)
|
|
193
|
+
if include_team_projects
|
|
194
|
+
else list_workspaces_for_org(client, org_context.id)
|
|
195
|
+
)
|
|
196
|
+
for workspace in org_workspaces:
|
|
197
|
+
workspaces.append(
|
|
198
|
+
_workspace_with_context(
|
|
199
|
+
workspace,
|
|
200
|
+
organization_id=org_context.id,
|
|
201
|
+
organization_title=org_context.title,
|
|
202
|
+
team=None,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return workspaces
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_workspace(client: KantreeClient, workspace_id: int) -> JsonObject:
|
|
209
|
+
payload = client.get_json(f"/projects/{workspace_id}")
|
|
210
|
+
return extract_object(
|
|
211
|
+
payload,
|
|
212
|
+
context=f"`GET /projects/{workspace_id}`",
|
|
213
|
+
preferred_keys=("project", "data", "item", "result"),
|
|
214
|
+
require_id_field=False,
|
|
215
|
+
object_check=_looks_like_workspace,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def list_workspace_reminders(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
220
|
+
payload = client.get_json(f"/projects/{workspace_id}/reminders")
|
|
221
|
+
return extract_object_list(
|
|
222
|
+
payload,
|
|
223
|
+
context=f"`GET /projects/{workspace_id}/reminders`",
|
|
224
|
+
preferred_keys=("reminders", "data", "items", "results"),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def list_workspace_export_templates(
|
|
229
|
+
client: KantreeClient,
|
|
230
|
+
workspace_id: int,
|
|
231
|
+
) -> list[JsonObject]:
|
|
232
|
+
payload = client.get_json(f"/projects/{workspace_id}/export-templates")
|
|
233
|
+
return extract_object_list(
|
|
234
|
+
payload,
|
|
235
|
+
context=f"`GET /projects/{workspace_id}/export-templates`",
|
|
236
|
+
preferred_keys=("export_templates", "templates", "data", "items", "results"),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def list_workspace_email_templates(
|
|
241
|
+
client: KantreeClient,
|
|
242
|
+
workspace_id: int,
|
|
243
|
+
) -> list[JsonObject]:
|
|
244
|
+
payload = client.get_json(f"/projects/{workspace_id}/email-templates")
|
|
245
|
+
return extract_object_list(
|
|
246
|
+
payload,
|
|
247
|
+
context=f"`GET /projects/{workspace_id}/email-templates`",
|
|
248
|
+
preferred_keys=("email_templates", "templates", "data", "items", "results"),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_workspace_email_template(
|
|
253
|
+
client: KantreeClient,
|
|
254
|
+
*,
|
|
255
|
+
workspace_id: int,
|
|
256
|
+
template_id: int,
|
|
257
|
+
) -> JsonObject:
|
|
258
|
+
payload = client.get_json(f"/projects/{workspace_id}/email-templates/{template_id}")
|
|
259
|
+
return extract_object(
|
|
260
|
+
payload,
|
|
261
|
+
context=f"`GET /projects/{workspace_id}/email-templates/{template_id}`",
|
|
262
|
+
preferred_keys=("email_template", "template", "data", "item", "result"),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def create_workspace_email_template(
|
|
267
|
+
client: KantreeClient,
|
|
268
|
+
*,
|
|
269
|
+
workspace_id: int,
|
|
270
|
+
name: str,
|
|
271
|
+
subject: str,
|
|
272
|
+
body: str | None = None,
|
|
273
|
+
) -> JsonObject:
|
|
274
|
+
payload: JsonObject = {"name": name, "subject": subject}
|
|
275
|
+
if body is not None:
|
|
276
|
+
payload["body"] = body
|
|
277
|
+
|
|
278
|
+
response = client.request(
|
|
279
|
+
method="POST",
|
|
280
|
+
path=f"/projects/{workspace_id}/email-templates",
|
|
281
|
+
json_body=payload,
|
|
282
|
+
)
|
|
283
|
+
return extract_object(
|
|
284
|
+
response.data,
|
|
285
|
+
context=f"`POST /projects/{workspace_id}/email-templates`",
|
|
286
|
+
preferred_keys=("email_template", "template", "data", "item", "result"),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def update_workspace_email_template(
|
|
291
|
+
client: KantreeClient,
|
|
292
|
+
*,
|
|
293
|
+
workspace_id: int,
|
|
294
|
+
template_id: int,
|
|
295
|
+
name: str | None = None,
|
|
296
|
+
subject: str | None = None,
|
|
297
|
+
body: str | None = None,
|
|
298
|
+
) -> JsonObject:
|
|
299
|
+
changes: JsonObject = {}
|
|
300
|
+
if name is not None:
|
|
301
|
+
changes["name"] = name
|
|
302
|
+
if subject is not None:
|
|
303
|
+
changes["subject"] = subject
|
|
304
|
+
if body is not None:
|
|
305
|
+
changes["body"] = body
|
|
306
|
+
if not changes:
|
|
307
|
+
raise ValidationError(
|
|
308
|
+
"Workspace email-template update requires at least one mutation option."
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
current_template = get_workspace_email_template(
|
|
312
|
+
client,
|
|
313
|
+
workspace_id=workspace_id,
|
|
314
|
+
template_id=template_id,
|
|
315
|
+
)
|
|
316
|
+
payload = {
|
|
317
|
+
"name": _required_email_template_value(
|
|
318
|
+
current_template,
|
|
319
|
+
key="name",
|
|
320
|
+
workspace_id=workspace_id,
|
|
321
|
+
template_id=template_id,
|
|
322
|
+
),
|
|
323
|
+
"subject": _required_email_template_value(
|
|
324
|
+
current_template,
|
|
325
|
+
key="subject",
|
|
326
|
+
workspace_id=workspace_id,
|
|
327
|
+
template_id=template_id,
|
|
328
|
+
),
|
|
329
|
+
"body": _required_email_template_value(
|
|
330
|
+
current_template,
|
|
331
|
+
key="body",
|
|
332
|
+
workspace_id=workspace_id,
|
|
333
|
+
template_id=template_id,
|
|
334
|
+
),
|
|
335
|
+
**changes,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
response = client.request(
|
|
339
|
+
method="PUT",
|
|
340
|
+
path=f"/projects/{workspace_id}/email-templates/{template_id}",
|
|
341
|
+
json_body=payload,
|
|
342
|
+
)
|
|
343
|
+
return extract_object(
|
|
344
|
+
response.data,
|
|
345
|
+
context=f"`PUT /projects/{workspace_id}/email-templates/{template_id}`",
|
|
346
|
+
preferred_keys=("email_template", "template", "data", "item", "result"),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def delete_workspace_email_template(
|
|
351
|
+
client: KantreeClient,
|
|
352
|
+
*,
|
|
353
|
+
workspace_id: int,
|
|
354
|
+
template_id: int,
|
|
355
|
+
) -> Any:
|
|
356
|
+
response = client.request(
|
|
357
|
+
method="DELETE",
|
|
358
|
+
path=f"/projects/{workspace_id}/email-templates/{template_id}",
|
|
359
|
+
)
|
|
360
|
+
return response.data
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def create_workspace_export_template(
|
|
364
|
+
client: KantreeClient,
|
|
365
|
+
*,
|
|
366
|
+
workspace_id: int,
|
|
367
|
+
name: str,
|
|
368
|
+
format_name: str,
|
|
369
|
+
attributes: list[str],
|
|
370
|
+
) -> JsonObject:
|
|
371
|
+
response = client.request(
|
|
372
|
+
method="POST",
|
|
373
|
+
path=f"/projects/{workspace_id}/export-templates",
|
|
374
|
+
json_body={
|
|
375
|
+
"name": name,
|
|
376
|
+
"format": format_name,
|
|
377
|
+
"attributes": attributes,
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
return extract_object(
|
|
381
|
+
response.data,
|
|
382
|
+
context=f"`POST /projects/{workspace_id}/export-templates`",
|
|
383
|
+
preferred_keys=("export_template", "template", "data", "item", "result"),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def update_workspace_export_template(
|
|
388
|
+
client: KantreeClient,
|
|
389
|
+
*,
|
|
390
|
+
workspace_id: int,
|
|
391
|
+
template_id: int,
|
|
392
|
+
name: str | None = None,
|
|
393
|
+
format_name: str | None = None,
|
|
394
|
+
attributes: list[str] | None = None,
|
|
395
|
+
) -> JsonObject:
|
|
396
|
+
payload: JsonObject = {}
|
|
397
|
+
if name is not None:
|
|
398
|
+
payload["name"] = name
|
|
399
|
+
if format_name is not None:
|
|
400
|
+
payload["format"] = format_name
|
|
401
|
+
if attributes is not None:
|
|
402
|
+
payload["attributes"] = attributes
|
|
403
|
+
if not payload:
|
|
404
|
+
raise ValidationError(
|
|
405
|
+
"Workspace export-template update requires at least one mutation option."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
current_template = _find_workspace_export_template(
|
|
409
|
+
list_workspace_export_templates(client, workspace_id),
|
|
410
|
+
workspace_id=workspace_id,
|
|
411
|
+
template_id=template_id,
|
|
412
|
+
)
|
|
413
|
+
payload = {
|
|
414
|
+
"name": _required_export_template_value(
|
|
415
|
+
current_template,
|
|
416
|
+
key="name",
|
|
417
|
+
workspace_id=workspace_id,
|
|
418
|
+
template_id=template_id,
|
|
419
|
+
),
|
|
420
|
+
"format": _required_export_template_value(
|
|
421
|
+
current_template,
|
|
422
|
+
key="format",
|
|
423
|
+
workspace_id=workspace_id,
|
|
424
|
+
template_id=template_id,
|
|
425
|
+
),
|
|
426
|
+
"attributes": _required_export_template_value(
|
|
427
|
+
current_template,
|
|
428
|
+
key="attributes",
|
|
429
|
+
workspace_id=workspace_id,
|
|
430
|
+
template_id=template_id,
|
|
431
|
+
),
|
|
432
|
+
**payload,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
response = client.request(
|
|
436
|
+
method="PUT",
|
|
437
|
+
path=f"/projects/{workspace_id}/export-templates/{template_id}",
|
|
438
|
+
json_body=payload,
|
|
439
|
+
)
|
|
440
|
+
return extract_object(
|
|
441
|
+
response.data,
|
|
442
|
+
context=f"`PUT /projects/{workspace_id}/export-templates/{template_id}`",
|
|
443
|
+
preferred_keys=("export_template", "template", "data", "item", "result"),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def delete_workspace_export_template(
|
|
448
|
+
client: KantreeClient,
|
|
449
|
+
*,
|
|
450
|
+
workspace_id: int,
|
|
451
|
+
template_id: int,
|
|
452
|
+
) -> Any:
|
|
453
|
+
response = client.request(
|
|
454
|
+
method="DELETE",
|
|
455
|
+
path=f"/projects/{workspace_id}/export-templates/{template_id}",
|
|
456
|
+
)
|
|
457
|
+
return response.data
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _find_workspace_export_template(
|
|
461
|
+
templates: list[JsonObject],
|
|
462
|
+
*,
|
|
463
|
+
workspace_id: int,
|
|
464
|
+
template_id: int,
|
|
465
|
+
) -> JsonObject:
|
|
466
|
+
for template in templates:
|
|
467
|
+
parsed_template_id = _parse_positive_int(template.get("id"))
|
|
468
|
+
if parsed_template_id == template_id:
|
|
469
|
+
return template
|
|
470
|
+
raise NotFoundError(
|
|
471
|
+
f"Workspace export-template id `{template_id}` was not found in workspace `{workspace_id}`."
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _required_export_template_value(
|
|
476
|
+
template: JsonObject,
|
|
477
|
+
*,
|
|
478
|
+
key: str,
|
|
479
|
+
workspace_id: int,
|
|
480
|
+
template_id: int,
|
|
481
|
+
) -> Any:
|
|
482
|
+
if key not in template:
|
|
483
|
+
raise ValidationError(
|
|
484
|
+
"Workspace export-template id "
|
|
485
|
+
f"`{template_id}` in workspace `{workspace_id}` does not include `{key}`."
|
|
486
|
+
)
|
|
487
|
+
return template[key]
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _required_email_template_value(
|
|
491
|
+
template: JsonObject,
|
|
492
|
+
*,
|
|
493
|
+
key: str,
|
|
494
|
+
workspace_id: int,
|
|
495
|
+
template_id: int,
|
|
496
|
+
) -> Any:
|
|
497
|
+
if key not in template:
|
|
498
|
+
raise ValidationError(
|
|
499
|
+
"Workspace email-template id "
|
|
500
|
+
f"`{template_id}` in workspace `{workspace_id}` does not include `{key}`."
|
|
501
|
+
)
|
|
502
|
+
return template[key]
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def create_workspace(
|
|
506
|
+
client: KantreeClient,
|
|
507
|
+
*,
|
|
508
|
+
title: str,
|
|
509
|
+
organization_id: int | None = None,
|
|
510
|
+
team_id: int | None = None,
|
|
511
|
+
include_cards: bool | None = None,
|
|
512
|
+
) -> JsonObject:
|
|
513
|
+
if team_id is not None and organization_id is None:
|
|
514
|
+
raise ValidationError("Workspace creation with a team requires an organization.")
|
|
515
|
+
|
|
516
|
+
payload: JsonObject = {"title": title}
|
|
517
|
+
if include_cards is not None:
|
|
518
|
+
payload["include_cards"] = include_cards
|
|
519
|
+
if team_id is not None:
|
|
520
|
+
payload["team_id"] = team_id
|
|
521
|
+
|
|
522
|
+
if organization_id is None:
|
|
523
|
+
endpoint = "/projects"
|
|
524
|
+
context = "`POST /projects`"
|
|
525
|
+
else:
|
|
526
|
+
endpoint = f"/organizations/{organization_id}/projects"
|
|
527
|
+
context = f"`POST /organizations/{organization_id}/projects`"
|
|
528
|
+
|
|
529
|
+
response = client.request(
|
|
530
|
+
method="POST",
|
|
531
|
+
path=endpoint,
|
|
532
|
+
json_body=payload,
|
|
533
|
+
)
|
|
534
|
+
return extract_object(
|
|
535
|
+
response.data,
|
|
536
|
+
context=context,
|
|
537
|
+
preferred_keys=("project", "workspace", "data", "item", "result"),
|
|
538
|
+
require_id_field=False,
|
|
539
|
+
object_check=_looks_like_workspace,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def delete_workspace(client: KantreeClient, *, workspace_id: int) -> Any:
|
|
544
|
+
response = client.request(
|
|
545
|
+
method="DELETE",
|
|
546
|
+
path=f"/projects/{workspace_id}",
|
|
547
|
+
)
|
|
548
|
+
return response.data
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def relocate_workspace(
|
|
552
|
+
client: KantreeClient,
|
|
553
|
+
*,
|
|
554
|
+
workspace_id: int,
|
|
555
|
+
organization_id: int,
|
|
556
|
+
team_id: int | None = None,
|
|
557
|
+
) -> Any:
|
|
558
|
+
payload: JsonObject = {"org_id": organization_id}
|
|
559
|
+
if team_id is not None:
|
|
560
|
+
payload["team_id"] = team_id
|
|
561
|
+
|
|
562
|
+
response = client.request(
|
|
563
|
+
method="POST",
|
|
564
|
+
path=f"/projects/{workspace_id}/relocate-to-other-org",
|
|
565
|
+
json_body=payload,
|
|
566
|
+
)
|
|
567
|
+
return response.data
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def list_workspace_fields(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
571
|
+
payload = client.get_json(f"/projects/{workspace_id}/attributes")
|
|
572
|
+
return extract_object_list(
|
|
573
|
+
payload,
|
|
574
|
+
context=f"`GET /projects/{workspace_id}/attributes`",
|
|
575
|
+
preferred_keys=("attributes", "data", "items", "results"),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def list_workspace_views(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
580
|
+
payload = client.get_json(f"/projects/{workspace_id}/views")
|
|
581
|
+
return extract_object_list(
|
|
582
|
+
payload,
|
|
583
|
+
context=f"`GET /projects/{workspace_id}/views`",
|
|
584
|
+
preferred_keys=("views", "data", "items", "results"),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def get_workspace_view(
|
|
589
|
+
client: KantreeClient,
|
|
590
|
+
*,
|
|
591
|
+
workspace_id: int,
|
|
592
|
+
view_id: int,
|
|
593
|
+
) -> JsonObject:
|
|
594
|
+
payload = client.get_json(f"/projects/{workspace_id}/views/{view_id}")
|
|
595
|
+
return extract_object(
|
|
596
|
+
payload,
|
|
597
|
+
context=f"`GET /projects/{workspace_id}/views/{view_id}`",
|
|
598
|
+
preferred_keys=("view", "data", "item", "result"),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def create_workspace_view(
|
|
603
|
+
client: KantreeClient,
|
|
604
|
+
*,
|
|
605
|
+
workspace_id: int,
|
|
606
|
+
name: str,
|
|
607
|
+
mode: str | None = None,
|
|
608
|
+
filter_text: str | None = None,
|
|
609
|
+
include_archived: bool | None = None,
|
|
610
|
+
include_descendants: bool | None = None,
|
|
611
|
+
public: bool | None = None,
|
|
612
|
+
) -> JsonObject:
|
|
613
|
+
payload = _workspace_view_payload(
|
|
614
|
+
name=name,
|
|
615
|
+
mode=mode,
|
|
616
|
+
filter_text=filter_text,
|
|
617
|
+
include_archived=include_archived,
|
|
618
|
+
include_descendants=include_descendants,
|
|
619
|
+
public=public,
|
|
620
|
+
)
|
|
621
|
+
response = client.request(
|
|
622
|
+
method="POST",
|
|
623
|
+
path=f"/projects/{workspace_id}/views",
|
|
624
|
+
json_body=payload,
|
|
625
|
+
)
|
|
626
|
+
return extract_object(
|
|
627
|
+
response.data,
|
|
628
|
+
context=f"`POST /projects/{workspace_id}/views`",
|
|
629
|
+
preferred_keys=("view", "data", "item", "result"),
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def update_workspace_view(
|
|
634
|
+
client: KantreeClient,
|
|
635
|
+
*,
|
|
636
|
+
workspace_id: int,
|
|
637
|
+
view_id: int,
|
|
638
|
+
name: str | None = None,
|
|
639
|
+
mode: str | None = None,
|
|
640
|
+
filter_text: str | None = None,
|
|
641
|
+
clear_filter: bool = False,
|
|
642
|
+
include_archived: bool | None = None,
|
|
643
|
+
include_descendants: bool | None = None,
|
|
644
|
+
icon: str | None = None,
|
|
645
|
+
card_ref: int | None = None,
|
|
646
|
+
clear_card_ref: bool = False,
|
|
647
|
+
limit_to_ref: bool | None = None,
|
|
648
|
+
public: bool | None = None,
|
|
649
|
+
) -> JsonObject:
|
|
650
|
+
if filter_text is not None and clear_filter:
|
|
651
|
+
raise ValidationError("Use either `--filter` or `--clear-filter`, not both.")
|
|
652
|
+
if card_ref is not None and clear_card_ref:
|
|
653
|
+
raise ValidationError("Use either `--card-ref` or `--clear-card-ref`, not both.")
|
|
654
|
+
|
|
655
|
+
payload = _workspace_view_payload(
|
|
656
|
+
name=name,
|
|
657
|
+
mode=mode,
|
|
658
|
+
filter_text=None if clear_filter else filter_text,
|
|
659
|
+
include_filter=filter_text is not None or clear_filter,
|
|
660
|
+
include_archived=include_archived,
|
|
661
|
+
include_descendants=include_descendants,
|
|
662
|
+
icon=icon,
|
|
663
|
+
card_ref=None if clear_card_ref else card_ref,
|
|
664
|
+
include_card_ref=card_ref is not None or clear_card_ref,
|
|
665
|
+
limit_to_ref=limit_to_ref,
|
|
666
|
+
public=public,
|
|
667
|
+
)
|
|
668
|
+
if not payload:
|
|
669
|
+
raise ValidationError("Workspace view update requires at least one mutation option.")
|
|
670
|
+
|
|
671
|
+
response = client.request(
|
|
672
|
+
method="PUT",
|
|
673
|
+
path=f"/projects/{workspace_id}/views/{view_id}",
|
|
674
|
+
json_body=payload,
|
|
675
|
+
)
|
|
676
|
+
return extract_object(
|
|
677
|
+
response.data,
|
|
678
|
+
context=f"`PUT /projects/{workspace_id}/views/{view_id}`",
|
|
679
|
+
preferred_keys=("view", "data", "item", "result"),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def delete_workspace_view(
|
|
684
|
+
client: KantreeClient,
|
|
685
|
+
*,
|
|
686
|
+
workspace_id: int,
|
|
687
|
+
view_id: int,
|
|
688
|
+
) -> Any:
|
|
689
|
+
response = client.request(
|
|
690
|
+
method="DELETE",
|
|
691
|
+
path=f"/projects/{workspace_id}/views/{view_id}",
|
|
692
|
+
)
|
|
693
|
+
return response.data
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def list_project_automations(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
697
|
+
payload = client.get_json(f"/projects/{workspace_id}/automations/")
|
|
698
|
+
return extract_object_list(
|
|
699
|
+
payload,
|
|
700
|
+
context=f"`GET /projects/{workspace_id}/automations/`",
|
|
701
|
+
preferred_keys=("automations", "data", "items", "results"),
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def get_project_automation(
|
|
706
|
+
client: KantreeClient,
|
|
707
|
+
*,
|
|
708
|
+
workspace_id: int,
|
|
709
|
+
rule_id: int,
|
|
710
|
+
) -> JsonObject:
|
|
711
|
+
response = client.get_json(f"/projects/{workspace_id}/automations/{rule_id}")
|
|
712
|
+
return extract_object(
|
|
713
|
+
response,
|
|
714
|
+
context=f"`GET /projects/{workspace_id}/automations/{rule_id}`",
|
|
715
|
+
preferred_keys=("automation", "data", "item", "result"),
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def create_project_automation(
|
|
720
|
+
client: KantreeClient,
|
|
721
|
+
*,
|
|
722
|
+
workspace_id: int,
|
|
723
|
+
payload: JsonObject,
|
|
724
|
+
) -> JsonObject:
|
|
725
|
+
response = client.request(
|
|
726
|
+
method="POST",
|
|
727
|
+
path=f"/projects/{workspace_id}/automations/",
|
|
728
|
+
json_body=payload,
|
|
729
|
+
)
|
|
730
|
+
return extract_object(
|
|
731
|
+
response.data,
|
|
732
|
+
context=f"`POST /projects/{workspace_id}/automations/`",
|
|
733
|
+
preferred_keys=("automation", "data", "item", "result"),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def update_project_automation(
|
|
738
|
+
client: KantreeClient,
|
|
739
|
+
*,
|
|
740
|
+
workspace_id: int,
|
|
741
|
+
rule_id: int,
|
|
742
|
+
payload: JsonObject,
|
|
743
|
+
) -> JsonObject:
|
|
744
|
+
response = client.request(
|
|
745
|
+
method="PUT",
|
|
746
|
+
path=f"/projects/{workspace_id}/automations/{rule_id}",
|
|
747
|
+
json_body=payload,
|
|
748
|
+
)
|
|
749
|
+
return extract_object(
|
|
750
|
+
response.data,
|
|
751
|
+
context=f"`PUT /projects/{workspace_id}/automations/{rule_id}`",
|
|
752
|
+
preferred_keys=("automation", "data", "item", "result"),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def delete_project_automation(
|
|
757
|
+
client: KantreeClient,
|
|
758
|
+
*,
|
|
759
|
+
workspace_id: int,
|
|
760
|
+
rule_id: int,
|
|
761
|
+
) -> Any:
|
|
762
|
+
response = client.request(
|
|
763
|
+
method="DELETE",
|
|
764
|
+
path=f"/projects/{workspace_id}/automations/{rule_id}",
|
|
765
|
+
)
|
|
766
|
+
return response.data
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def list_automation_type_events(client: KantreeClient) -> list[JsonObject]:
|
|
770
|
+
payload = client.get_json("/automation-types/events")
|
|
771
|
+
return extract_object_list(
|
|
772
|
+
payload,
|
|
773
|
+
context="`GET /automation-types/events`",
|
|
774
|
+
preferred_keys=("events", "data", "items", "results"),
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def list_automation_type_actions(client: KantreeClient) -> list[JsonObject]:
|
|
779
|
+
payload = client.get_json("/automation-types/actions")
|
|
780
|
+
return extract_object_list(
|
|
781
|
+
payload,
|
|
782
|
+
context="`GET /automation-types/actions`",
|
|
783
|
+
preferred_keys=("actions", "data", "items", "results"),
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def list_automation_type_queries(client: KantreeClient) -> list[JsonObject]:
|
|
788
|
+
payload = client.get_json("/automation-types/queries")
|
|
789
|
+
return extract_object_list(
|
|
790
|
+
payload,
|
|
791
|
+
context="`GET /automation-types/queries`",
|
|
792
|
+
preferred_keys=("queries", "data", "items", "results"),
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def list_field_types(client: KantreeClient) -> list[JsonObject]:
|
|
797
|
+
payload = client.get_json("/attribute-types")
|
|
798
|
+
return extract_object_list(
|
|
799
|
+
payload,
|
|
800
|
+
context="`GET /attribute-types`",
|
|
801
|
+
preferred_keys=("attribute_types", "types", "data", "items", "results"),
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def list_workspace_group_types(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
806
|
+
payload = client.get_json(f"/projects/{workspace_id}/group-types")
|
|
807
|
+
return extract_object_list(
|
|
808
|
+
payload,
|
|
809
|
+
context=f"`GET /projects/{workspace_id}/group-types`",
|
|
810
|
+
preferred_keys=("group_types", "types", "data", "items", "results"),
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def list_card_groups(
|
|
815
|
+
client: KantreeClient,
|
|
816
|
+
parent_id: int,
|
|
817
|
+
*,
|
|
818
|
+
type_id: int | None = None,
|
|
819
|
+
) -> list[JsonObject]:
|
|
820
|
+
params: dict[str, str] | None = None
|
|
821
|
+
if type_id is not None:
|
|
822
|
+
params = {"type_id": str(type_id)}
|
|
823
|
+
|
|
824
|
+
response = client.request(
|
|
825
|
+
method="GET",
|
|
826
|
+
path=f"/cards/{parent_id}/groups",
|
|
827
|
+
params=params,
|
|
828
|
+
)
|
|
829
|
+
payload = response.data
|
|
830
|
+
return extract_object_list(
|
|
831
|
+
payload,
|
|
832
|
+
context=f"`GET /cards/{parent_id}/groups`",
|
|
833
|
+
preferred_keys=("groups", "data", "items", "results"),
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def create_card_group(
|
|
838
|
+
client: KantreeClient,
|
|
839
|
+
*,
|
|
840
|
+
parent_id: int,
|
|
841
|
+
title: str,
|
|
842
|
+
type_id: int,
|
|
843
|
+
card_state: str | None = None,
|
|
844
|
+
description: str | None = None,
|
|
845
|
+
color: str | None = None,
|
|
846
|
+
max_cards_limit: int | None = None,
|
|
847
|
+
) -> JsonObject:
|
|
848
|
+
payload: dict[str, object] = {
|
|
849
|
+
"title": title,
|
|
850
|
+
"type_id": type_id,
|
|
851
|
+
}
|
|
852
|
+
if card_state is not None:
|
|
853
|
+
payload["card_state"] = card_state
|
|
854
|
+
if description is not None:
|
|
855
|
+
payload["description"] = description
|
|
856
|
+
if color is not None:
|
|
857
|
+
payload["color"] = color
|
|
858
|
+
if max_cards_limit is not None:
|
|
859
|
+
payload["max_cards_limit"] = max_cards_limit
|
|
860
|
+
|
|
861
|
+
response = client.request(
|
|
862
|
+
method="POST",
|
|
863
|
+
path=f"/cards/{parent_id}/groups/",
|
|
864
|
+
json_body=payload,
|
|
865
|
+
)
|
|
866
|
+
return extract_object(
|
|
867
|
+
response.data,
|
|
868
|
+
context=f"`POST /cards/{parent_id}/groups/`",
|
|
869
|
+
preferred_keys=("group", "data", "item", "result"),
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def update_card_group(
|
|
874
|
+
client: KantreeClient,
|
|
875
|
+
*,
|
|
876
|
+
parent_id: int,
|
|
877
|
+
group_id: int,
|
|
878
|
+
title: str | None = None,
|
|
879
|
+
card_state: str | None = None,
|
|
880
|
+
description: str | None = None,
|
|
881
|
+
color: str | None = None,
|
|
882
|
+
max_cards_limit: int | None = None,
|
|
883
|
+
) -> JsonObject:
|
|
884
|
+
payload: dict[str, object] = {}
|
|
885
|
+
if title is not None:
|
|
886
|
+
payload["title"] = title
|
|
887
|
+
if card_state is not None:
|
|
888
|
+
payload["card_state"] = card_state
|
|
889
|
+
if description is not None:
|
|
890
|
+
payload["description"] = description
|
|
891
|
+
if color is not None:
|
|
892
|
+
payload["color"] = color
|
|
893
|
+
if max_cards_limit is not None:
|
|
894
|
+
payload["max_cards_limit"] = max_cards_limit
|
|
895
|
+
|
|
896
|
+
response = client.request(
|
|
897
|
+
method="PUT",
|
|
898
|
+
path=f"/cards/{parent_id}/groups/{group_id}/",
|
|
899
|
+
json_body=payload,
|
|
900
|
+
)
|
|
901
|
+
return extract_object(
|
|
902
|
+
response.data,
|
|
903
|
+
context=f"`PUT /cards/{parent_id}/groups/{group_id}/`",
|
|
904
|
+
preferred_keys=("group", "data", "item", "result"),
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def set_cards_state(
|
|
909
|
+
client: KantreeClient,
|
|
910
|
+
*,
|
|
911
|
+
parent_id: int,
|
|
912
|
+
group_id: int,
|
|
913
|
+
state: str,
|
|
914
|
+
) -> Any:
|
|
915
|
+
response = client.request(
|
|
916
|
+
method="POST",
|
|
917
|
+
path=f"/cards/{parent_id}/groups/{group_id}/set-cards-state",
|
|
918
|
+
json_body={"state": state},
|
|
919
|
+
)
|
|
920
|
+
return response.data
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def export_workspace_json(
|
|
924
|
+
client: KantreeClient,
|
|
925
|
+
workspace_id: int,
|
|
926
|
+
*,
|
|
927
|
+
kql_filter: str | None = None,
|
|
928
|
+
) -> Any:
|
|
929
|
+
params: dict[str, str] = {}
|
|
930
|
+
if kql_filter is not None:
|
|
931
|
+
normalized_filter = kql_filter.strip()
|
|
932
|
+
if not normalized_filter:
|
|
933
|
+
raise ValidationError("`--filter` expects a non-empty KQL value.")
|
|
934
|
+
params["filter"] = normalized_filter
|
|
935
|
+
|
|
936
|
+
merged_payload: Any = None
|
|
937
|
+
next_page = 1
|
|
938
|
+
while True:
|
|
939
|
+
request_params = dict(params)
|
|
940
|
+
request_params["page"] = str(next_page)
|
|
941
|
+
|
|
942
|
+
response = client.request(
|
|
943
|
+
method="GET",
|
|
944
|
+
path=f"/projects/{workspace_id}/export/json",
|
|
945
|
+
params=request_params,
|
|
946
|
+
)
|
|
947
|
+
if merged_payload is None:
|
|
948
|
+
merged_payload = response.data
|
|
949
|
+
else:
|
|
950
|
+
merged_payload = _merge_export_page(
|
|
951
|
+
merged_payload,
|
|
952
|
+
response.data,
|
|
953
|
+
context=f"`GET /projects/{workspace_id}/export/json`",
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
parsed_next_page = next_page_header_value(response.headers)
|
|
957
|
+
if parsed_next_page is None:
|
|
958
|
+
break
|
|
959
|
+
next_page = parsed_next_page
|
|
960
|
+
|
|
961
|
+
return merged_payload
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def start_workspace_export_artifact(
|
|
965
|
+
client: KantreeClient,
|
|
966
|
+
workspace_id: int,
|
|
967
|
+
*,
|
|
968
|
+
export_format: str,
|
|
969
|
+
kql_filter: str | None = None,
|
|
970
|
+
include_descendants: bool | None = None,
|
|
971
|
+
notify: bool = False,
|
|
972
|
+
) -> Any:
|
|
973
|
+
payload: JsonObject = {
|
|
974
|
+
"format": export_format,
|
|
975
|
+
"notify": notify,
|
|
976
|
+
}
|
|
977
|
+
if kql_filter is not None:
|
|
978
|
+
normalized_filter = kql_filter.strip()
|
|
979
|
+
if not normalized_filter:
|
|
980
|
+
raise ValidationError("`--filter` expects a non-empty KQL value.")
|
|
981
|
+
payload["filter"] = normalized_filter
|
|
982
|
+
if include_descendants is not None:
|
|
983
|
+
payload["include_descendants"] = include_descendants
|
|
984
|
+
|
|
985
|
+
response = client.request(
|
|
986
|
+
method="POST",
|
|
987
|
+
path=f"/projects/{workspace_id}/export",
|
|
988
|
+
json_body=payload,
|
|
989
|
+
)
|
|
990
|
+
return response.data
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def workspace_summary(workspace: JsonObject) -> JsonObject:
|
|
994
|
+
summary: JsonObject = {
|
|
995
|
+
"id": workspace.get("id"),
|
|
996
|
+
"title": workspace.get("title"),
|
|
997
|
+
"top_level_card_id": workspace.get("top_level_card_id"),
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
organization_id = workspace.get("organization_id")
|
|
1001
|
+
if organization_id is not None:
|
|
1002
|
+
summary["organization_id"] = organization_id
|
|
1003
|
+
|
|
1004
|
+
organization_title = workspace.get("organization_title")
|
|
1005
|
+
if organization_title is not None:
|
|
1006
|
+
summary["organization_title"] = organization_title
|
|
1007
|
+
|
|
1008
|
+
team_id = workspace.get("team_id")
|
|
1009
|
+
if team_id is not None:
|
|
1010
|
+
summary["team_id"] = team_id
|
|
1011
|
+
|
|
1012
|
+
team_name = workspace.get("team_name")
|
|
1013
|
+
if team_name is not None:
|
|
1014
|
+
summary["team_name"] = team_name
|
|
1015
|
+
|
|
1016
|
+
return summary
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def workspace_view_summary(view: JsonObject) -> JsonObject:
|
|
1020
|
+
summary: JsonObject = {
|
|
1021
|
+
"id": view.get("id"),
|
|
1022
|
+
"title": view.get("title", view.get("name")),
|
|
1023
|
+
}
|
|
1024
|
+
view_type = view.get("type")
|
|
1025
|
+
if view_type is not None:
|
|
1026
|
+
summary["type"] = view_type
|
|
1027
|
+
return summary
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _workspace_view_payload(
|
|
1031
|
+
*,
|
|
1032
|
+
name: str | None = None,
|
|
1033
|
+
mode: str | None = None,
|
|
1034
|
+
filter_text: str | None = None,
|
|
1035
|
+
include_filter: bool = False,
|
|
1036
|
+
include_archived: bool | None = None,
|
|
1037
|
+
include_descendants: bool | None = None,
|
|
1038
|
+
icon: str | None = None,
|
|
1039
|
+
card_ref: int | None = None,
|
|
1040
|
+
include_card_ref: bool = False,
|
|
1041
|
+
limit_to_ref: bool | None = None,
|
|
1042
|
+
public: bool | None = None,
|
|
1043
|
+
) -> JsonObject:
|
|
1044
|
+
payload: JsonObject = {}
|
|
1045
|
+
if name is not None:
|
|
1046
|
+
payload["name"] = name
|
|
1047
|
+
if mode is not None:
|
|
1048
|
+
payload["mode"] = mode
|
|
1049
|
+
if include_filter or filter_text is not None:
|
|
1050
|
+
payload["filter"] = filter_text
|
|
1051
|
+
if include_archived is not None:
|
|
1052
|
+
payload["include_archived"] = include_archived
|
|
1053
|
+
if include_descendants is not None:
|
|
1054
|
+
payload["include_descendants"] = include_descendants
|
|
1055
|
+
if icon is not None:
|
|
1056
|
+
payload["icon"] = icon
|
|
1057
|
+
if include_card_ref or card_ref is not None:
|
|
1058
|
+
payload["card_ref"] = card_ref
|
|
1059
|
+
if limit_to_ref is not None:
|
|
1060
|
+
payload["limit_to_ref"] = limit_to_ref
|
|
1061
|
+
if public is not None:
|
|
1062
|
+
payload["public"] = public
|
|
1063
|
+
return payload
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def list_workspace_models(client: KantreeClient, workspace_id: int) -> list[JsonObject]:
|
|
1067
|
+
payload = client.get_json(f"/projects/{workspace_id}/models")
|
|
1068
|
+
return extract_object_list(
|
|
1069
|
+
payload,
|
|
1070
|
+
context=f"`GET /projects/{workspace_id}/models`",
|
|
1071
|
+
preferred_keys=("models", "data", "items", "results"),
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def get_workspace_model(
|
|
1076
|
+
client: KantreeClient,
|
|
1077
|
+
*,
|
|
1078
|
+
workspace_id: int,
|
|
1079
|
+
model_id: int,
|
|
1080
|
+
) -> JsonObject:
|
|
1081
|
+
return _find_workspace_model(
|
|
1082
|
+
list_workspace_models(client, workspace_id),
|
|
1083
|
+
workspace_id=workspace_id,
|
|
1084
|
+
model_id=model_id,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def create_workspace_model(
|
|
1089
|
+
client: KantreeClient,
|
|
1090
|
+
*,
|
|
1091
|
+
workspace_id: int,
|
|
1092
|
+
name: str,
|
|
1093
|
+
color: str | None = None,
|
|
1094
|
+
only_show_own_attributes: bool | None = None,
|
|
1095
|
+
hide_children_attributes: bool | None = None,
|
|
1096
|
+
) -> JsonObject:
|
|
1097
|
+
payload: JsonObject = {"name": name, "project_id": workspace_id}
|
|
1098
|
+
if color is not None:
|
|
1099
|
+
payload["card_color"] = color
|
|
1100
|
+
if only_show_own_attributes is not None:
|
|
1101
|
+
payload["only_show_own_attributes"] = only_show_own_attributes
|
|
1102
|
+
if hide_children_attributes is not None:
|
|
1103
|
+
payload["hide_children_attributes"] = hide_children_attributes
|
|
1104
|
+
|
|
1105
|
+
response = client.request(method="POST", path="/models/", json_body=payload)
|
|
1106
|
+
return extract_object(
|
|
1107
|
+
response.data,
|
|
1108
|
+
context="`POST /models/`",
|
|
1109
|
+
preferred_keys=("model", "data", "item", "result"),
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def update_workspace_model(
|
|
1114
|
+
client: KantreeClient,
|
|
1115
|
+
*,
|
|
1116
|
+
workspace_id: int,
|
|
1117
|
+
model_id: int,
|
|
1118
|
+
changes: JsonObject,
|
|
1119
|
+
) -> JsonObject:
|
|
1120
|
+
_find_workspace_model(
|
|
1121
|
+
list_workspace_models(client, workspace_id),
|
|
1122
|
+
workspace_id=workspace_id,
|
|
1123
|
+
model_id=model_id,
|
|
1124
|
+
)
|
|
1125
|
+
response = client.request(method="PUT", path=f"/models/{model_id}", json_body=changes)
|
|
1126
|
+
return extract_object(
|
|
1127
|
+
response.data,
|
|
1128
|
+
context=f"`PUT /models/{model_id}`",
|
|
1129
|
+
preferred_keys=("model", "data", "item", "result"),
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def delete_workspace_model(
|
|
1134
|
+
client: KantreeClient,
|
|
1135
|
+
*,
|
|
1136
|
+
workspace_id: int,
|
|
1137
|
+
model_id: int,
|
|
1138
|
+
) -> Any:
|
|
1139
|
+
_find_workspace_model(
|
|
1140
|
+
list_workspace_models(client, workspace_id),
|
|
1141
|
+
workspace_id=workspace_id,
|
|
1142
|
+
model_id=model_id,
|
|
1143
|
+
)
|
|
1144
|
+
response = client.request(method="DELETE", path=f"/projects/{workspace_id}/models/{model_id}")
|
|
1145
|
+
return response.data
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def list_workspace_model_fields(
|
|
1149
|
+
client: KantreeClient,
|
|
1150
|
+
*,
|
|
1151
|
+
workspace_id: int,
|
|
1152
|
+
model_id: int,
|
|
1153
|
+
) -> list[JsonObject]:
|
|
1154
|
+
models = list_workspace_models(client, workspace_id)
|
|
1155
|
+
for model in models:
|
|
1156
|
+
parsed_model_id = _parse_int(model.get("id"))
|
|
1157
|
+
if parsed_model_id == model_id:
|
|
1158
|
+
return _extract_model_fields(
|
|
1159
|
+
model,
|
|
1160
|
+
context=f"Model id `{model_id}` from `GET /projects/{workspace_id}/models`",
|
|
1161
|
+
)
|
|
1162
|
+
raise NotFoundError(f"Card type/model id `{model_id}` was not found in this workspace.")
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _find_workspace_model(
|
|
1166
|
+
models: list[JsonObject],
|
|
1167
|
+
*,
|
|
1168
|
+
workspace_id: int,
|
|
1169
|
+
model_id: int,
|
|
1170
|
+
) -> JsonObject:
|
|
1171
|
+
for model in models:
|
|
1172
|
+
parsed_model_id = _parse_int(model.get("id"))
|
|
1173
|
+
if parsed_model_id == model_id:
|
|
1174
|
+
return model
|
|
1175
|
+
raise NotFoundError(
|
|
1176
|
+
f"Card type/model id `{model_id}` was not found in workspace `{workspace_id}`."
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def workspace_id(workspace: JsonObject) -> int:
|
|
1181
|
+
parsed = _parse_int(workspace.get("id"))
|
|
1182
|
+
if parsed is None:
|
|
1183
|
+
raise ValidationError(
|
|
1184
|
+
"Workspace payload does not contain a usable integer `id` for this operation."
|
|
1185
|
+
)
|
|
1186
|
+
return parsed
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _organization_context(org: JsonObject) -> OrganizationContext:
|
|
1190
|
+
org_id = _parse_int(org.get("id"))
|
|
1191
|
+
if org_id is None:
|
|
1192
|
+
raise ValidationError("Organization payload is missing integer `id`.")
|
|
1193
|
+
title = org.get("name")
|
|
1194
|
+
if title is None:
|
|
1195
|
+
title = org.get("title")
|
|
1196
|
+
return OrganizationContext(id=org_id, title=title if isinstance(title, str) else None)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _workspace_with_context(
|
|
1200
|
+
workspace: JsonObject,
|
|
1201
|
+
*,
|
|
1202
|
+
organization_id: int,
|
|
1203
|
+
organization_title: str | None = None,
|
|
1204
|
+
team: JsonObject | None,
|
|
1205
|
+
) -> JsonObject:
|
|
1206
|
+
with_context = dict(workspace)
|
|
1207
|
+
with_context.setdefault("organization_id", organization_id)
|
|
1208
|
+
if organization_title is not None:
|
|
1209
|
+
with_context.setdefault("organization_title", organization_title)
|
|
1210
|
+
if team is not None:
|
|
1211
|
+
team_id = _parse_positive_int(team.get("id"))
|
|
1212
|
+
if team_id is not None:
|
|
1213
|
+
with_context.setdefault("team_id", team_id)
|
|
1214
|
+
team_title = _team_title(team)
|
|
1215
|
+
if team_title is not None:
|
|
1216
|
+
with_context.setdefault("team_name", team_title)
|
|
1217
|
+
return with_context
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _team_title(team: JsonObject) -> str | None:
|
|
1221
|
+
for key in ("name", "title"):
|
|
1222
|
+
value = team.get(key)
|
|
1223
|
+
if isinstance(value, str):
|
|
1224
|
+
normalized = value.strip()
|
|
1225
|
+
if normalized:
|
|
1226
|
+
return normalized
|
|
1227
|
+
return None
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _looks_like_workspace(payload: JsonObject) -> bool:
|
|
1231
|
+
return "id" in payload and ("title" in payload or "top_level_card_id" in payload)
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _extract_model_fields(model: JsonObject, *, context: str) -> list[JsonObject]:
|
|
1235
|
+
saw_field_container = False
|
|
1236
|
+
for key in ("attributes", "fields"):
|
|
1237
|
+
maybe_fields = model.get(key)
|
|
1238
|
+
if isinstance(maybe_fields, (dict, list)):
|
|
1239
|
+
saw_field_container = True
|
|
1240
|
+
try:
|
|
1241
|
+
return extract_object_list(
|
|
1242
|
+
maybe_fields,
|
|
1243
|
+
context=context,
|
|
1244
|
+
preferred_keys=("attributes", "fields", "data", "items", "results"),
|
|
1245
|
+
)
|
|
1246
|
+
except ValidationError:
|
|
1247
|
+
continue
|
|
1248
|
+
|
|
1249
|
+
if saw_field_container:
|
|
1250
|
+
raise ValidationError(f"{context} does not include usable model-scoped fields.")
|
|
1251
|
+
raise ValidationError(f"{context} does not include model-scoped fields.")
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def _parse_int(value: Any) -> int | None:
|
|
1255
|
+
if isinstance(value, bool):
|
|
1256
|
+
return None
|
|
1257
|
+
if isinstance(value, int):
|
|
1258
|
+
return value
|
|
1259
|
+
if isinstance(value, str):
|
|
1260
|
+
stripped = value.strip()
|
|
1261
|
+
if not stripped:
|
|
1262
|
+
return None
|
|
1263
|
+
try:
|
|
1264
|
+
return int(stripped)
|
|
1265
|
+
except ValueError:
|
|
1266
|
+
return None
|
|
1267
|
+
return None
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def _parse_positive_int(value: Any) -> int | None:
|
|
1271
|
+
parsed = _parse_int(value)
|
|
1272
|
+
if parsed is None or parsed < 1:
|
|
1273
|
+
return None
|
|
1274
|
+
return parsed
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _merge_export_page(base_payload: Any, page_payload: Any, *, context: str) -> Any:
|
|
1278
|
+
if not isinstance(base_payload, dict) or not isinstance(page_payload, dict):
|
|
1279
|
+
raise ValidationError(
|
|
1280
|
+
f"Unexpected paginated response payload for {context}: expected objects."
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
merged = dict(base_payload)
|
|
1284
|
+
for key, page_value in page_payload.items():
|
|
1285
|
+
if key not in merged:
|
|
1286
|
+
merged[key] = page_value
|
|
1287
|
+
continue
|
|
1288
|
+
|
|
1289
|
+
base_value = merged[key]
|
|
1290
|
+
if isinstance(base_value, list) and isinstance(page_value, list):
|
|
1291
|
+
merged[key] = [*base_value, *page_value]
|
|
1292
|
+
continue
|
|
1293
|
+
|
|
1294
|
+
if page_value != base_value:
|
|
1295
|
+
raise ValidationError(
|
|
1296
|
+
f"Unexpected paginated response payload for {context}: "
|
|
1297
|
+
f"conflicting `{key}` values across pages."
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
return merged
|