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.
@@ -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