dart-tools 0.6.16__py3-none-any.whl → 0.7.1__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.

Potentially problematic release.


This version of dart-tools might be problematic. Click here for more details.

Files changed (285) hide show
  1. dart/__init__.py +8 -6
  2. dart/dart.py +423 -702
  3. dart/generated/__init__.py +1 -1
  4. dart/generated/api/__init__.py +8 -0
  5. dart/generated/api/comment/__init__.py +1 -0
  6. dart/generated/api/{transactions/transactions_create.py → comment/create_comment.py} +57 -58
  7. dart/generated/api/config/__init__.py +1 -0
  8. dart/generated/api/config/get_config.py +141 -0
  9. dart/generated/api/dartboard/__init__.py +1 -0
  10. dart/generated/api/{attachments/attachments_list.py → dartboard/retrieve_dartboard.py} +58 -62
  11. dart/generated/api/doc/__init__.py +1 -0
  12. dart/generated/api/doc/create_doc.py +179 -0
  13. dart/generated/api/doc/delete_doc.py +165 -0
  14. dart/generated/api/{comments/comments_list.py → doc/list_docs.py} +115 -111
  15. dart/generated/api/{forms/forms_list.py → doc/retrieve_doc.py} +58 -62
  16. dart/generated/api/doc/update_doc.py +188 -0
  17. dart/generated/api/folder/__init__.py +1 -0
  18. dart/generated/api/{webhooks/webhooks_list.py → folder/retrieve_folder.py} +58 -62
  19. dart/generated/api/task/__init__.py +1 -0
  20. dart/generated/api/task/create_task.py +172 -0
  21. dart/generated/api/task/delete_task.py +165 -0
  22. dart/generated/api/task/list_tasks.py +497 -0
  23. dart/generated/api/{layouts/layouts_list.py → task/retrieve_task.py} +58 -62
  24. dart/generated/api/task/update_task.py +188 -0
  25. dart/generated/api/view/__init__.py +1 -0
  26. dart/generated/api/{tenants/tenants_list.py → view/retrieve_view.py} +58 -62
  27. dart/generated/client.py +13 -13
  28. dart/generated/models/__init__.py +34 -342
  29. dart/generated/models/comment.py +34 -150
  30. dart/generated/models/comment_create.py +18 -97
  31. dart/generated/models/concise_doc.py +84 -0
  32. dart/generated/models/concise_task.py +267 -0
  33. dart/generated/models/dartboard.py +37 -237
  34. dart/generated/models/doc.py +31 -183
  35. dart/generated/models/doc_create.py +22 -238
  36. dart/generated/models/doc_update.py +26 -233
  37. dart/generated/models/folder.py +39 -84
  38. dart/generated/models/list_docs_o_item.py +15 -0
  39. dart/generated/models/{paginated_attachment_list.py → paginated_concise_doc_list.py} +18 -17
  40. dart/generated/models/{paginated_dartboard_list.py → paginated_concise_task_list.py} +18 -17
  41. dart/generated/models/task.py +149 -406
  42. dart/generated/models/task_create.py +113 -428
  43. dart/generated/models/task_update.py +113 -421
  44. dart/generated/models/user.py +14 -192
  45. dart/generated/models/user_space_configuration.py +147 -0
  46. dart/generated/models/view.py +39 -149
  47. dart/generated/models/wrapped_comment.py +65 -0
  48. dart/generated/models/wrapped_comment_create.py +65 -0
  49. dart/generated/models/wrapped_dartboard.py +65 -0
  50. dart/generated/models/wrapped_doc.py +65 -0
  51. dart/generated/models/wrapped_doc_create.py +65 -0
  52. dart/generated/models/wrapped_doc_update.py +65 -0
  53. dart/generated/models/wrapped_folder.py +65 -0
  54. dart/generated/models/wrapped_task.py +65 -0
  55. dart/generated/models/wrapped_task_create.py +65 -0
  56. dart/generated/models/wrapped_task_update.py +65 -0
  57. dart/generated/models/wrapped_view.py +65 -0
  58. dart/generated/types.py +4 -3
  59. dart/old.py +216 -0
  60. dart/order_manager.py +4 -15
  61. dart/webhook.py +2 -7
  62. {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/METADATA +33 -62
  63. dart_tools-0.7.1.dist-info/RECORD +71 -0
  64. {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/WHEEL +1 -1
  65. dart/generated/api/attachments/__init__.py +0 -0
  66. dart/generated/api/comments/__init__.py +0 -0
  67. dart/generated/api/dartboards/__init__.py +0 -0
  68. dart/generated/api/dartboards/dartboards_list.py +0 -286
  69. dart/generated/api/dashboards/__init__.py +0 -0
  70. dart/generated/api/dashboards/dashboards_list.py +0 -199
  71. dart/generated/api/docs/__init__.py +0 -0
  72. dart/generated/api/docs/docs_list.py +0 -387
  73. dart/generated/api/folders/__init__.py +0 -0
  74. dart/generated/api/folders/folders_list.py +0 -249
  75. dart/generated/api/form_fields/__init__.py +0 -0
  76. dart/generated/api/form_fields/form_fields_list.py +0 -169
  77. dart/generated/api/forms/__init__.py +0 -0
  78. dart/generated/api/layouts/__init__.py +0 -0
  79. dart/generated/api/links/__init__.py +0 -0
  80. dart/generated/api/links/links_list.py +0 -169
  81. dart/generated/api/options/__init__.py +0 -0
  82. dart/generated/api/options/options_list.py +0 -244
  83. dart/generated/api/properties/__init__.py +0 -0
  84. dart/generated/api/properties/properties_list.py +0 -219
  85. dart/generated/api/reactions/__init__.py +0 -0
  86. dart/generated/api/reactions/reactions_list.py +0 -169
  87. dart/generated/api/relationship_kinds/__init__.py +0 -0
  88. dart/generated/api/relationship_kinds/relationship_kinds_list.py +0 -169
  89. dart/generated/api/relationships/__init__.py +0 -0
  90. dart/generated/api/relationships/relationships_list.py +0 -169
  91. dart/generated/api/spaces/__init__.py +0 -0
  92. dart/generated/api/spaces/spaces_list.py +0 -229
  93. dart/generated/api/statuses/__init__.py +0 -0
  94. dart/generated/api/statuses/statuses_list.py +0 -264
  95. dart/generated/api/task_doc_relationships/__init__.py +0 -0
  96. dart/generated/api/task_doc_relationships/task_doc_relationships_list.py +0 -169
  97. dart/generated/api/task_kinds/__init__.py +0 -0
  98. dart/generated/api/task_kinds/task_kinds_list.py +0 -219
  99. dart/generated/api/tasks/__init__.py +0 -0
  100. dart/generated/api/tasks/tasks_list.py +0 -461
  101. dart/generated/api/tenants/__init__.py +0 -0
  102. dart/generated/api/transactions/__init__.py +0 -0
  103. dart/generated/api/user_dartboard_layouts/__init__.py +0 -0
  104. dart/generated/api/user_dartboard_layouts/user_dartboard_layouts_list.py +0 -169
  105. dart/generated/api/user_data/__init__.py +0 -0
  106. dart/generated/api/user_data/user_data_entity_retrieve.py +0 -580
  107. dart/generated/api/users/__init__.py +0 -0
  108. dart/generated/api/users/users_list.py +0 -229
  109. dart/generated/api/views/__init__.py +0 -0
  110. dart/generated/api/views/views_list.py +0 -199
  111. dart/generated/api/webhooks/__init__.py +0 -0
  112. dart/generated/models/attachment.py +0 -112
  113. dart/generated/models/attachment_create.py +0 -121
  114. dart/generated/models/attachment_update.py +0 -125
  115. dart/generated/models/bar_chart_adtl.py +0 -98
  116. dart/generated/models/brainstorm.py +0 -149
  117. dart/generated/models/brainstorm_create.py +0 -134
  118. dart/generated/models/brainstorm_state.py +0 -10
  119. dart/generated/models/brainstorm_update.py +0 -153
  120. dart/generated/models/burn_up_chart_adtl.py +0 -103
  121. dart/generated/models/chart.py +0 -208
  122. dart/generated/models/chart_aggregation.py +0 -10
  123. dart/generated/models/chart_type.py +0 -14
  124. dart/generated/models/comment_reaction.py +0 -84
  125. dart/generated/models/comment_reaction_create.py +0 -82
  126. dart/generated/models/comment_reaction_update.py +0 -87
  127. dart/generated/models/comment_update.py +0 -148
  128. dart/generated/models/dartboard_create.py +0 -315
  129. dart/generated/models/dartboard_kind.py +0 -12
  130. dart/generated/models/dartboard_update.py +0 -317
  131. dart/generated/models/dartboards_list_kind.py +0 -12
  132. dart/generated/models/dashboard.py +0 -185
  133. dart/generated/models/dashboard_create.py +0 -171
  134. dart/generated/models/dashboard_update.py +0 -173
  135. dart/generated/models/discord_integration.py +0 -72
  136. dart/generated/models/doc_source_type.py +0 -13
  137. dart/generated/models/docs_list_o_item.py +0 -15
  138. dart/generated/models/entity_name.py +0 -22
  139. dart/generated/models/event.py +0 -420
  140. dart/generated/models/event_actor.py +0 -19
  141. dart/generated/models/event_create.py +0 -158
  142. dart/generated/models/event_kind.py +0 -88
  143. dart/generated/models/event_subscription.py +0 -74
  144. dart/generated/models/event_subscription_update.py +0 -173
  145. dart/generated/models/filter_applicability.py +0 -22
  146. dart/generated/models/filter_assignee.py +0 -116
  147. dart/generated/models/filter_connector.py +0 -9
  148. dart/generated/models/filter_group.py +0 -112
  149. dart/generated/models/filter_search.py +0 -82
  150. dart/generated/models/filter_set.py +0 -116
  151. dart/generated/models/folder_create.py +0 -150
  152. dart/generated/models/folder_kind.py +0 -10
  153. dart/generated/models/folder_update.py +0 -152
  154. dart/generated/models/folders_list_kind.py +0 -10
  155. dart/generated/models/form.py +0 -147
  156. dart/generated/models/form_create.py +0 -141
  157. dart/generated/models/form_field.py +0 -144
  158. dart/generated/models/form_field_create.py +0 -129
  159. dart/generated/models/form_field_update.py +0 -132
  160. dart/generated/models/form_update.py +0 -142
  161. dart/generated/models/github_integration.py +0 -163
  162. dart/generated/models/github_integration_tenant_extension_status.py +0 -11
  163. dart/generated/models/google_data.py +0 -94
  164. dart/generated/models/icon_kind.py +0 -10
  165. dart/generated/models/layout.py +0 -167
  166. dart/generated/models/layout_config.py +0 -70
  167. dart/generated/models/layout_create.py +0 -130
  168. dart/generated/models/layout_kind.py +0 -11
  169. dart/generated/models/layout_kind_config_map.py +0 -56
  170. dart/generated/models/layout_update.py +0 -130
  171. dart/generated/models/line_chart_adtl.py +0 -72
  172. dart/generated/models/models_response.py +0 -671
  173. dart/generated/models/notification.py +0 -120
  174. dart/generated/models/notification_update.py +0 -100
  175. dart/generated/models/notion_integration.py +0 -90
  176. dart/generated/models/notion_integration_tenant_extension_status.py +0 -10
  177. dart/generated/models/number_chart_adtl.py +0 -77
  178. dart/generated/models/operation.py +0 -874
  179. dart/generated/models/operation_kind.py +0 -12
  180. dart/generated/models/operation_model_kind.py +0 -36
  181. dart/generated/models/option.py +0 -118
  182. dart/generated/models/option_create.py +0 -105
  183. dart/generated/models/option_update.py +0 -107
  184. dart/generated/models/paginated_comment_list.py +0 -122
  185. dart/generated/models/paginated_comment_reaction_list.py +0 -122
  186. dart/generated/models/paginated_dashboard_list.py +0 -122
  187. dart/generated/models/paginated_doc_list.py +0 -122
  188. dart/generated/models/paginated_folder_list.py +0 -122
  189. dart/generated/models/paginated_form_field_list.py +0 -122
  190. dart/generated/models/paginated_form_list.py +0 -122
  191. dart/generated/models/paginated_layout_list.py +0 -122
  192. dart/generated/models/paginated_option_list.py +0 -122
  193. dart/generated/models/paginated_property_list.py +0 -122
  194. dart/generated/models/paginated_relationship_kind_list.py +0 -122
  195. dart/generated/models/paginated_relationship_list.py +0 -122
  196. dart/generated/models/paginated_space_list.py +0 -122
  197. dart/generated/models/paginated_status_list.py +0 -122
  198. dart/generated/models/paginated_task_doc_relationship_list.py +0 -122
  199. dart/generated/models/paginated_task_kind_list.py +0 -122
  200. dart/generated/models/paginated_task_link_list.py +0 -122
  201. dart/generated/models/paginated_task_list.py +0 -122
  202. dart/generated/models/paginated_tenant_list.py +0 -122
  203. dart/generated/models/paginated_user_dartboard_layout_list.py +0 -122
  204. dart/generated/models/paginated_user_list.py +0 -122
  205. dart/generated/models/paginated_view_list.py +0 -122
  206. dart/generated/models/paginated_webhook_list.py +0 -122
  207. dart/generated/models/pie_chart_adtl.py +0 -69
  208. dart/generated/models/pie_chart_display_metric.py +0 -9
  209. dart/generated/models/properties_list_kind.py +0 -32
  210. dart/generated/models/property_.py +0 -153
  211. dart/generated/models/property_create.py +0 -137
  212. dart/generated/models/property_kind.py +0 -32
  213. dart/generated/models/property_update.py +0 -146
  214. dart/generated/models/relationship.py +0 -74
  215. dart/generated/models/relationship_create.py +0 -93
  216. dart/generated/models/relationship_kind.py +0 -123
  217. dart/generated/models/relationship_kind_create.py +0 -117
  218. dart/generated/models/relationship_kind_kind.py +0 -12
  219. dart/generated/models/relationship_kind_update.py +0 -119
  220. dart/generated/models/report_kind.py +0 -9
  221. dart/generated/models/request_body.py +0 -83
  222. dart/generated/models/response_body.py +0 -72
  223. dart/generated/models/saml_config.py +0 -77
  224. dart/generated/models/saml_config_tenant_extension_status.py +0 -9
  225. dart/generated/models/slack_integration.py +0 -90
  226. dart/generated/models/slack_integration_tenant_extension_status.py +0 -10
  227. dart/generated/models/sort.py +0 -66
  228. dart/generated/models/space.py +0 -311
  229. dart/generated/models/space_create.py +0 -340
  230. dart/generated/models/space_kind.py +0 -10
  231. dart/generated/models/space_update.py +0 -341
  232. dart/generated/models/sprint_mode.py +0 -9
  233. dart/generated/models/status.py +0 -141
  234. dart/generated/models/status_create.py +0 -125
  235. dart/generated/models/status_kind.py +0 -12
  236. dart/generated/models/status_update.py +0 -135
  237. dart/generated/models/statuses_list_kind.py +0 -12
  238. dart/generated/models/subscription.py +0 -9
  239. dart/generated/models/subtask_display_mode.py +0 -10
  240. dart/generated/models/summary_statistic_kind.py +0 -14
  241. dart/generated/models/table_chart_adtl.py +0 -98
  242. dart/generated/models/task_detail_mode.py +0 -10
  243. dart/generated/models/task_doc_relationship.py +0 -96
  244. dart/generated/models/task_doc_relationship_create.py +0 -74
  245. dart/generated/models/task_kind.py +0 -149
  246. dart/generated/models/task_kind_create.py +0 -144
  247. dart/generated/models/task_kind_kind.py +0 -9
  248. dart/generated/models/task_kind_update.py +0 -153
  249. dart/generated/models/task_kinds_list_kind.py +0 -9
  250. dart/generated/models/task_link.py +0 -131
  251. dart/generated/models/task_link_create.py +0 -152
  252. dart/generated/models/task_link_kind.py +0 -19
  253. dart/generated/models/task_link_update.py +0 -155
  254. dart/generated/models/task_notion_document.py +0 -196
  255. dart/generated/models/task_notion_document_block_children_map_type_0.py +0 -43
  256. dart/generated/models/task_notion_document_block_map_type_0.py +0 -43
  257. dart/generated/models/task_notion_document_page_map_type_0.py +0 -43
  258. dart/generated/models/task_properties.py +0 -43
  259. dart/generated/models/task_source_type.py +0 -31
  260. dart/generated/models/tenant.py +0 -386
  261. dart/generated/models/tenant_update.py +0 -166
  262. dart/generated/models/theme.py +0 -10
  263. dart/generated/models/transaction.py +0 -158
  264. dart/generated/models/transaction_kind.py +0 -74
  265. dart/generated/models/transaction_response.py +0 -96
  266. dart/generated/models/user_dartboard_layout.py +0 -66
  267. dart/generated/models/user_dartboard_layout_create.py +0 -74
  268. dart/generated/models/user_data_entity_retrieve_entity_kind.py +0 -32
  269. dart/generated/models/user_role.py +0 -12
  270. dart/generated/models/user_status.py +0 -12
  271. dart/generated/models/user_update.py +0 -210
  272. dart/generated/models/validation_error_response.py +0 -64
  273. dart/generated/models/validation_error_response_items.py +0 -43
  274. dart/generated/models/view_create.py +0 -213
  275. dart/generated/models/view_kind.py +0 -11
  276. dart/generated/models/view_update.py +0 -215
  277. dart/generated/models/webhook.py +0 -96
  278. dart/generated/models/webhook_create.py +0 -77
  279. dart/generated/models/webhook_update.py +0 -78
  280. dart/generated/models/zapier_integration.py +0 -66
  281. dart_tools-0.6.16.dist-info/RECORD +0 -257
  282. dart_tools-0.6.16.dist-info/dist/dart-tools-0.3.3.tar.gz +0 -0
  283. {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/entry_points.txt +0 -0
  284. {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info/licenses}/LICENSE +0 -0
  285. {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/top_level.txt +0 -0
dart/dart.py CHANGED
@@ -1,12 +1,11 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env uv run python3
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  """A CLI to interact with the Dart web app."""
5
5
 
6
6
  # Required for type hinting compatibility when using Python 3.9
7
7
  from __future__ import annotations
8
- from argparse import ArgumentParser
9
- from functools import wraps
8
+
10
9
  import json
11
10
  import os
12
11
  import random
@@ -15,46 +14,45 @@ import signal
15
14
  import string
16
15
  import subprocess
17
16
  import sys
17
+ from argparse import ArgumentParser
18
18
  from collections import defaultdict
19
19
  from datetime import timezone
20
+ from functools import wraps
20
21
  from importlib.metadata import version
21
- from typing import Any, Callable, NoReturn
22
+ from typing import Callable, NoReturn, TypeVar, Union
22
23
  from webbrowser import open_new_tab
23
24
 
24
25
  import dateparser
25
- from pick import pick
26
- import requests
26
+ import httpx
27
27
  import platformdirs
28
+ from pick import pick
28
29
 
29
30
  from .exception import DartException
30
- from .generated import Client
31
+ from .generated import Client, api
31
32
  from .generated.models import (
32
- Dartboard,
33
- DartboardKind,
34
- DartboardUpdate,
35
- Folder,
36
- FolderKind,
37
- FolderUpdate,
38
- Operation,
39
- OperationKind,
40
- OperationModelKind,
33
+ Comment,
34
+ CommentCreate,
35
+ ConciseTask,
36
+ Doc,
37
+ DocCreate,
38
+ DocUpdate,
39
+ PaginatedConciseDocList,
40
+ PaginatedConciseTaskList,
41
41
  Priority,
42
- PropertyKind,
43
- RequestBody,
44
- SpaceKind,
45
- StatusKind,
46
- SprintMode,
47
42
  Task,
48
43
  TaskCreate,
49
- TaskSourceType,
50
44
  TaskUpdate,
51
- Transaction,
52
- TransactionKind,
45
+ UserSpaceConfiguration,
46
+ WrappedComment,
47
+ WrappedCommentCreate,
48
+ WrappedDoc,
49
+ WrappedDocCreate,
50
+ WrappedDocUpdate,
51
+ WrappedTask,
52
+ WrappedTaskCreate,
53
+ WrappedTaskUpdate,
53
54
  )
54
- from .generated.api.dartboards import dartboards_list
55
- from .generated.api.folders import folders_list
56
- from .generated.api.transactions import transactions_create
57
- from .order_manager import get_orders_between
55
+ from .generated.types import UNSET, Response, Unset
58
56
 
59
57
  _APP = "dart-tools"
60
58
  _PROG = "dart"
@@ -64,52 +62,74 @@ _STAG_HOST = "https://stag.itsdart.com"
64
62
  _DEV_HOST = "http://localhost:5173"
65
63
  _HOST_MAP = {"prod": _PROD_HOST, "stag": _STAG_HOST, "dev": _DEV_HOST}
66
64
 
67
- _VERSION_CMD = "--version"
68
- _SET_HOST_CMD = "sethost"
65
+ # Service commands
69
66
  _LOGIN_CMD = "login"
70
- _CREATE_TASK_CMD = "createtask"
71
- _UPDATE_TASK_CMD = "updatetask"
72
- _BEGIN_TASK_CMD = "begintask"
67
+ _SET_HOST_CMD = "host-set"
68
+ _VERSION_CMD = "--version"
69
+ # Task commands
70
+ _CREATE_TASK_CMD = "task-create"
71
+ _UPDATE_TASK_CMD = "task-update"
72
+ _DELETE_TASK_CMD = "task-delete"
73
+ _BEGIN_TASK_CMD = "task-begin"
74
+ # Doc commands
75
+ _CREATE_DOC_CMD = "doc-create"
76
+ _UPDATE_DOC_CMD = "doc-update"
77
+ _DELETE_DOC_CMD = "doc-delete"
78
+ # Comment commands
79
+ _CREATE_COMMENT_CMD = "comment-create"
73
80
 
74
81
  _PROFILE_SETTINGS_URL_FRAG = "/?settings=account"
75
- _ROOT_API_URL_FRAG = "/api/v0"
76
- _USER_STATUS_URL_FRAG = _ROOT_API_URL_FRAG + "/user-status"
77
- _USER_DATA_URL_FRAG = _ROOT_API_URL_FRAG + "/user-data?mode=auto"
78
- _COPY_BRANCH_URL_FRAG = _ROOT_API_URL_FRAG + "/vcs/copy-branch-link"
79
- _REPLICATE_SPACE_URL_FRAG_FMT = _ROOT_API_URL_FRAG + "/spaces/replicate/{duid}"
80
- _REPLICATE_DARTBOARD_URL_FRAG_FMT = _ROOT_API_URL_FRAG + "/dartboards/replicate/{duid}"
82
+ _ROOT_PRIVATE_API_URL_FRAG = "/api/v0"
83
+ _ROOT_PUBLIC_API_URL_FRAG = f"{_ROOT_PRIVATE_API_URL_FRAG}/public"
84
+ _COPY_BRANCH_URL_FRAG = "/vcs/copy-branch-link"
81
85
 
82
86
  _AUTH_TOKEN_ENVVAR_KEY = "DART_TOKEN"
83
87
  _CONFIG_FPATH = platformdirs.user_config_path(_APP)
84
- _CLIENT_DUID_KEY = "clientDuid"
88
+ _CLIENT_ID_KEY = "clientId"
85
89
  _HOST_KEY = "host"
86
90
  _HOSTS_KEY = "hosts"
87
91
  _AUTH_TOKEN_KEY = "authToken"
88
92
 
89
- _DUID_CHARS = string.ascii_lowercase + string.ascii_uppercase + string.digits
93
+ _ID_CHARS = string.ascii_lowercase + string.ascii_uppercase + string.digits
90
94
  _NON_ALPHANUM_RE = re.compile(r"[^a-zA-Z0-9-]+")
91
95
  _REPEATED_DASH_RE = re.compile(r"-{2,}")
92
- _PRIORITY_MAP = {
96
+ _PRIORITY_MAP: dict[int, str] = {
93
97
  0: Priority.CRITICAL,
94
98
  1: Priority.HIGH,
95
99
  2: Priority.MEDIUM,
96
100
  3: Priority.LOW,
97
101
  }
98
102
  _SIZES = {1, 2, 3, 5, 8}
99
- _COMPLETED_STATUS_KINDS = {"Finished", "Canceled"}
100
103
 
101
104
  _VERSION = version(_APP)
102
105
  _AUTH_TOKEN_ENVVAR = os.environ.get(_AUTH_TOKEN_ENVVAR_KEY)
103
106
 
107
+
108
+ def _get_help_text(fn: Callable) -> str:
109
+ if fn.__doc__ is None:
110
+ raise ValueError(f"Function {fn.__name__} has no docstring.")
111
+ return fn.__doc__.split("\n")[0].lower()
112
+
113
+
114
+ _HELP_TEXT_TO_COMMAND = {
115
+ _CREATE_TASK_CMD: _get_help_text(api.create_task.sync_detailed),
116
+ _UPDATE_TASK_CMD: _get_help_text(api.update_task.sync_detailed),
117
+ _DELETE_TASK_CMD: _get_help_text(api.delete_task.sync_detailed),
118
+ _CREATE_DOC_CMD: _get_help_text(api.create_doc.sync_detailed),
119
+ _UPDATE_DOC_CMD: _get_help_text(api.update_doc.sync_detailed),
120
+ _DELETE_DOC_CMD: _get_help_text(api.delete_doc.sync_detailed),
121
+ _CREATE_COMMENT_CMD: _get_help_text(api.create_comment.sync_detailed),
122
+ }
123
+
104
124
  _is_cli = False
105
125
 
106
126
 
107
127
  # TODO dedupe these functions with other usages elsewhere
108
- def _make_duid() -> str:
109
- return "".join(random.choices(_DUID_CHARS, k=12))
128
+ def _make_id() -> str:
129
+ return "".join(random.choices(_ID_CHARS, k=12))
110
130
 
111
131
 
112
- def trim_slug_str(s: str, length: int, max_under: int | None = None) -> str:
132
+ def trim_slug_str(s: str, length: int, max_under: Union[int, None] = None) -> str:
113
133
  max_under = max_under if max_under is not None else length // 6
114
134
  if len(s) <= length:
115
135
  return s
@@ -119,37 +139,17 @@ def trim_slug_str(s: str, length: int, max_under: int | None = None) -> str:
119
139
  return s[:length]
120
140
 
121
141
 
122
- def slugify_str(s: str, lower: bool = False, trim_kwargs: dict | None = None) -> str:
142
+ def slugify_str(s: str, lower: bool = False, trim_kwargs: Union[dict, None] = None) -> str:
123
143
  lowered = s.lower() if lower else s
124
144
  formatted = _NON_ALPHANUM_RE.sub("-", lowered.replace("'", ""))
125
145
  formatted = _REPEATED_DASH_RE.sub("-", formatted).strip("-")
126
- return (
127
- trim_slug_str(formatted, **trim_kwargs)
128
- if trim_kwargs is not None
129
- else formatted
130
- )
146
+ return trim_slug_str(formatted, **trim_kwargs) if trim_kwargs is not None else formatted
131
147
 
132
148
 
133
149
  def _run_cmd(cmd: str) -> str:
134
150
  return subprocess.check_output(cmd, shell=True).decode()
135
151
 
136
152
 
137
- def _get_space_url(host: str, duid: str) -> str:
138
- return f"{host}/s/{duid}"
139
-
140
-
141
- def _get_dartboard_url(host: str, duid: str) -> str:
142
- return f"{host}/d/{duid}"
143
-
144
-
145
- def _get_task_url(host: str, duid: str) -> str:
146
- return f"{host}/t/{duid}"
147
-
148
-
149
- def _get_folder_url(host: str, duid: str) -> str:
150
- return f"{host}/f/{duid}"
151
-
152
-
153
153
  def _suppress_exception(fn: Callable) -> Callable:
154
154
  @wraps(fn)
155
155
  def wrapper(*args, **kwargs):
@@ -177,6 +177,37 @@ def _log(s: str) -> None:
177
177
  print(s)
178
178
 
179
179
 
180
+ T = TypeVar("T")
181
+
182
+
183
+ def _get_response_parsed(response: Response[T], not_found_message="Not found.") -> T:
184
+ if response.parsed is not None:
185
+ return response.parsed
186
+ if response.status_code in {401, 403}:
187
+ _auth_failure_exit()
188
+ elif response.status_code == 404:
189
+ _dart_exit(not_found_message)
190
+ try:
191
+ response_content = json.loads(response.content)
192
+ error_message = response_content.get("detail") or " ".join(response_content.get("errors", []))
193
+ _dart_exit(error_message)
194
+ except (json.JSONDecodeError, AttributeError):
195
+ _unknown_failure_exit()
196
+
197
+
198
+ def _handle_request_errors(fn: Callable) -> Callable:
199
+ @wraps(fn)
200
+ def wrapper(*args, **kwargs):
201
+ try:
202
+ return fn(*args, **kwargs)
203
+ except DartException as ex:
204
+ _dart_exit(ex)
205
+ except (httpx.TimeoutException, httpx.RequestError, httpx.ConnectError) as ex:
206
+ _dart_exit(f"Failed to execute API call: {ex}.")
207
+
208
+ return wrapper
209
+
210
+
180
211
  class _Config:
181
212
  def __init__(self):
182
213
  self._content = {}
@@ -187,14 +218,14 @@ class _Config:
187
218
  except OSError:
188
219
  pass
189
220
  self._content = {
190
- _CLIENT_DUID_KEY: _make_duid(),
221
+ _CLIENT_ID_KEY: _make_id(),
191
222
  _HOST_KEY: _PROD_HOST,
192
223
  _HOSTS_KEY: {},
193
224
  } | self._content
194
225
  self._content[_HOSTS_KEY] = defaultdict(dict, self._content[_HOSTS_KEY])
195
226
  self._write()
196
227
 
197
- def _write(self):
228
+ def _write(self) -> None:
198
229
  try:
199
230
  with open(_CONFIG_FPATH, "w+", encoding="UTF-8") as fout:
200
231
  json.dump(self._content, fout, indent=2)
@@ -202,176 +233,140 @@ class _Config:
202
233
  pass
203
234
 
204
235
  @property
205
- def client_duid(self):
206
- return self._content[_CLIENT_DUID_KEY]
236
+ def client_id(self) -> str:
237
+ return self._content[_CLIENT_ID_KEY]
207
238
 
208
239
  @property
209
- def host(self):
240
+ def host(self) -> str:
210
241
  return self._content[_HOST_KEY]
211
242
 
212
243
  @host.setter
213
- def host(self, v):
244
+ def host(self, v: str) -> None:
214
245
  self._content[_HOST_KEY] = v
215
246
  self._write()
216
247
 
217
- def get(self, k):
248
+ def get(self, k: str) -> str | None:
218
249
  return self._content[_HOSTS_KEY][self.host].get(k)
219
250
 
220
- def set(self, k, v):
251
+ def set(self, k: str, v: str) -> None:
221
252
  self._content[_HOSTS_KEY][self.host][k] = v
222
253
  self._write()
223
254
 
224
255
 
225
- class _Session:
256
+ class Dart:
226
257
  def __init__(self, config=None):
227
258
  self._config = config or _Config()
228
- self._session = requests.Session()
259
+ self._init_clients()
260
+
261
+ def _init_clients(self) -> None:
262
+ self._private_api = Client(
263
+ base_url=self.get_base_url() + _ROOT_PRIVATE_API_URL_FRAG,
264
+ headers=self.get_headers(),
265
+ )
266
+ self._public_api = Client(
267
+ base_url=self.get_base_url() + _ROOT_PUBLIC_API_URL_FRAG,
268
+ headers=self.get_headers(),
269
+ )
229
270
 
230
- def get_base_url(self):
271
+ def get_base_url(self) -> str:
231
272
  return self._config.host
232
273
 
233
- def get_client_duid(self):
234
- return self._config.client_duid
274
+ def get_client_id(self) -> str:
275
+ return self._config.client_id
235
276
 
236
- def get_auth_token(self):
277
+ def get_auth_token(self) -> Union[str, None]:
237
278
  result = self._config.get(_AUTH_TOKEN_KEY)
238
279
  if result is not None:
239
280
  return result
240
281
  return _AUTH_TOKEN_ENVVAR
241
282
 
242
- def get_headers(self):
283
+ def get_headers(self) -> dict[str, str]:
243
284
  result = {
244
285
  "Origin": self._config.host,
245
- "client-duid": self.get_client_duid(),
286
+ "client-duid": self.get_client_id(),
246
287
  }
247
288
  if (auth_token := self.get_auth_token()) is not None:
248
289
  result["Authorization"] = f"Bearer {auth_token}"
249
290
  return result
250
291
 
251
- def get(self, url_frag, *args, **kwargs):
252
- kwargs["headers"] = self.get_headers() | kwargs.get("headers", {})
253
- return self._session.get(self._config.host + url_frag, *args, **kwargs)
254
-
255
- def post(self, url_frag, *args, **kwargs):
256
- kwargs["headers"] = self.get_headers() | kwargs.get("headers", {})
257
- result = self._session.post(self._config.host + url_frag, *args, **kwargs)
258
- if result.status_code != 403:
259
- return result
260
- kwargs["headers"] = self.get_headers() | kwargs.get("headers", {})
261
- return self._session.post(self._config.host + url_frag, *args, **kwargs)
262
-
263
-
264
- class Dart(Client):
265
- def __init__(self, session=None):
266
- self._session = session or _Session()
267
- super().__init__(
268
- base_url=self._session.get_base_url(),
269
- headers=self._session.get_headers(),
270
- )
271
-
272
- def transact(self, operations: list[Operation], kind: TransactionKind):
273
- transaction = Transaction(
274
- duid=_make_duid(),
275
- kind=kind,
276
- operations=operations,
277
- )
278
- request_body = RequestBody(
279
- client_duid=self._session.get_client_duid(),
280
- items=[transaction],
281
- )
282
- return transactions_create.sync(
283
- client=self,
284
- body=request_body,
285
- )
286
-
287
-
288
- class UserBundle:
289
- def __init__(self, session):
290
- _log("Loading active tasks")
291
- response = session.get(_USER_DATA_URL_FRAG)
292
- _check_request_response_and_maybe_exit(response)
293
- self._raw = response.json()
294
- if not self.is_logged_in:
295
- _auth_failure_exit()
296
-
297
- @property
298
- def is_logged_in(self):
299
- return self._raw["isLoggedIn"]
300
-
301
- @property
302
- def user(self):
303
- return self._raw["user"]
304
-
305
- @property
306
- def users(self):
307
- return self._raw["users"]
308
-
309
- @property
310
- def properties(self):
311
- return self._raw["properties"]
312
-
313
- @property
314
- def task_kinds(self):
315
- return self._raw["taskKinds"]
316
-
317
- @property
318
- def default_dartboard(self):
319
- space = next(
320
- (e for e in self.spaces if e["kind"] == SpaceKind.WORKSPACE),
321
- self.spaces[0],
322
- )
323
- space_duid = space["duid"]
324
- dartboard_kind = (
325
- DartboardKind.ACTIVE
326
- if space["sprintMode"] == SprintMode.ANBA
327
- else DartboardKind.CUSTOM
328
- )
329
- return next(
330
- e
331
- for e in self.dartboards
332
- if e["spaceDuid"] == space_duid and e["kind"] == dartboard_kind
333
- )
334
-
335
- @property
336
- def default_statuses(self):
337
- default_status_property_duid = next(
338
- e["duid"]
339
- for e in self.properties
340
- if e["kind"] == PropertyKind.DEFAULT_STATUS
341
- )
342
- return [
343
- e
344
- for e in self._raw["statuses"]
345
- if e["propertyDuid"] == default_status_property_duid
346
- ]
347
-
348
- @property
349
- def default_tags(self):
350
- default_tags_property_duid = next(
351
- e["duid"] for e in self.properties if e["kind"] == PropertyKind.DEFAULT_TAGS
352
- )
353
- return [
354
- e
355
- for e in self._raw["options"]
356
- if e["propertyDuid"] == default_tags_property_duid
357
- ]
358
-
359
- @property
360
- def spaces(self):
361
- return self._raw["spaces"]
362
-
363
- @property
364
- def dartboards(self):
365
- return self._raw["dartboards"]
292
+ def is_logged_in(self) -> bool:
293
+ self._init_clients()
294
+ try:
295
+ config = api.get_config.sync(client=self._public_api)
296
+ if config is None:
297
+ return False
298
+ except:
299
+ return False
300
+ return True
366
301
 
367
- @property
368
- def tasks(self):
369
- return self._raw["tasks"]
302
+ @_handle_request_errors
303
+ def get_config(self) -> UserSpaceConfiguration:
304
+ response = api.get_config.sync_detailed(client=self._public_api)
305
+ return _get_response_parsed(response)
306
+
307
+ @_handle_request_errors
308
+ def create_task(self, body: WrappedTaskCreate) -> WrappedTask:
309
+ response = api.create_task.sync_detailed(client=self._public_api, body=body)
310
+ return _get_response_parsed(response)
311
+
312
+ @_handle_request_errors
313
+ def retrieve_task(self, id: str) -> WrappedTask:
314
+ response = api.retrieve_task.sync_detailed(id, client=self._public_api)
315
+ return _get_response_parsed(response, not_found_message=f"Task with ID {id} not found.")
316
+
317
+ @_handle_request_errors
318
+ def update_task(self, id: str, body: WrappedTaskUpdate) -> WrappedTask:
319
+ response = api.update_task.sync_detailed(id, client=self._public_api, body=body)
320
+ return _get_response_parsed(response, not_found_message=f"Task with ID {id} not found.")
321
+
322
+ @_handle_request_errors
323
+ def delete_task(self, id: str) -> WrappedTask:
324
+ response = api.delete_task.sync_detailed(id, client=self._public_api)
325
+ return _get_response_parsed(response, not_found_message=f"Task with ID {id} not found.")
326
+
327
+ @_handle_request_errors
328
+ def list_tasks(self, **kwargs) -> PaginatedConciseTaskList:
329
+ response = api.list_tasks.sync_detailed(client=self._public_api, **kwargs)
330
+ return _get_response_parsed(response)
331
+
332
+ @_handle_request_errors
333
+ def create_comment(self, body: WrappedCommentCreate) -> WrappedComment:
334
+ response = api.create_comment.sync_detailed(client=self._public_api, body=body)
335
+ return _get_response_parsed(response)
336
+
337
+ @_handle_request_errors
338
+ def create_doc(self, body: WrappedDocCreate) -> WrappedDoc:
339
+ response = api.create_doc.sync_detailed(client=self._public_api, body=body)
340
+ return _get_response_parsed(response)
341
+
342
+ @_handle_request_errors
343
+ def retrieve_doc(self, id: str) -> WrappedDoc:
344
+ response = api.retrieve_doc.sync_detailed(id, client=self._public_api)
345
+ return _get_response_parsed(response, not_found_message=f"Doc with ID {id} not found.")
346
+
347
+ @_handle_request_errors
348
+ def update_doc(self, id: str, body: WrappedDocUpdate) -> WrappedDoc:
349
+ response = api.update_doc.sync_detailed(id, client=self._public_api, body=body)
350
+ return _get_response_parsed(response, not_found_message=f"Doc with ID {id} not found.")
351
+
352
+ @_handle_request_errors
353
+ def delete_doc(self, id: str) -> WrappedDoc:
354
+ response = api.delete_doc.sync_detailed(id, client=self._public_api)
355
+ return _get_response_parsed(response, not_found_message=f"Doc with ID {id} not found.")
356
+
357
+ @_handle_request_errors
358
+ def list_docs(self, **kwargs) -> PaginatedConciseDocList:
359
+ response = api.list_docs.sync_detailed(client=self._public_api, **kwargs)
360
+ return _get_response_parsed(response)
361
+
362
+ @_handle_request_errors
363
+ def copy_branch_link(self, id: str) -> None:
364
+ self._private_api.get_httpx_client().post(_COPY_BRANCH_URL_FRAG, json={"duid": id})
370
365
 
371
366
 
372
367
  class _Git:
373
368
  @staticmethod
374
- def _cmd_succeeds(cmd):
369
+ def _cmd_succeeds(cmd: str) -> bool:
375
370
  try:
376
371
  _run_cmd(f"{cmd} 2>&1")
377
372
  except subprocess.CalledProcessError as ex:
@@ -381,29 +376,29 @@ class _Git:
381
376
  return True
382
377
 
383
378
  @staticmethod
384
- def make_task_name(email, task):
379
+ def make_task_name(email: str, task: ConciseTask | Task) -> str:
385
380
  username = slugify_str(email.split("@")[0], lower=True)
386
381
  title = slugify_str(task.title, lower=True)
387
- return trim_slug_str(f"{username}/{task.duid}-{title}", length=60)
382
+ return trim_slug_str(f"{username}/{task.id}-{title}", length=60)
388
383
 
389
384
  @staticmethod
390
- def get_current_branch():
385
+ def get_current_branch() -> str:
391
386
  return _run_cmd("git rev-parse --abbrev-ref HEAD").strip()
392
387
 
393
388
  @staticmethod
394
- def ensure_in_repo():
389
+ def ensure_in_repo() -> None:
395
390
  if _Git._cmd_succeeds("git rev-parse --is-inside-work-tree"):
396
391
  return
397
392
  _dart_exit("You are not in a git repo.")
398
393
 
399
394
  @staticmethod
400
- def ensure_no_unstaged_changes():
395
+ def ensure_no_unstaged_changes() -> None:
401
396
  if _run_cmd("git status --porcelain") == "":
402
397
  return
403
398
  _dart_exit("You have uncommitted changes. Please commit or stash them.")
404
399
 
405
400
  @staticmethod
406
- def ensure_on_main_or_intended():
401
+ def ensure_on_main_or_intended() -> None:
407
402
  branch = _Git.get_current_branch()
408
403
  if branch == "main":
409
404
  return
@@ -419,11 +414,11 @@ class _Git:
419
414
  _run_cmd("git checkout main")
420
415
 
421
416
  @staticmethod
422
- def branch_exists(branch):
417
+ def branch_exists(branch: str) -> bool:
423
418
  return _Git._cmd_succeeds(f"git rev-parse --verify {branch}")
424
419
 
425
420
  @staticmethod
426
- def checkout_branch(branch):
421
+ def checkout_branch(branch: str) -> None:
427
422
  if _Git.branch_exists(branch):
428
423
  _run_cmd(f"git checkout {branch}")
429
424
  return
@@ -456,39 +451,17 @@ def set_host(host: str) -> bool:
456
451
 
457
452
 
458
453
  def _auth_failure_exit() -> NoReturn:
459
- _dart_exit(f"Not logged in, run\n\n {_PROG} {_LOGIN_CMD}\n\nto log in.")
454
+ _dart_exit(
455
+ f"Not logged in, run\n\n {_PROG} {_LOGIN_CMD}\n\nto log in."
456
+ if _is_cli
457
+ else "Not logged in, either run\n\n dart.login(token)\n\nor save the token into the DART_TOKEN environment variable."
458
+ )
460
459
 
461
460
 
462
461
  def _unknown_failure_exit() -> NoReturn:
463
462
  _dart_exit("Unknown failure, email\n\n support@itsdart.com\n\nfor help.")
464
463
 
465
464
 
466
- def _check_request_response_and_maybe_exit(response) -> None:
467
- try:
468
- response.raise_for_status()
469
- except requests.exceptions.HTTPError:
470
- if response.status_code in {401, 403}:
471
- _auth_failure_exit()
472
- _unknown_failure_exit()
473
-
474
-
475
- def _parse_transaction_response_and_maybe_exit(
476
- response, model_kind: str, duid: str
477
- ) -> Any:
478
- if (
479
- response is None
480
- or not hasattr(response, "results")
481
- or len(response.results) == 0
482
- or not response.results[0].success
483
- ):
484
- _unknown_failure_exit()
485
- models = getattr(response.results[0].models, f"{model_kind}s")
486
- model = next((e for e in models if e.duid == duid), None)
487
- if model is None:
488
- _unknown_failure_exit()
489
- return model
490
-
491
-
492
465
  def print_version() -> str:
493
466
  result = f"dart-tools version {_VERSION}"
494
467
  _log(result)
@@ -503,9 +476,7 @@ def print_version_update_message_maybe() -> None:
503
476
  .split("\n", maxsplit=1)[0]
504
477
  .strip()
505
478
  )
506
- if latest == _VERSION or [int(e) for e in latest.split(".")] <= [
507
- int(e) for e in _VERSION.split(".")
508
- ]:
479
+ if latest == _VERSION or [int(e) for e in latest.split(".")] <= [int(e) for e in _VERSION.split(".")]:
509
480
  return
510
481
 
511
482
  _log(
@@ -513,39 +484,31 @@ def print_version_update_message_maybe() -> None:
513
484
  )
514
485
 
515
486
 
516
- def _get_is_logged_in(session: _Session) -> bool:
517
- response = session.get(_USER_STATUS_URL_FRAG)
518
- return response.json().get("isLoggedIn", False)
519
-
520
-
521
487
  def is_logged_in(should_raise: bool = False) -> bool:
522
- session = _Session()
523
-
524
- result = _get_is_logged_in(session)
488
+ dart = Dart()
489
+ result = dart.is_logged_in()
525
490
 
526
491
  if not result and should_raise:
527
492
  _auth_failure_exit()
528
- _log(f"You are {'' if result else 'not '}logged in")
493
+ _log(f"You are{'' if result else ' not'} logged in")
529
494
  return result
530
495
 
531
496
 
532
497
  def login(token: str | None = None) -> bool:
533
498
  config = _Config()
534
- session = _Session(config)
499
+ dart = Dart(config=config)
535
500
 
536
501
  _log("Log in to Dart")
537
502
  if token is None:
538
503
  if not _is_cli:
539
504
  _dart_exit("Login failed, token is required.")
540
- _log(
541
- "Dart is opening in your browser, log in if needed and copy your authentication token from the page"
542
- )
505
+ _log("Dart is opening in your browser, log in if needed and copy your authentication token from the page")
543
506
  open_new_tab(config.host + _PROFILE_SETTINGS_URL_FRAG)
544
507
  token = input("Token: ")
545
508
 
546
509
  config.set(_AUTH_TOKEN_KEY, token)
547
510
 
548
- worked = _get_is_logged_in(session)
511
+ worked = dart.is_logged_in()
549
512
  if not worked:
550
513
  _dart_exit("Invalid token.")
551
514
 
@@ -553,507 +516,207 @@ def login(token: str | None = None) -> bool:
553
516
  return True
554
517
 
555
518
 
556
- def _begin_task(
557
- config: _Config, session: _Session, user_email: str, get_task: Callable
558
- ) -> bool:
519
+ def _begin_task(dart: Dart, email: str, task: ConciseTask | Task) -> bool:
559
520
  _Git.ensure_in_repo()
560
521
  _Git.ensure_no_unstaged_changes()
561
522
  _Git.ensure_on_main_or_intended()
562
523
 
563
- task = get_task()
564
-
565
- response = session.post(_COPY_BRANCH_URL_FRAG, json={"duid": task.duid})
566
- _check_request_response_and_maybe_exit(response)
524
+ dart = Dart()
525
+ dart.copy_branch_link(task.id)
567
526
 
568
- branch_name = _Git.make_task_name(user_email, task)
527
+ branch_name = _Git.make_task_name(email, task)
569
528
  _Git.checkout_branch(branch_name)
570
529
 
571
- _log(
572
- f"Started work on\n\n {task.title}\n {_get_task_url(config.host, task.duid)}\n"
573
- )
530
+ _log(f"Started work on\n\n {task.title}\n {task.html_url}\n")
574
531
  return True
575
532
 
576
533
 
577
534
  def begin_task() -> bool:
578
- config = _Config()
579
- session = _Session(config)
580
-
581
- user_bundle = UserBundle(session)
582
- user = user_bundle.user
583
-
584
- def _get_task():
585
- user_duid = user["duid"]
586
- active_duid = user_bundle.default_dartboard["duid"]
587
- incomplete_status_duids = {
588
- e["duid"]
589
- for e in user_bundle.default_statuses
590
- if e["kind"] not in _COMPLETED_STATUS_KINDS
591
- }
592
- filtered_tasks = [
593
- e
594
- for e in user_bundle.tasks
595
- if not e["inTrash"]
596
- and e["dartboardDuid"] == active_duid
597
- and user_duid in e["assigneeDuids"]
598
- and e["statusDuid"] in incomplete_status_duids
599
- and e["drafterDuid"] is None
600
- ]
601
- filtered_tasks.sort(key=lambda e: e["order"])
535
+ dart = Dart()
536
+ config = dart.get_config()
537
+ user = config.user
538
+ filtered_tasks = dart.list_tasks(assignee=user.email, is_completed=False).results
602
539
 
603
- if len(filtered_tasks) == 0:
604
- _dart_exit("No active, incomplete tasks found.")
540
+ if not filtered_tasks:
541
+ _dart_exit("No active, incomplete tasks found.")
605
542
 
606
- picked_idx = pick(
607
- [e["title"] for e in filtered_tasks],
608
- "Which of your active, incomplete tasks are you beginning work on?",
609
- "→",
610
- )[1]
611
- assert isinstance(picked_idx, int)
612
- return TaskCreate.from_dict(filtered_tasks[picked_idx])
543
+ picked_idx = pick(
544
+ [e.title for e in filtered_tasks],
545
+ "Which of your active, incomplete tasks are you beginning work on?",
546
+ "→",
547
+ )[1]
548
+ assert isinstance(picked_idx, int)
613
549
 
614
- _begin_task(config, session, user["email"], _get_task)
550
+ _begin_task(dart, user.email, filtered_tasks[picked_idx])
615
551
 
616
552
  _log("Done.")
617
553
  return True
618
554
 
619
555
 
556
+ def _normalize_priority(priority_int: Union[int, None, Unset]) -> Union[str, None, Unset]:
557
+ if priority_int in (None, UNSET):
558
+ return priority_int
559
+
560
+ if priority_int not in _PRIORITY_MAP:
561
+ _dart_exit(f"Invalid priority {priority_int}. Valid values are {list(_PRIORITY_MAP.keys())}.")
562
+
563
+ return _PRIORITY_MAP[priority_int]
564
+
565
+
566
+ def _get_due_at_from_str_arg(due_at_str: Union[str, None, Unset]) -> Union[str, None, Unset]:
567
+ if due_at_str in (None, UNSET):
568
+ return due_at_str
569
+
570
+ due_at = dateparser.parse(due_at_str)
571
+ if not due_at:
572
+ _dart_exit(f"Could not parse due date: {due_at_str}.")
573
+ due_at = due_at.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc).isoformat()
574
+
575
+ return due_at
576
+
577
+
620
578
  def create_task(
621
579
  title: str,
622
580
  *,
623
- dartboard_duid: str | None = None,
624
- dartboard_title: str | None = None,
625
- kind_title: str | None = None,
626
- status_title: str | None = None,
627
- assignee_emails: list[str] | None = None,
628
- tag_titles: list[str] | None = None,
629
- priority_int: int | None = None,
630
- size_int: int | None = None,
631
- due_at_str: str | None = None,
581
+ dartboard_title: Union[str, Unset] = UNSET,
582
+ status_title: Union[str, Unset] = UNSET,
583
+ assignee_emails: Union[list[str], Unset] = UNSET,
584
+ tag_titles: Union[list[str], Unset] = UNSET,
585
+ priority_int: Union[int, None, Unset] = UNSET,
586
+ size_int: Union[int, None, Unset] = UNSET,
587
+ due_at_str: Union[str, None, Unset] = UNSET,
632
588
  should_begin: bool = False,
633
589
  ) -> Task:
634
- config = _Config()
635
- session = _Session(config)
636
- dart = Dart(session)
637
-
638
- user_bundle = UserBundle(session)
639
-
640
- user = user_bundle.user
641
- user_duid = user["duid"]
642
-
643
- dartboards = user_bundle.dartboards
644
- if dartboard_duid is None:
645
- if dartboard_title is not None:
646
- dartboard_title_norm = dartboard_title.strip().lower()
647
- dartboard = next(
648
- (
649
- e
650
- for e in dartboards
651
- if dartboard_title_norm in {e["title"].lower(), e["kind"].lower()}
652
- ),
653
- None,
654
- )
655
- if dartboard is None:
656
- _dart_exit(f"No dartboard found with title '{dartboard_title}'.")
657
- else:
658
- dartboard = user_bundle.default_dartboard
659
- dartboard_duid = dartboard["duid"]
660
-
661
- orders = [
662
- e["order"] for e in user_bundle.tasks if e["dartboardDuid"] == dartboard_duid
663
- ]
664
- first_order = min(orders) if len(orders) > 0 else None
665
- order = get_orders_between(None, first_order, 1)[0]
666
-
667
- kinds = user_bundle.task_kinds
668
- if kind_title is not None:
669
- kind_title_norm = kind_title.strip().lower()
670
- kind = next((e for e in kinds if e["title"].lower() == kind_title_norm), None)
671
- if kind is None:
672
- _dart_exit(f"No status found with title '{status_title}'.")
673
- else:
674
- kind = next(e for e in kinds if e["locked"])
675
- kind_duid = kind["duid"]
676
-
677
- statuses = user_bundle.default_statuses
678
- if status_title is not None:
679
- status_title_norm = status_title.strip().lower()
680
- status = next(
681
- (e for e in statuses if e["title"].lower() == status_title_norm), None
682
- )
683
- if status is None:
684
- _dart_exit(f"No status found with title '{status_title}'.")
685
- else:
686
- status = next(
687
- e for e in statuses if e["kind"] == StatusKind.UNSTARTED and e["locked"]
688
- )
689
- status_duid = status["duid"]
690
-
691
- users = user_bundle.users
692
- user_emails_to_duids = {e["email"]: e["duid"] for e in users}
693
- assignee_duids = []
694
- subscriber_duids = []
695
- if assignee_emails is not None:
696
- for assignee_email in assignee_emails:
697
- assignee_email_norm = assignee_email.strip().lower()
698
- if assignee_email_norm not in user_emails_to_duids:
699
- _dart_exit(f"No user found with email '{assignee_email}'.")
700
- assignee_duids.append(user_emails_to_duids[assignee_email_norm])
701
- subscriber_duids.append(user_emails_to_duids[assignee_email_norm])
702
- else:
703
- assignee_duids.append(user_duid)
704
- assignee_duids = list(set(assignee_duids))
705
- subscriber_duids.append(user_duid)
706
- subscriber_duids = list(set(subscriber_duids))
707
-
708
- tags = user_bundle.default_tags
709
- tag_titles_to_duids = {e["title"]: e["duid"] for e in tags}
710
- tag_duids = []
711
- if tag_titles is not None:
712
- for tag_title in tag_titles:
713
- tag_title_norm = tag_title.strip().lower()
714
- if tag_title_norm not in tag_titles_to_duids:
715
- _dart_exit(f"No tag found with title '{tag_title}'.")
716
- tag_duids.append(tag_titles_to_duids[tag_title_norm])
717
-
718
- priority = None
719
- if priority_int is not None:
720
- priority = _PRIORITY_MAP[priority_int]
721
-
722
- size = size_int
723
-
724
- due_at = None
725
- if due_at_str is not None:
726
- due_at = dateparser.parse(due_at_str)
727
- if due_at is None:
728
- _dart_exit(f"Could not parse due date '{due_at_str}'.")
729
- due_at = due_at.replace(
730
- hour=9, minute=0, second=0, microsecond=0, tzinfo=timezone.utc
590
+ dart = Dart()
591
+ task_create = WrappedTaskCreate(
592
+ item=TaskCreate(
593
+ title,
594
+ dartboard=dartboard_title,
595
+ status=status_title,
596
+ assignees=assignee_emails if assignee_emails is not None else UNSET,
597
+ tags=tag_titles if tag_titles is not None else UNSET,
598
+ priority=_normalize_priority(priority_int),
599
+ size=size_int,
600
+ due_at=_get_due_at_from_str_arg(due_at_str),
731
601
  )
732
-
733
- task_create = TaskCreate(
734
- duid=_make_duid(),
735
- source_type=TaskSourceType.CLI,
736
- drafter_duid=None,
737
- dartboard_duid=dartboard_duid,
738
- order=order,
739
- kind_duid=kind_duid,
740
- title=title,
741
- status_duid=status_duid,
742
- assignee_duids=assignee_duids,
743
- subscriber_duids=subscriber_duids,
744
- tag_duids=tag_duids,
745
- priority=priority,
746
- size=size,
747
- due_at=due_at,
748
- )
749
- task_create_op = Operation(
750
- model=OperationModelKind.TASK,
751
- kind=OperationKind.CREATE,
752
- data=task_create,
753
- )
754
- response = dart.transact([task_create_op], TransactionKind.TASK_CREATE)
755
- task = _parse_transaction_response_and_maybe_exit(
756
- response, OperationModelKind.TASK, task_create.duid
757
602
  )
758
-
759
- _log(f"Created task {task.title} at {_get_task_url(config.host, task.duid)}")
603
+ task = dart.create_task(task_create).item
604
+ _log(f"Created task\n\n {task.title}\n {task.html_url}\n ID: {task.id}\n")
760
605
 
761
606
  if should_begin:
762
- _begin_task(config, session, user["email"], lambda: task_create)
607
+ user = dart.get_config().user
608
+ _begin_task(dart, user.email, task)
763
609
 
764
610
  _log("Done.")
765
611
  return task
766
612
 
767
613
 
768
614
  def update_task(
769
- duid: str,
615
+ id: str,
770
616
  *,
771
- title: str | None = None,
772
- dartboard_duid: str | None = None,
773
- dartboard_title: str | None = None,
774
- status_title: str | None = None,
775
- assignee_emails: list[str] | None = None,
776
- tag_titles: list[str] | None = None,
777
- priority_int: int | None = None,
778
- size_int: int | None = None,
779
- due_at_str: str | None = None,
617
+ title: Union[Unset, str] = UNSET,
618
+ dartboard_title: Union[str, Unset] = UNSET,
619
+ status_title: Union[str, Unset] = UNSET,
620
+ assignee_emails: Union[list[str], Unset] = UNSET,
621
+ tag_titles: Union[list[str], Unset] = UNSET,
622
+ priority_int: Union[int, None, Unset] = UNSET,
623
+ size_int: Union[int, None, Unset] = UNSET,
624
+ due_at_str: Union[str, None, Unset] = UNSET,
780
625
  ) -> Task:
781
- config = _Config()
782
- session = _Session(config)
783
- dart = Dart(session)
784
-
785
- user_bundle = UserBundle(session)
786
-
787
- user = user_bundle.user
788
- user_duid = user["duid"]
789
-
790
- tasks = user_bundle.tasks
791
- old_task = next((e for e in tasks if e["duid"] == duid), None)
792
- if old_task is None:
793
- _dart_exit(f"No task found with DUID '{duid}'.")
794
-
795
- task_update_kwargs = {"duid": duid}
796
-
797
- if title is not None:
798
- task_update_kwargs["title"] = title
799
-
800
- dartboards = user_bundle.dartboards
801
- if dartboard_duid is not None:
802
- task_update_kwargs["dartboard_duid"] = dartboard_duid
803
- elif dartboard_title is not None:
804
- dartboard_title_norm = dartboard_title.strip().lower()
805
- dartboard = next(
806
- (
807
- e
808
- for e in dartboards
809
- if dartboard_title_norm in {e["title"].lower(), e["kind"].lower()}
810
- ),
811
- None,
812
- )
813
- if dartboard is None:
814
- _dart_exit(f"No dartboard found with title '{dartboard_title}'.")
815
- dartboard_duid = dartboard["duid"]
816
- if dartboard_duid != old_task["dartboardDuid"]:
817
- task_update_kwargs["dartboard_duid"] = dartboard_duid
818
-
819
- statuses = user_bundle.default_statuses
820
- if status_title is not None:
821
- status_title_norm = status_title.strip().lower()
822
- status = next(
823
- (e for e in statuses if e["title"].lower() == status_title_norm), None
824
- )
825
- if status is None:
826
- _dart_exit(f"No status found with title '{status_title}'.")
827
- status_duid = status["duid"]
828
- if status_duid != old_task["statusDuid"]:
829
- task_update_kwargs["status_duid"] = status_duid
830
-
831
- users = user_bundle.users
832
- user_emails_to_duids = {e["email"]: e["duid"] for e in users}
833
- subscriber_duids = []
834
- if assignee_emails is not None:
835
- assignee_duids = []
836
- for assignee_email in assignee_emails:
837
- assignee_email_norm = assignee_email.strip().lower()
838
- if assignee_email_norm not in user_emails_to_duids:
839
- _dart_exit(f"No user found with email '{assignee_email}'.")
840
- assignee_duids.append(user_emails_to_duids[assignee_email_norm])
841
- subscriber_duids.append(user_emails_to_duids[assignee_email_norm])
842
- assignee_duids = sorted(set(assignee_duids))
843
- if assignee_duids != old_task["assigneeDuids"]:
844
- task_update_kwargs["assignee_duids"] = assignee_duids
845
-
846
- # TODO do add to list operation rather than replace
847
- subscriber_duids = list(
848
- set(old_task["subscriberDuids"]) | set(subscriber_duids) | {user_duid}
849
- )
850
- if subscriber_duids != old_task["subscriberDuids"]:
851
- task_update_kwargs["subscriber_duids"] = subscriber_duids
852
-
853
- tags = user_bundle.default_tags
854
- tag_titles_to_duids = {e["title"]: e["duid"] for e in tags}
855
- if tag_titles is not None:
856
- tag_duids = []
857
- for tag_title in tag_titles:
858
- tag_title_norm = tag_title.strip().lower()
859
- if tag_title_norm not in tag_titles_to_duids:
860
- _dart_exit(f"No tag found with title '{tag_title}'.")
861
- tag_duids.append(tag_titles_to_duids[tag_title_norm])
862
- task_update_kwargs["tag_duids"] = tag_duids
863
-
864
- # TODO add a way for optional stuff to be removed
865
- if priority_int is not None:
866
- priority = _PRIORITY_MAP[priority_int]
867
- if priority != old_task["priority"]:
868
- task_update_kwargs["priority"] = priority
869
-
870
- if size_int is not None:
871
- size = size_int
872
- if size != old_task["size"]:
873
- task_update_kwargs["size"] = size
874
-
875
- if due_at_str is not None:
876
- due_at = dateparser.parse(due_at_str)
877
- if due_at is None:
878
- _dart_exit(f"Could not parse due date '{due_at_str}'.")
879
- due_at = due_at.replace(
880
- hour=9, minute=0, second=0, microsecond=0, tzinfo=timezone.utc
626
+ dart = Dart()
627
+ task_update = WrappedTaskUpdate(
628
+ item=TaskUpdate(
629
+ id,
630
+ title=title,
631
+ dartboard=dartboard_title,
632
+ status=status_title,
633
+ assignees=assignee_emails if assignee_emails is not None else UNSET,
634
+ tags=tag_titles if tag_titles is not None else UNSET,
635
+ priority=_normalize_priority(priority_int),
636
+ size=size_int,
637
+ due_at=_get_due_at_from_str_arg(due_at_str),
881
638
  )
882
- due_at = due_at.isoformat()[:-6] + ".000Z"
883
- if due_at != old_task["dueAt"]:
884
- task_update_kwargs["due_at"] = due_at
885
-
886
- task_update = TaskUpdate(**task_update_kwargs)
887
- task_update_op = Operation(
888
- model=OperationModelKind.TASK,
889
- kind=OperationKind.UPDATE,
890
- data=task_update,
891
- )
892
- response = dart.transact([task_update_op], TransactionKind.TASK_UPDATE)
893
- task = _parse_transaction_response_and_maybe_exit(
894
- response, OperationModelKind.TASK, task_update.duid
895
639
  )
640
+ task = dart.update_task(id, task_update).item
896
641
 
897
- _log(f"Updated task {task.title} at {_get_task_url(config.host, task.duid)}")
642
+ _log(f"Updated task\n\n {task.title}\n {task.html_url}\n ID: {task.id}\n")
898
643
  _log("Done.")
899
644
  return task
900
645
 
901
646
 
902
- def replicate_space(
903
- duid: str,
904
- *,
905
- title: str | None = None,
906
- abrev: str | None = None,
907
- color_hex: str | None = None,
908
- accessible_by_team: bool | None = None,
909
- accessor_duids: list[str] | None = None,
910
- ) -> str:
911
- config = _Config()
912
- session = _Session(config)
913
-
914
- content = {}
915
- if title is not None:
916
- content["title"] = title
917
- if abrev is not None:
918
- content["abrev"] = abrev
919
- if color_hex is not None:
920
- content["colorHex"] = color_hex
921
- if accessible_by_team is not None:
922
- content["accessibleByTeam"] = accessible_by_team
923
- if accessor_duids is not None:
924
- content["accessorIds"] = accessor_duids
925
- response = session.post(
926
- _REPLICATE_SPACE_URL_FRAG_FMT.format(duid=duid), json=content
927
- )
928
- _check_request_response_and_maybe_exit(response)
929
-
930
- space_duid = response.json()["duid"]
931
-
932
- _log(f"Replicated space at {_get_space_url(config.host, space_duid)}")
933
- _log("Done.")
934
- return space_duid
935
-
936
-
937
- def get_dartboards(space_duid: str, include_special: bool = False) -> list[Dartboard]:
647
+ def delete_task(id: str) -> Task:
938
648
  dart = Dart()
649
+ task = dart.delete_task(id).item
939
650
 
940
- response = dartboards_list.sync(client=dart, space_duid=space_duid)
941
- dartboards = response.results if response is not None else []
942
- if not include_special:
943
- dartboards = [e for e in dartboards if e.kind == DartboardKind.CUSTOM]
944
-
945
- _log(f"Got {len(dartboards)} dartboards")
651
+ _log(f"Deleted task\n\n {task.title}\n {task.html_url}\n ID: {task.id}\n")
946
652
  _log("Done.")
947
- return dartboards
948
-
949
-
950
- def replicate_dartboard(duid: str, *, title: str | None = None) -> str:
951
- config = _Config()
952
- session = _Session(config)
653
+ return task
953
654
 
954
- content = {}
955
- if title is not None:
956
- content["title"] = title
957
- response = session.post(
958
- _REPLICATE_DARTBOARD_URL_FRAG_FMT.format(duid=duid), json=content
959
- )
960
- _check_request_response_and_maybe_exit(response)
961
655
 
962
- dartboard_duid = response.json()["duid"]
656
+ def create_doc(
657
+ title: str,
658
+ *,
659
+ folder: Union[str, Unset] = UNSET,
660
+ text: Union[str, Unset] = UNSET,
661
+ ) -> Doc:
662
+ dart = Dart()
663
+ doc_create = WrappedDocCreate(item=DocCreate(title=title, folder=folder, text=text))
664
+ doc = dart.create_doc(doc_create).item
963
665
 
964
- _log(f"Replicated dartboard at {_get_dartboard_url(config.host, dartboard_duid)}")
666
+ _log(f"Created doc\n\n {doc.title}\n {doc.html_url}\n ID: {doc.id}\n")
965
667
  _log("Done.")
966
- return dartboard_duid
668
+ return doc
967
669
 
968
670
 
969
- def update_dartboard(
970
- duid: str,
671
+ def update_doc(
672
+ id: str,
971
673
  *,
972
- title: str | None = None,
973
- color_hex: str | None = None,
974
- ) -> Dartboard:
975
- config = _Config()
976
- session = _Session(config)
977
- dart = Dart(session)
978
-
979
- dartboard_update_kwargs = {"duid": duid}
980
-
981
- if title is not None:
982
- dartboard_update_kwargs["title"] = title
983
- if color_hex is not None:
984
- dartboard_update_kwargs["color_hex"] = color_hex
985
-
986
- dartboard_update = DartboardUpdate(**dartboard_update_kwargs)
987
- dartboard_update_op = Operation(
988
- model=OperationModelKind.DARTBOARD,
989
- kind=OperationKind.UPDATE,
990
- data=dartboard_update,
991
- )
992
- response = dart.transact([dartboard_update_op], TransactionKind.DARTBOARD_UPDATE)
993
- dartboard = _parse_transaction_response_and_maybe_exit(
994
- response, OperationModelKind.DARTBOARD, dartboard_update.duid
995
- )
674
+ title: str,
675
+ folder: Union[str, Unset] = UNSET,
676
+ text: Union[str, Unset] = UNSET,
677
+ ) -> Doc:
678
+ dart = Dart()
679
+ doc_update = WrappedDocUpdate(item=DocUpdate(id, title=title, folder=folder, text=text))
680
+ doc = dart.update_doc(id, doc_update).item
996
681
 
997
- _log(
998
- f"Updated dartboard {dartboard.title} at {_get_dartboard_url(config.host, dartboard.duid)}"
999
- )
682
+ _log(f"Updated doc\n\n {doc.title}\n {doc.html_url}\n ID: {doc.id}\n")
1000
683
  _log("Done.")
1001
- return dartboard
684
+ return doc
1002
685
 
1003
686
 
1004
- def get_folders(space_duid: str, *, include_special: bool = False) -> list[Folder]:
687
+ def delete_doc(id: str) -> Doc:
1005
688
  dart = Dart()
689
+ doc = dart.delete_doc(id).item
1006
690
 
1007
- response = folders_list.sync(client=dart, space_duid=space_duid)
1008
- folders = response.results if response is not None else []
1009
- if not include_special:
1010
- folders = [e for e in folders if e.kind == FolderKind.OTHER]
1011
-
1012
- _log(f"Got {len(folders)} folders")
691
+ _log(f"Deleted doc\n\n {doc.title}\n {doc.html_url}\n ID: {doc.id}\n")
1013
692
  _log("Done.")
1014
- return folders
1015
-
1016
-
1017
- def update_folder(
1018
- duid: str,
1019
- *,
1020
- title: str | None = None,
1021
- color_hex: str | None = None,
1022
- ) -> Folder:
1023
- config = _Config()
1024
- session = _Session(config)
1025
- dart = Dart(session)
1026
-
1027
- folder_update_kwargs = {"duid": duid}
693
+ return doc
1028
694
 
1029
- if title is not None:
1030
- folder_update_kwargs["title"] = title
1031
- if color_hex is not None:
1032
- folder_update_kwargs["color_hex"] = color_hex
1033
695
 
1034
- folder_update = FolderUpdate(**folder_update_kwargs)
1035
- folder_update_op = Operation(
1036
- model=OperationModelKind.FOLDER,
1037
- kind=OperationKind.UPDATE,
1038
- data=folder_update,
1039
- )
1040
- response = dart.transact([folder_update_op], TransactionKind.FOLDER_UPDATE)
1041
- folder = _parse_transaction_response_and_maybe_exit(
1042
- response, OperationModelKind.FOLDER, folder_update.duid
1043
- )
1044
-
1045
- _log(
1046
- f"Updated folder {folder.title} at {_get_folder_url(config.host, folder.duid)}"
1047
- )
696
+ def create_comment(id: str, text: str) -> Comment:
697
+ dart = Dart()
698
+ comment_create = WrappedCommentCreate(item=CommentCreate(task_id=id, text=text))
699
+ comment = dart.create_comment(comment_create).item
700
+ _log(f"Created comment\n\n {comment.html_url}\n ID: {comment.id}\n")
1048
701
  _log("Done.")
1049
- return folder
702
+ return comment
1050
703
 
1051
704
 
1052
705
  def _add_standard_task_arguments(parser: ArgumentParser) -> None:
1053
706
  parser.add_argument(
1054
- "-d", "--dartboard", dest="dartboard_title", help="dartboard title"
707
+ "-d",
708
+ "--dartboard",
709
+ dest="dartboard_title",
710
+ help="dartboard title",
711
+ default=UNSET,
712
+ )
713
+ parser.add_argument(
714
+ "-s",
715
+ "--status",
716
+ dest="status_title",
717
+ help="status title",
718
+ default=UNSET,
1055
719
  )
1056
- parser.add_argument("-s", "--status", dest="status_title", help="status title")
1057
720
  parser.add_argument(
1058
721
  "-a",
1059
722
  "--assignee",
@@ -1077,11 +740,24 @@ def _add_standard_task_arguments(parser: ArgumentParser) -> None:
1077
740
  type=int,
1078
741
  choices=_PRIORITY_MAP.keys(),
1079
742
  help="priority",
743
+ default=UNSET,
744
+ )
745
+ parser.add_argument(
746
+ "-i",
747
+ "--size",
748
+ dest="size_int",
749
+ type=int,
750
+ choices=_SIZES,
751
+ help="size",
752
+ default=UNSET,
1080
753
  )
1081
754
  parser.add_argument(
1082
- "-i", "--size", dest="size_int", type=int, choices=_SIZES, help="size"
755
+ "-r",
756
+ "--duedate",
757
+ dest="due_at_str",
758
+ help="due date",
759
+ default=UNSET,
1083
760
  )
1084
- parser.add_argument("-r", "--duedate", dest="due_at_str", help="due date")
1085
761
 
1086
762
 
1087
763
  def cli() -> None:
@@ -1096,24 +772,35 @@ def cli() -> None:
1096
772
  return
1097
773
 
1098
774
  parser = ArgumentParser(prog=_PROG, description="A CLI to interact with Dart")
775
+ metavar = ",".join(
776
+ [
777
+ _LOGIN_CMD,
778
+ _CREATE_TASK_CMD,
779
+ _UPDATE_TASK_CMD,
780
+ _DELETE_TASK_CMD,
781
+ _BEGIN_TASK_CMD,
782
+ _CREATE_DOC_CMD,
783
+ _UPDATE_DOC_CMD,
784
+ _DELETE_DOC_CMD,
785
+ _CREATE_COMMENT_CMD,
786
+ ]
787
+ )
1099
788
  subparsers = parser.add_subparsers(
1100
789
  title="command",
1101
790
  required=True,
1102
- metavar=f"{{{_LOGIN_CMD},{_CREATE_TASK_CMD},{_UPDATE_TASK_CMD},{_BEGIN_TASK_CMD}}}",
791
+ metavar=f"{{{metavar}}}",
1103
792
  )
1104
793
 
1105
- set_host_parser = subparsers.add_parser(_SET_HOST_CMD, aliases="s")
794
+ set_host_parser = subparsers.add_parser(_SET_HOST_CMD, aliases=["sh"])
1106
795
  set_host_parser.add_argument("host", help="the new host: {prod|stag|dev|[URL]}")
1107
796
  set_host_parser.set_defaults(func=set_host)
1108
797
 
1109
- login_parser = subparsers.add_parser(_LOGIN_CMD, aliases="l", help="login")
1110
- login_parser.add_argument(
1111
- "-t", "--token", dest="token", help="your authentication token"
1112
- )
798
+ login_parser = subparsers.add_parser(_LOGIN_CMD, aliases=["l"], help="login")
799
+ login_parser.add_argument("-t", "--token", dest="token", help="your authentication token")
1113
800
  login_parser.set_defaults(func=login)
1114
801
 
1115
802
  create_task_parser = subparsers.add_parser(
1116
- _CREATE_TASK_CMD, aliases="c", help="create a new task"
803
+ _CREATE_TASK_CMD, aliases=["tc"], help=_HELP_TEXT_TO_COMMAND[_CREATE_TASK_CMD]
1117
804
  )
1118
805
  create_task_parser.add_argument("title", help="title of the task")
1119
806
  create_task_parser.add_argument(
@@ -1127,18 +814,52 @@ def cli() -> None:
1127
814
  create_task_parser.set_defaults(func=create_task)
1128
815
 
1129
816
  update_task_parser = subparsers.add_parser(
1130
- _UPDATE_TASK_CMD, aliases="u", help="update an existing task"
817
+ _UPDATE_TASK_CMD, aliases=["tu"], help=_HELP_TEXT_TO_COMMAND[_UPDATE_TASK_CMD]
1131
818
  )
1132
- update_task_parser.add_argument("duid", help="Dart ID (DUID) of the task")
1133
- update_task_parser.add_argument("-e", "--title", dest="title", help="task title")
819
+ update_task_parser.add_argument("id", help="ID of the task")
820
+ update_task_parser.add_argument("-e", "--title", dest="title", help="task title", default=UNSET)
1134
821
  _add_standard_task_arguments(update_task_parser)
1135
822
  update_task_parser.set_defaults(func=update_task)
1136
823
 
1137
- begin_task_parser = subparsers.add_parser(
1138
- _BEGIN_TASK_CMD, aliases="b", help="begin work on a task"
824
+ delete_task_parser = subparsers.add_parser(
825
+ _DELETE_TASK_CMD, aliases=["td"], help=_HELP_TEXT_TO_COMMAND[_DELETE_TASK_CMD]
1139
826
  )
827
+ delete_task_parser.add_argument("id", help="ID of the task")
828
+ delete_task_parser.set_defaults(func=delete_task)
829
+
830
+ begin_task_parser = subparsers.add_parser(_BEGIN_TASK_CMD, aliases=["tb"], help="begin work on a task")
1140
831
  begin_task_parser.set_defaults(func=begin_task)
1141
832
 
833
+ create_doc_parser = subparsers.add_parser(
834
+ _CREATE_DOC_CMD, aliases=["dc"], help=_HELP_TEXT_TO_COMMAND[_CREATE_DOC_CMD]
835
+ )
836
+ create_doc_parser.add_argument("title", help="title of the doc")
837
+ create_doc_parser.add_argument("-f", "--folder", dest="folder", help="doc folder", default=UNSET)
838
+ create_doc_parser.add_argument("-t", "--text", dest="text", help="doc text", default=UNSET)
839
+ create_doc_parser.set_defaults(func=create_doc)
840
+
841
+ update_doc_parser = subparsers.add_parser(
842
+ _UPDATE_DOC_CMD, aliases=["du"], help=_HELP_TEXT_TO_COMMAND[_UPDATE_DOC_CMD]
843
+ )
844
+ update_doc_parser.add_argument("id", help="ID of the doc")
845
+ update_doc_parser.add_argument("-e", "--title", dest="title", help="doc title", default=UNSET)
846
+ update_doc_parser.add_argument("-f", "--folder", dest="folder", help="doc folder", default=UNSET)
847
+ update_doc_parser.add_argument("-t", "--text", dest="text", help="doc text", default=UNSET)
848
+ update_doc_parser.set_defaults(func=update_doc)
849
+
850
+ delete_doc_parser = subparsers.add_parser(
851
+ _DELETE_DOC_CMD, aliases=["dd"], help=_HELP_TEXT_TO_COMMAND[_DELETE_DOC_CMD]
852
+ )
853
+ delete_doc_parser.add_argument("id", help="ID of the doc")
854
+ delete_doc_parser.set_defaults(func=delete_doc)
855
+
856
+ create_comment_parser = subparsers.add_parser(
857
+ _CREATE_COMMENT_CMD, aliases=["cc"], help=_HELP_TEXT_TO_COMMAND[_CREATE_COMMENT_CMD]
858
+ )
859
+ create_comment_parser.add_argument("id", help="ID of the task")
860
+ create_comment_parser.add_argument("text", help="text of the comment")
861
+ create_comment_parser.set_defaults(func=create_comment)
862
+
1142
863
  args = vars(parser.parse_args())
1143
864
  func = args.pop("func")
1144
865
  func(**args)