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.
- dart/__init__.py +8 -6
- dart/dart.py +423 -702
- dart/generated/__init__.py +1 -1
- dart/generated/api/__init__.py +8 -0
- dart/generated/api/comment/__init__.py +1 -0
- dart/generated/api/{transactions/transactions_create.py → comment/create_comment.py} +57 -58
- dart/generated/api/config/__init__.py +1 -0
- dart/generated/api/config/get_config.py +141 -0
- dart/generated/api/dartboard/__init__.py +1 -0
- dart/generated/api/{attachments/attachments_list.py → dartboard/retrieve_dartboard.py} +58 -62
- dart/generated/api/doc/__init__.py +1 -0
- dart/generated/api/doc/create_doc.py +179 -0
- dart/generated/api/doc/delete_doc.py +165 -0
- dart/generated/api/{comments/comments_list.py → doc/list_docs.py} +115 -111
- dart/generated/api/{forms/forms_list.py → doc/retrieve_doc.py} +58 -62
- dart/generated/api/doc/update_doc.py +188 -0
- dart/generated/api/folder/__init__.py +1 -0
- dart/generated/api/{webhooks/webhooks_list.py → folder/retrieve_folder.py} +58 -62
- dart/generated/api/task/__init__.py +1 -0
- dart/generated/api/task/create_task.py +172 -0
- dart/generated/api/task/delete_task.py +165 -0
- dart/generated/api/task/list_tasks.py +497 -0
- dart/generated/api/{layouts/layouts_list.py → task/retrieve_task.py} +58 -62
- dart/generated/api/task/update_task.py +188 -0
- dart/generated/api/view/__init__.py +1 -0
- dart/generated/api/{tenants/tenants_list.py → view/retrieve_view.py} +58 -62
- dart/generated/client.py +13 -13
- dart/generated/models/__init__.py +34 -342
- dart/generated/models/comment.py +34 -150
- dart/generated/models/comment_create.py +18 -97
- dart/generated/models/concise_doc.py +84 -0
- dart/generated/models/concise_task.py +267 -0
- dart/generated/models/dartboard.py +37 -237
- dart/generated/models/doc.py +31 -183
- dart/generated/models/doc_create.py +22 -238
- dart/generated/models/doc_update.py +26 -233
- dart/generated/models/folder.py +39 -84
- dart/generated/models/list_docs_o_item.py +15 -0
- dart/generated/models/{paginated_attachment_list.py → paginated_concise_doc_list.py} +18 -17
- dart/generated/models/{paginated_dartboard_list.py → paginated_concise_task_list.py} +18 -17
- dart/generated/models/task.py +149 -406
- dart/generated/models/task_create.py +113 -428
- dart/generated/models/task_update.py +113 -421
- dart/generated/models/user.py +14 -192
- dart/generated/models/user_space_configuration.py +147 -0
- dart/generated/models/view.py +39 -149
- dart/generated/models/wrapped_comment.py +65 -0
- dart/generated/models/wrapped_comment_create.py +65 -0
- dart/generated/models/wrapped_dartboard.py +65 -0
- dart/generated/models/wrapped_doc.py +65 -0
- dart/generated/models/wrapped_doc_create.py +65 -0
- dart/generated/models/wrapped_doc_update.py +65 -0
- dart/generated/models/wrapped_folder.py +65 -0
- dart/generated/models/wrapped_task.py +65 -0
- dart/generated/models/wrapped_task_create.py +65 -0
- dart/generated/models/wrapped_task_update.py +65 -0
- dart/generated/models/wrapped_view.py +65 -0
- dart/generated/types.py +4 -3
- dart/old.py +216 -0
- dart/order_manager.py +4 -15
- dart/webhook.py +2 -7
- {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/METADATA +33 -62
- dart_tools-0.7.1.dist-info/RECORD +71 -0
- {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/WHEEL +1 -1
- dart/generated/api/attachments/__init__.py +0 -0
- dart/generated/api/comments/__init__.py +0 -0
- dart/generated/api/dartboards/__init__.py +0 -0
- dart/generated/api/dartboards/dartboards_list.py +0 -286
- dart/generated/api/dashboards/__init__.py +0 -0
- dart/generated/api/dashboards/dashboards_list.py +0 -199
- dart/generated/api/docs/__init__.py +0 -0
- dart/generated/api/docs/docs_list.py +0 -387
- dart/generated/api/folders/__init__.py +0 -0
- dart/generated/api/folders/folders_list.py +0 -249
- dart/generated/api/form_fields/__init__.py +0 -0
- dart/generated/api/form_fields/form_fields_list.py +0 -169
- dart/generated/api/forms/__init__.py +0 -0
- dart/generated/api/layouts/__init__.py +0 -0
- dart/generated/api/links/__init__.py +0 -0
- dart/generated/api/links/links_list.py +0 -169
- dart/generated/api/options/__init__.py +0 -0
- dart/generated/api/options/options_list.py +0 -244
- dart/generated/api/properties/__init__.py +0 -0
- dart/generated/api/properties/properties_list.py +0 -219
- dart/generated/api/reactions/__init__.py +0 -0
- dart/generated/api/reactions/reactions_list.py +0 -169
- dart/generated/api/relationship_kinds/__init__.py +0 -0
- dart/generated/api/relationship_kinds/relationship_kinds_list.py +0 -169
- dart/generated/api/relationships/__init__.py +0 -0
- dart/generated/api/relationships/relationships_list.py +0 -169
- dart/generated/api/spaces/__init__.py +0 -0
- dart/generated/api/spaces/spaces_list.py +0 -229
- dart/generated/api/statuses/__init__.py +0 -0
- dart/generated/api/statuses/statuses_list.py +0 -264
- dart/generated/api/task_doc_relationships/__init__.py +0 -0
- dart/generated/api/task_doc_relationships/task_doc_relationships_list.py +0 -169
- dart/generated/api/task_kinds/__init__.py +0 -0
- dart/generated/api/task_kinds/task_kinds_list.py +0 -219
- dart/generated/api/tasks/__init__.py +0 -0
- dart/generated/api/tasks/tasks_list.py +0 -461
- dart/generated/api/tenants/__init__.py +0 -0
- dart/generated/api/transactions/__init__.py +0 -0
- dart/generated/api/user_dartboard_layouts/__init__.py +0 -0
- dart/generated/api/user_dartboard_layouts/user_dartboard_layouts_list.py +0 -169
- dart/generated/api/user_data/__init__.py +0 -0
- dart/generated/api/user_data/user_data_entity_retrieve.py +0 -580
- dart/generated/api/users/__init__.py +0 -0
- dart/generated/api/users/users_list.py +0 -229
- dart/generated/api/views/__init__.py +0 -0
- dart/generated/api/views/views_list.py +0 -199
- dart/generated/api/webhooks/__init__.py +0 -0
- dart/generated/models/attachment.py +0 -112
- dart/generated/models/attachment_create.py +0 -121
- dart/generated/models/attachment_update.py +0 -125
- dart/generated/models/bar_chart_adtl.py +0 -98
- dart/generated/models/brainstorm.py +0 -149
- dart/generated/models/brainstorm_create.py +0 -134
- dart/generated/models/brainstorm_state.py +0 -10
- dart/generated/models/brainstorm_update.py +0 -153
- dart/generated/models/burn_up_chart_adtl.py +0 -103
- dart/generated/models/chart.py +0 -208
- dart/generated/models/chart_aggregation.py +0 -10
- dart/generated/models/chart_type.py +0 -14
- dart/generated/models/comment_reaction.py +0 -84
- dart/generated/models/comment_reaction_create.py +0 -82
- dart/generated/models/comment_reaction_update.py +0 -87
- dart/generated/models/comment_update.py +0 -148
- dart/generated/models/dartboard_create.py +0 -315
- dart/generated/models/dartboard_kind.py +0 -12
- dart/generated/models/dartboard_update.py +0 -317
- dart/generated/models/dartboards_list_kind.py +0 -12
- dart/generated/models/dashboard.py +0 -185
- dart/generated/models/dashboard_create.py +0 -171
- dart/generated/models/dashboard_update.py +0 -173
- dart/generated/models/discord_integration.py +0 -72
- dart/generated/models/doc_source_type.py +0 -13
- dart/generated/models/docs_list_o_item.py +0 -15
- dart/generated/models/entity_name.py +0 -22
- dart/generated/models/event.py +0 -420
- dart/generated/models/event_actor.py +0 -19
- dart/generated/models/event_create.py +0 -158
- dart/generated/models/event_kind.py +0 -88
- dart/generated/models/event_subscription.py +0 -74
- dart/generated/models/event_subscription_update.py +0 -173
- dart/generated/models/filter_applicability.py +0 -22
- dart/generated/models/filter_assignee.py +0 -116
- dart/generated/models/filter_connector.py +0 -9
- dart/generated/models/filter_group.py +0 -112
- dart/generated/models/filter_search.py +0 -82
- dart/generated/models/filter_set.py +0 -116
- dart/generated/models/folder_create.py +0 -150
- dart/generated/models/folder_kind.py +0 -10
- dart/generated/models/folder_update.py +0 -152
- dart/generated/models/folders_list_kind.py +0 -10
- dart/generated/models/form.py +0 -147
- dart/generated/models/form_create.py +0 -141
- dart/generated/models/form_field.py +0 -144
- dart/generated/models/form_field_create.py +0 -129
- dart/generated/models/form_field_update.py +0 -132
- dart/generated/models/form_update.py +0 -142
- dart/generated/models/github_integration.py +0 -163
- dart/generated/models/github_integration_tenant_extension_status.py +0 -11
- dart/generated/models/google_data.py +0 -94
- dart/generated/models/icon_kind.py +0 -10
- dart/generated/models/layout.py +0 -167
- dart/generated/models/layout_config.py +0 -70
- dart/generated/models/layout_create.py +0 -130
- dart/generated/models/layout_kind.py +0 -11
- dart/generated/models/layout_kind_config_map.py +0 -56
- dart/generated/models/layout_update.py +0 -130
- dart/generated/models/line_chart_adtl.py +0 -72
- dart/generated/models/models_response.py +0 -671
- dart/generated/models/notification.py +0 -120
- dart/generated/models/notification_update.py +0 -100
- dart/generated/models/notion_integration.py +0 -90
- dart/generated/models/notion_integration_tenant_extension_status.py +0 -10
- dart/generated/models/number_chart_adtl.py +0 -77
- dart/generated/models/operation.py +0 -874
- dart/generated/models/operation_kind.py +0 -12
- dart/generated/models/operation_model_kind.py +0 -36
- dart/generated/models/option.py +0 -118
- dart/generated/models/option_create.py +0 -105
- dart/generated/models/option_update.py +0 -107
- dart/generated/models/paginated_comment_list.py +0 -122
- dart/generated/models/paginated_comment_reaction_list.py +0 -122
- dart/generated/models/paginated_dashboard_list.py +0 -122
- dart/generated/models/paginated_doc_list.py +0 -122
- dart/generated/models/paginated_folder_list.py +0 -122
- dart/generated/models/paginated_form_field_list.py +0 -122
- dart/generated/models/paginated_form_list.py +0 -122
- dart/generated/models/paginated_layout_list.py +0 -122
- dart/generated/models/paginated_option_list.py +0 -122
- dart/generated/models/paginated_property_list.py +0 -122
- dart/generated/models/paginated_relationship_kind_list.py +0 -122
- dart/generated/models/paginated_relationship_list.py +0 -122
- dart/generated/models/paginated_space_list.py +0 -122
- dart/generated/models/paginated_status_list.py +0 -122
- dart/generated/models/paginated_task_doc_relationship_list.py +0 -122
- dart/generated/models/paginated_task_kind_list.py +0 -122
- dart/generated/models/paginated_task_link_list.py +0 -122
- dart/generated/models/paginated_task_list.py +0 -122
- dart/generated/models/paginated_tenant_list.py +0 -122
- dart/generated/models/paginated_user_dartboard_layout_list.py +0 -122
- dart/generated/models/paginated_user_list.py +0 -122
- dart/generated/models/paginated_view_list.py +0 -122
- dart/generated/models/paginated_webhook_list.py +0 -122
- dart/generated/models/pie_chart_adtl.py +0 -69
- dart/generated/models/pie_chart_display_metric.py +0 -9
- dart/generated/models/properties_list_kind.py +0 -32
- dart/generated/models/property_.py +0 -153
- dart/generated/models/property_create.py +0 -137
- dart/generated/models/property_kind.py +0 -32
- dart/generated/models/property_update.py +0 -146
- dart/generated/models/relationship.py +0 -74
- dart/generated/models/relationship_create.py +0 -93
- dart/generated/models/relationship_kind.py +0 -123
- dart/generated/models/relationship_kind_create.py +0 -117
- dart/generated/models/relationship_kind_kind.py +0 -12
- dart/generated/models/relationship_kind_update.py +0 -119
- dart/generated/models/report_kind.py +0 -9
- dart/generated/models/request_body.py +0 -83
- dart/generated/models/response_body.py +0 -72
- dart/generated/models/saml_config.py +0 -77
- dart/generated/models/saml_config_tenant_extension_status.py +0 -9
- dart/generated/models/slack_integration.py +0 -90
- dart/generated/models/slack_integration_tenant_extension_status.py +0 -10
- dart/generated/models/sort.py +0 -66
- dart/generated/models/space.py +0 -311
- dart/generated/models/space_create.py +0 -340
- dart/generated/models/space_kind.py +0 -10
- dart/generated/models/space_update.py +0 -341
- dart/generated/models/sprint_mode.py +0 -9
- dart/generated/models/status.py +0 -141
- dart/generated/models/status_create.py +0 -125
- dart/generated/models/status_kind.py +0 -12
- dart/generated/models/status_update.py +0 -135
- dart/generated/models/statuses_list_kind.py +0 -12
- dart/generated/models/subscription.py +0 -9
- dart/generated/models/subtask_display_mode.py +0 -10
- dart/generated/models/summary_statistic_kind.py +0 -14
- dart/generated/models/table_chart_adtl.py +0 -98
- dart/generated/models/task_detail_mode.py +0 -10
- dart/generated/models/task_doc_relationship.py +0 -96
- dart/generated/models/task_doc_relationship_create.py +0 -74
- dart/generated/models/task_kind.py +0 -149
- dart/generated/models/task_kind_create.py +0 -144
- dart/generated/models/task_kind_kind.py +0 -9
- dart/generated/models/task_kind_update.py +0 -153
- dart/generated/models/task_kinds_list_kind.py +0 -9
- dart/generated/models/task_link.py +0 -131
- dart/generated/models/task_link_create.py +0 -152
- dart/generated/models/task_link_kind.py +0 -19
- dart/generated/models/task_link_update.py +0 -155
- dart/generated/models/task_notion_document.py +0 -196
- dart/generated/models/task_notion_document_block_children_map_type_0.py +0 -43
- dart/generated/models/task_notion_document_block_map_type_0.py +0 -43
- dart/generated/models/task_notion_document_page_map_type_0.py +0 -43
- dart/generated/models/task_properties.py +0 -43
- dart/generated/models/task_source_type.py +0 -31
- dart/generated/models/tenant.py +0 -386
- dart/generated/models/tenant_update.py +0 -166
- dart/generated/models/theme.py +0 -10
- dart/generated/models/transaction.py +0 -158
- dart/generated/models/transaction_kind.py +0 -74
- dart/generated/models/transaction_response.py +0 -96
- dart/generated/models/user_dartboard_layout.py +0 -66
- dart/generated/models/user_dartboard_layout_create.py +0 -74
- dart/generated/models/user_data_entity_retrieve_entity_kind.py +0 -32
- dart/generated/models/user_role.py +0 -12
- dart/generated/models/user_status.py +0 -12
- dart/generated/models/user_update.py +0 -210
- dart/generated/models/validation_error_response.py +0 -64
- dart/generated/models/validation_error_response_items.py +0 -43
- dart/generated/models/view_create.py +0 -213
- dart/generated/models/view_kind.py +0 -11
- dart/generated/models/view_update.py +0 -215
- dart/generated/models/webhook.py +0 -96
- dart/generated/models/webhook_create.py +0 -77
- dart/generated/models/webhook_update.py +0 -78
- dart/generated/models/zapier_integration.py +0 -66
- dart_tools-0.6.16.dist-info/RECORD +0 -257
- dart_tools-0.6.16.dist-info/dist/dart-tools-0.3.3.tar.gz +0 -0
- {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info}/entry_points.txt +0 -0
- {dart_tools-0.6.16.dist-info → dart_tools-0.7.1.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
22
|
+
from typing import Callable, NoReturn, TypeVar, Union
|
|
22
23
|
from webbrowser import open_new_tab
|
|
23
24
|
|
|
24
25
|
import dateparser
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
52
|
-
|
|
45
|
+
UserSpaceConfiguration,
|
|
46
|
+
WrappedComment,
|
|
47
|
+
WrappedCommentCreate,
|
|
48
|
+
WrappedDoc,
|
|
49
|
+
WrappedDocCreate,
|
|
50
|
+
WrappedDocUpdate,
|
|
51
|
+
WrappedTask,
|
|
52
|
+
WrappedTaskCreate,
|
|
53
|
+
WrappedTaskUpdate,
|
|
53
54
|
)
|
|
54
|
-
from .generated.
|
|
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
|
-
|
|
68
|
-
_SET_HOST_CMD = "sethost"
|
|
65
|
+
# Service commands
|
|
69
66
|
_LOGIN_CMD = "login"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
88
|
+
_CLIENT_ID_KEY = "clientId"
|
|
85
89
|
_HOST_KEY = "host"
|
|
86
90
|
_HOSTS_KEY = "hosts"
|
|
87
91
|
_AUTH_TOKEN_KEY = "authToken"
|
|
88
92
|
|
|
89
|
-
|
|
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
|
|
109
|
-
return "".join(random.choices(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
206
|
-
return self._content[
|
|
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
|
|
256
|
+
class Dart:
|
|
226
257
|
def __init__(self, config=None):
|
|
227
258
|
self._config = config or _Config()
|
|
228
|
-
self.
|
|
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
|
|
234
|
-
return self._config.
|
|
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.
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
@
|
|
368
|
-
def
|
|
369
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
604
|
-
|
|
540
|
+
if not filtered_tasks:
|
|
541
|
+
_dart_exit("No active, incomplete tasks found.")
|
|
605
542
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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(
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
615
|
+
id: str,
|
|
770
616
|
*,
|
|
771
|
-
title: str
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
666
|
+
_log(f"Created doc\n\n {doc.title}\n {doc.html_url}\n ID: {doc.id}\n")
|
|
965
667
|
_log("Done.")
|
|
966
|
-
return
|
|
668
|
+
return doc
|
|
967
669
|
|
|
968
670
|
|
|
969
|
-
def
|
|
970
|
-
|
|
671
|
+
def update_doc(
|
|
672
|
+
id: str,
|
|
971
673
|
*,
|
|
972
|
-
title: str
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
684
|
+
return doc
|
|
1002
685
|
|
|
1003
686
|
|
|
1004
|
-
def
|
|
687
|
+
def delete_doc(id: str) -> Doc:
|
|
1005
688
|
dart = Dart()
|
|
689
|
+
doc = dart.delete_doc(id).item
|
|
1006
690
|
|
|
1007
|
-
|
|
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
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
702
|
+
return comment
|
|
1050
703
|
|
|
1051
704
|
|
|
1052
705
|
def _add_standard_task_arguments(parser: ArgumentParser) -> None:
|
|
1053
706
|
parser.add_argument(
|
|
1054
|
-
"-d",
|
|
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
|
-
"-
|
|
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"{{{
|
|
791
|
+
metavar=f"{{{metavar}}}",
|
|
1103
792
|
)
|
|
1104
793
|
|
|
1105
|
-
set_host_parser = subparsers.add_parser(_SET_HOST_CMD, aliases="
|
|
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="
|
|
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="
|
|
817
|
+
_UPDATE_TASK_CMD, aliases=["tu"], help=_HELP_TEXT_TO_COMMAND[_UPDATE_TASK_CMD]
|
|
1131
818
|
)
|
|
1132
|
-
update_task_parser.add_argument("
|
|
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
|
-
|
|
1138
|
-
|
|
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)
|