annofabcli 1.113.0__py3-none-any.whl → 1.114.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/__main__.py +2 -0
- annofabcli/annotation/dump_annotation.py +19 -12
- annofabcli/annotation_zip/list_annotation_3d_bounding_box.py +7 -0
- annofabcli/comment/list_all_comment.py +41 -8
- annofabcli/common/download.py +69 -0
- annofabcli/input_data/list_all_input_data.py +34 -11
- annofabcli/task/list_all_tasks.py +29 -8
- annofabcli/task/list_all_tasks_added_task_history.py +38 -25
- annofabcli/task_count/__init__.py +0 -0
- annofabcli/task_count/list_by_phase.py +447 -0
- annofabcli/task_count/subcommand_task_count.py +21 -0
- annofabcli/task_history/list_all_task_history.py +31 -9
- annofabcli/task_history_event/list_all_task_history_event.py +30 -9
- {annofabcli-1.113.0.dist-info → annofabcli-1.114.0.dist-info}/METADATA +1 -1
- {annofabcli-1.113.0.dist-info → annofabcli-1.114.0.dist-info}/RECORD +18 -15
- {annofabcli-1.113.0.dist-info → annofabcli-1.114.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.113.0.dist-info → annofabcli-1.114.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.113.0.dist-info → annofabcli-1.114.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import tempfile
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import isodate
|
|
10
|
+
import pandas
|
|
11
|
+
from annofabapi.models import ProjectMemberRole, Task, TaskPhase, TaskStatus
|
|
12
|
+
from annofabapi.resource import Resource as AnnofabResource
|
|
13
|
+
from annofabapi.utils import get_number_of_rejections
|
|
14
|
+
|
|
15
|
+
import annofabcli.common
|
|
16
|
+
from annofabcli.common.cli import (
|
|
17
|
+
ArgumentParser,
|
|
18
|
+
CommandLine,
|
|
19
|
+
build_annofabapi_resource_and_login,
|
|
20
|
+
)
|
|
21
|
+
from annofabcli.common.download import DownloadingFile
|
|
22
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
23
|
+
from annofabcli.common.type_util import assert_noreturn
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def isoduration_to_second(duration: str) -> float:
|
|
29
|
+
"""
|
|
30
|
+
ISO 8601 duration を 秒に変換する
|
|
31
|
+
"""
|
|
32
|
+
return isodate.parse_duration(duration).total_seconds()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaskStatusForSummary(Enum):
|
|
36
|
+
"""
|
|
37
|
+
Annofabユーザーがタスクについて知りたい粒度でまとめて分解した、タスクの状態。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
NEVER_WORKED_UNASSIGNED = "never_worked.unassigned"
|
|
41
|
+
"""一度も作業していない状態かつ担当者未割り当て"""
|
|
42
|
+
NEVER_WORKED_ASSIGNED = "never_worked.assigned"
|
|
43
|
+
"""一度も作業していない状態かつ担当者割り当て済み"""
|
|
44
|
+
WORKED_NOT_REJECTED = "worked.not_rejected"
|
|
45
|
+
"""タスクのstatusは作業中または休憩中 AND 次のフェーズでまだ差し戻されていない(次のフェーズに進んでいない)"""
|
|
46
|
+
WORKED_REJECTED = "worked.rejected"
|
|
47
|
+
"""タスクのstatusは作業中または休憩中 AND 次のフェーズで差し戻された"""
|
|
48
|
+
ON_HOLD = "on_hold"
|
|
49
|
+
"""保留中"""
|
|
50
|
+
COMPLETE = "complete"
|
|
51
|
+
"""完了"""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _get_not_started_status(task: Task, task_history_list: list[dict[str, Any]], not_worked_threshold_second: float) -> "TaskStatusForSummary":
|
|
55
|
+
"""
|
|
56
|
+
NOT_STARTED状態のタスクについて、一度も作業されていないか、差し戻されて未着手かを判定する。
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
task: タスク情報
|
|
60
|
+
task_history_list: タスク履歴のリスト
|
|
61
|
+
not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
|
|
62
|
+
"""
|
|
63
|
+
# `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
|
|
64
|
+
step = get_step_for_current_phase(task, number_of_inspections=1)
|
|
65
|
+
if step == 1:
|
|
66
|
+
phase = task["phase"]
|
|
67
|
+
worktime_second = sum(isoduration_to_second(history["accumulated_labor_time_milliseconds"]) for history in task_history_list if history["phase"] == phase)
|
|
68
|
+
if worktime_second <= not_worked_threshold_second:
|
|
69
|
+
# 一度も作業されていない(または閾値以下の作業時間)
|
|
70
|
+
account_id = task["account_id"]
|
|
71
|
+
if account_id is None:
|
|
72
|
+
return TaskStatusForSummary.NEVER_WORKED_UNASSIGNED
|
|
73
|
+
else:
|
|
74
|
+
return TaskStatusForSummary.NEVER_WORKED_ASSIGNED
|
|
75
|
+
else:
|
|
76
|
+
return TaskStatusForSummary.WORKED_NOT_REJECTED
|
|
77
|
+
else:
|
|
78
|
+
return TaskStatusForSummary.WORKED_NOT_REJECTED
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _get_working_or_break_status(task: Task) -> "TaskStatusForSummary":
|
|
82
|
+
"""
|
|
83
|
+
WORKINGまたはBREAK状態のタスクについて、rejectされているかどうかを判定する。
|
|
84
|
+
"""
|
|
85
|
+
# `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
|
|
86
|
+
step = get_step_for_current_phase(task, number_of_inspections=1)
|
|
87
|
+
if step == 1:
|
|
88
|
+
return TaskStatusForSummary.WORKED_NOT_REJECTED
|
|
89
|
+
else:
|
|
90
|
+
return TaskStatusForSummary.WORKED_REJECTED
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def from_task(task: dict[str, Any], task_history_list: list[dict[str, Any]], not_worked_threshold_second: float = 0) -> "TaskStatusForSummary":
|
|
94
|
+
"""
|
|
95
|
+
タスク情報(dict)からインスタンスを生成します。
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
task: APIから取得したタスク情報. 以下のkeyが必要です。
|
|
99
|
+
* status
|
|
100
|
+
* phase
|
|
101
|
+
* phase_stage
|
|
102
|
+
* histories_by_phase
|
|
103
|
+
* account_id
|
|
104
|
+
task_history_list: タスク履歴のリスト
|
|
105
|
+
not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
status = TaskStatus(task["status"])
|
|
109
|
+
match status:
|
|
110
|
+
case TaskStatus.COMPLETE:
|
|
111
|
+
return TaskStatusForSummary.COMPLETE
|
|
112
|
+
|
|
113
|
+
case TaskStatus.ON_HOLD:
|
|
114
|
+
return TaskStatusForSummary.ON_HOLD
|
|
115
|
+
|
|
116
|
+
case TaskStatus.NOT_STARTED:
|
|
117
|
+
return TaskStatusForSummary._get_not_started_status(task, task_history_list, not_worked_threshold_second)
|
|
118
|
+
|
|
119
|
+
case TaskStatus.BREAK | TaskStatus.WORKING:
|
|
120
|
+
return TaskStatusForSummary._get_working_or_break_status(task)
|
|
121
|
+
|
|
122
|
+
case _:
|
|
123
|
+
raise RuntimeError(f"'{status}'は対象外です。")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_step_for_current_phase(task: Task, number_of_inspections: int) -> int:
|
|
127
|
+
"""
|
|
128
|
+
今のフェーズが何回目かを取得する。
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
task: 対象タスクの情報。phase, phase_stage, histories_by_phase を含む。
|
|
132
|
+
number_of_inspections: 対象プロジェクトの検査フェーズの回数
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
現在のフェーズのステップ数
|
|
136
|
+
"""
|
|
137
|
+
current_phase = TaskPhase(task["phase"])
|
|
138
|
+
current_phase_stage = task["phase_stage"]
|
|
139
|
+
histories_by_phase = task["histories_by_phase"]
|
|
140
|
+
|
|
141
|
+
number_of_rejections_by_acceptance = get_number_of_rejections(histories_by_phase, phase=TaskPhase.ACCEPTANCE)
|
|
142
|
+
match current_phase:
|
|
143
|
+
case TaskPhase.ACCEPTANCE:
|
|
144
|
+
return number_of_rejections_by_acceptance + 1
|
|
145
|
+
|
|
146
|
+
case TaskPhase.ANNOTATION:
|
|
147
|
+
number_of_rejections_by_inspection = sum(
|
|
148
|
+
get_number_of_rejections(histories_by_phase, phase=TaskPhase.INSPECTION, phase_stage=phase_stage) for phase_stage in range(1, number_of_inspections + 1)
|
|
149
|
+
)
|
|
150
|
+
return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
|
|
151
|
+
|
|
152
|
+
case TaskPhase.INSPECTION:
|
|
153
|
+
number_of_rejections_by_inspection = sum(
|
|
154
|
+
get_number_of_rejections(histories_by_phase, phase=TaskPhase.INSPECTION, phase_stage=phase_stage) for phase_stage in range(current_phase_stage, number_of_inspections + 1)
|
|
155
|
+
)
|
|
156
|
+
return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
|
|
157
|
+
|
|
158
|
+
case _ as unreachable:
|
|
159
|
+
assert_noreturn(unreachable)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_df_task(
|
|
163
|
+
task_list: list[dict[str, Any]], task_history_dict: dict[str, list[dict[str, Any]]], not_worked_threshold_second: float = 0, metadata_keys: list[str] | None = None
|
|
164
|
+
) -> pandas.DataFrame:
|
|
165
|
+
"""
|
|
166
|
+
以下の列が含まれたタスクのDataFrameを生成します。
|
|
167
|
+
* task_id
|
|
168
|
+
* phase
|
|
169
|
+
* task_status_for_summary
|
|
170
|
+
* metadata.{key} (metadata_keysで指定された各キー)
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
task_list: タスク情報のlist
|
|
174
|
+
task_history_dict: タスクIDをキーとしたタスク履歴のdict
|
|
175
|
+
not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
|
|
176
|
+
metadata_keys: 集計対象のメタデータキーのリスト
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
タスク情報のDataFrame
|
|
180
|
+
"""
|
|
181
|
+
metadata_keys = metadata_keys or []
|
|
182
|
+
|
|
183
|
+
for task in task_list:
|
|
184
|
+
task_history = task_history_dict[task["task_id"]]
|
|
185
|
+
task["task_status_for_summary"] = TaskStatusForSummary.from_task(task, task_history, not_worked_threshold_second).value
|
|
186
|
+
|
|
187
|
+
# メタデータの値を抽出
|
|
188
|
+
metadata = task.get("metadata", {})
|
|
189
|
+
for key in metadata_keys:
|
|
190
|
+
task[f"metadata.{key}"] = metadata.get(key, "")
|
|
191
|
+
|
|
192
|
+
columns = ["task_id", "phase"] + [f"metadata.{key}" for key in metadata_keys] + ["task_status_for_summary"]
|
|
193
|
+
df = pandas.DataFrame(task_list, columns=columns)
|
|
194
|
+
return df
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def aggregate_df(df: pandas.DataFrame, metadata_keys: list[str] | None = None) -> pandas.DataFrame:
|
|
198
|
+
"""
|
|
199
|
+
タスク数を集計する。
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
df: 以下の列を持つDataFrame
|
|
203
|
+
* phase
|
|
204
|
+
* task_status_for_summary
|
|
205
|
+
* metadata.{key} (metadata_keysで指定された各キー)
|
|
206
|
+
metadata_keys: 集計対象のメタデータキーのリスト
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
indexがphase(とmetadata.*列),列がtask_status_for_summaryであるDataFrame
|
|
210
|
+
"""
|
|
211
|
+
metadata_keys = metadata_keys or []
|
|
212
|
+
metadata_columns = [f"metadata.{key}" for key in metadata_keys]
|
|
213
|
+
|
|
214
|
+
df["task_count"] = 1
|
|
215
|
+
index_columns = ["phase", *metadata_columns]
|
|
216
|
+
df2 = df.pivot_table(values="task_count", index=index_columns, columns="task_status_for_summary", aggfunc="sum", fill_value=0)
|
|
217
|
+
|
|
218
|
+
# 列数を固定する
|
|
219
|
+
for status in TaskStatusForSummary:
|
|
220
|
+
if status.value not in df2.columns:
|
|
221
|
+
df2[status.value] = 0
|
|
222
|
+
|
|
223
|
+
# ソート処理
|
|
224
|
+
sorted_phase = [
|
|
225
|
+
TaskPhase.ANNOTATION.value,
|
|
226
|
+
TaskPhase.INSPECTION.value,
|
|
227
|
+
TaskPhase.ACCEPTANCE.value,
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
if len(metadata_columns) > 0:
|
|
231
|
+
# メタデータ列がある場合は、phase列でソートし、その後メタデータ列でソート
|
|
232
|
+
df2 = df2.reset_index()
|
|
233
|
+
df2["_phase_order"] = df2["phase"].map(lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
|
|
234
|
+
df2 = df2.sort_values(["_phase_order", *metadata_columns])
|
|
235
|
+
df2 = df2.drop(columns=["_phase_order"])
|
|
236
|
+
else:
|
|
237
|
+
# メタデータ列がない場合は既存のロジック
|
|
238
|
+
new_index = sorted(df2.index, key=lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
|
|
239
|
+
df2 = df2.loc[new_index]
|
|
240
|
+
df2 = df2.reset_index()
|
|
241
|
+
|
|
242
|
+
# 列の順序を設定
|
|
243
|
+
result_columns = [
|
|
244
|
+
"phase",
|
|
245
|
+
*metadata_columns,
|
|
246
|
+
TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
|
|
247
|
+
TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
|
|
248
|
+
TaskStatusForSummary.WORKED_NOT_REJECTED.value,
|
|
249
|
+
TaskStatusForSummary.WORKED_REJECTED.value,
|
|
250
|
+
TaskStatusForSummary.ON_HOLD.value,
|
|
251
|
+
TaskStatusForSummary.COMPLETE.value,
|
|
252
|
+
]
|
|
253
|
+
df2 = df2[result_columns]
|
|
254
|
+
return df2
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class GettingTaskCountSummary:
|
|
258
|
+
"""
|
|
259
|
+
タスク数のサマリーを取得するクラス
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
annofab_service: AnnofabResource,
|
|
265
|
+
project_id: str,
|
|
266
|
+
*,
|
|
267
|
+
temp_dir: Path | None = None,
|
|
268
|
+
should_execute_get_tasks_api: bool = False,
|
|
269
|
+
not_worked_threshold_second: float = 0,
|
|
270
|
+
metadata_keys: list[str] | None = None,
|
|
271
|
+
) -> None:
|
|
272
|
+
self.annofab_service = annofab_service
|
|
273
|
+
self.project_id = project_id
|
|
274
|
+
self.temp_dir = temp_dir
|
|
275
|
+
self.should_execute_get_tasks_api = should_execute_get_tasks_api
|
|
276
|
+
self.not_worked_threshold_second = not_worked_threshold_second
|
|
277
|
+
self.metadata_keys = metadata_keys or []
|
|
278
|
+
|
|
279
|
+
def create_df_task(self) -> pandas.DataFrame:
|
|
280
|
+
"""
|
|
281
|
+
以下の列が含まれたタスクのDataFrameを生成します。
|
|
282
|
+
* task_id
|
|
283
|
+
* phase
|
|
284
|
+
* task_status_for_summary
|
|
285
|
+
* metadata.{key} (metadata_keys で指定した各メタデータキーに対応する列)
|
|
286
|
+
"""
|
|
287
|
+
if self.should_execute_get_tasks_api:
|
|
288
|
+
task_list = self.annofab_service.wrapper.get_all_tasks(self.project_id)
|
|
289
|
+
task_history_dict = {}
|
|
290
|
+
for index, task in enumerate(task_list, start=1):
|
|
291
|
+
task_id = task["task_id"]
|
|
292
|
+
task_history_dict[task_id], _ = self.annofab_service.api.get_task_histories(self.project_id, task_id)
|
|
293
|
+
if index % 100 == 0:
|
|
294
|
+
logger.info(f"{index} 件目のタスク履歴を取得しました。")
|
|
295
|
+
|
|
296
|
+
else:
|
|
297
|
+
task_list = self.get_task_list_with_downloading()
|
|
298
|
+
task_history_dict = self.get_task_history_with_downloading()
|
|
299
|
+
|
|
300
|
+
df = create_df_task(task_list, task_history_dict, self.not_worked_threshold_second, self.metadata_keys)
|
|
301
|
+
return df
|
|
302
|
+
|
|
303
|
+
def _get_task_list_with_downloading(self, temp_dir: Path) -> list[dict[str, Any]]:
|
|
304
|
+
downloading_obj = DownloadingFile(self.annofab_service)
|
|
305
|
+
task_json = downloading_obj.download_task_json_to_dir(self.project_id, temp_dir)
|
|
306
|
+
with task_json.open(encoding="utf-8") as f:
|
|
307
|
+
return json.load(f)
|
|
308
|
+
|
|
309
|
+
def _get_task_history_with_downloading(self, temp_dir: Path) -> dict[str, list[dict[str, Any]]]:
|
|
310
|
+
downloading_obj = DownloadingFile(self.annofab_service)
|
|
311
|
+
task_history_json = downloading_obj.download_task_history_json_to_dir(self.project_id, temp_dir)
|
|
312
|
+
with task_history_json.open(encoding="utf-8") as f:
|
|
313
|
+
return json.load(f)
|
|
314
|
+
|
|
315
|
+
def get_task_list_with_downloading(self) -> list[dict[str, Any]]:
|
|
316
|
+
"""
|
|
317
|
+
タスク全件ファイルをダウンロードしてタスク情報を取得する。
|
|
318
|
+
"""
|
|
319
|
+
if self.temp_dir is not None:
|
|
320
|
+
return self._get_task_list_with_downloading(self.temp_dir)
|
|
321
|
+
else:
|
|
322
|
+
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
323
|
+
return self._get_task_list_with_downloading(Path(str_temp_dir))
|
|
324
|
+
|
|
325
|
+
def get_task_history_with_downloading(self) -> dict[str, list[dict[str, Any]]]:
|
|
326
|
+
"""
|
|
327
|
+
タスク履歴全件ファイルをダウンロードしてタスク情報を取得する。
|
|
328
|
+
"""
|
|
329
|
+
if self.temp_dir is not None:
|
|
330
|
+
return self._get_task_history_with_downloading(self.temp_dir)
|
|
331
|
+
else:
|
|
332
|
+
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
333
|
+
return self._get_task_history_with_downloading(Path(str_temp_dir))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ListTaskCountByPhase(CommandLine):
|
|
337
|
+
"""
|
|
338
|
+
フェーズごとのタスク数を一覧表示する。
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def list_task_count_by_phase(
|
|
342
|
+
self, project_id: str, *, temp_dir: Path | None = None, should_execute_get_tasks_api: bool = False, not_worked_threshold_second: float = 0, metadata_keys: list[str] | None = None
|
|
343
|
+
) -> None:
|
|
344
|
+
"""
|
|
345
|
+
フェーズごとのタスク数をCSV形式で出力する。
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
project_id: プロジェクトID
|
|
349
|
+
temp_dir: 一時ファイルの保存先ディレクトリ
|
|
350
|
+
should_execute_get_tasks_api: getTasks APIを実行するかどうか
|
|
351
|
+
not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
|
|
352
|
+
metadata_keys: 集計対象のメタデータキーのリスト
|
|
353
|
+
"""
|
|
354
|
+
super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
|
|
355
|
+
|
|
356
|
+
logger.info(f"project_id='{project_id}' :: フェーズごとのタスク数を集計します。")
|
|
357
|
+
|
|
358
|
+
getting_obj = GettingTaskCountSummary(
|
|
359
|
+
self.service, project_id, temp_dir=temp_dir, should_execute_get_tasks_api=should_execute_get_tasks_api, not_worked_threshold_second=not_worked_threshold_second, metadata_keys=metadata_keys
|
|
360
|
+
)
|
|
361
|
+
df_task = getting_obj.create_df_task()
|
|
362
|
+
|
|
363
|
+
if len(df_task) == 0:
|
|
364
|
+
logger.info("タスクが0件ですが、ヘッダ行を出力します。")
|
|
365
|
+
# aggregate_df関数と同じ列構成の空のDataFrameを作成
|
|
366
|
+
metadata_columns = [f"metadata.{key}" for key in (metadata_keys or [])]
|
|
367
|
+
result_columns = [
|
|
368
|
+
"phase",
|
|
369
|
+
*metadata_columns,
|
|
370
|
+
TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
|
|
371
|
+
TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
|
|
372
|
+
TaskStatusForSummary.WORKED_NOT_REJECTED.value,
|
|
373
|
+
TaskStatusForSummary.WORKED_REJECTED.value,
|
|
374
|
+
TaskStatusForSummary.ON_HOLD.value,
|
|
375
|
+
TaskStatusForSummary.COMPLETE.value,
|
|
376
|
+
]
|
|
377
|
+
df_summary = pandas.DataFrame(columns=result_columns)
|
|
378
|
+
else:
|
|
379
|
+
logger.info(f"{len(df_task)} 件のタスクを集計しました。")
|
|
380
|
+
df_summary = aggregate_df(df_task, metadata_keys)
|
|
381
|
+
|
|
382
|
+
self.print_csv(df_summary)
|
|
383
|
+
logger.info(f"project_id='{project_id}' :: フェーズごとのタスク数をCSV形式で出力しました。")
|
|
384
|
+
|
|
385
|
+
def main(self) -> None:
|
|
386
|
+
args = self.args
|
|
387
|
+
project_id = args.project_id
|
|
388
|
+
temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
|
|
389
|
+
|
|
390
|
+
self.list_task_count_by_phase(
|
|
391
|
+
project_id,
|
|
392
|
+
temp_dir=temp_dir,
|
|
393
|
+
should_execute_get_tasks_api=args.execute_get_tasks_api,
|
|
394
|
+
not_worked_threshold_second=args.not_worked_threshold_second,
|
|
395
|
+
metadata_keys=args.metadata_key,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
400
|
+
argument_parser = ArgumentParser(parser)
|
|
401
|
+
|
|
402
|
+
argument_parser.add_project_id()
|
|
403
|
+
|
|
404
|
+
parser.add_argument(
|
|
405
|
+
"--execute_get_tasks_api",
|
|
406
|
+
action="store_true",
|
|
407
|
+
help="タスク全件ファイルをダウンロードせずに、`getTasks` APIを実行してタスク一覧を取得します。`getTasks` APIを複数回実行するので、タスク全件ファイルをダウンロードするよりも時間がかかります。",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"--temp_dir",
|
|
412
|
+
type=str,
|
|
413
|
+
help="指定したディレクトリに、一時ファイルをダウンロードします。",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"--not_worked_threshold_second",
|
|
418
|
+
type=float,
|
|
419
|
+
default=0,
|
|
420
|
+
help="作業していないとみなす作業時間の閾値を秒単位で指定します。この値以下の作業時間のタスクは、作業していないとみなします。",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
parser.add_argument(
|
|
424
|
+
"--metadata_key",
|
|
425
|
+
type=str,
|
|
426
|
+
nargs="+",
|
|
427
|
+
help="集計対象のメタデータキーを指定します。指定したキーの値でグループ化してタスク数を集計します。",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
argument_parser.add_output()
|
|
431
|
+
|
|
432
|
+
parser.set_defaults(subcommand_func=main)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def main(args: argparse.Namespace) -> None:
|
|
436
|
+
service = build_annofabapi_resource_and_login(args)
|
|
437
|
+
facade = AnnofabApiFacade(service)
|
|
438
|
+
ListTaskCountByPhase(service, facade, args).main()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
|
|
442
|
+
subcommand_name = "list_by_phase"
|
|
443
|
+
subcommand_help = "フェーズごとのタスク数をCSV形式で出力します。"
|
|
444
|
+
epilog = "オーナロールまたはアノテーションユーザーロールを持つユーザで実行してください。"
|
|
445
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
446
|
+
parse_args(parser)
|
|
447
|
+
return parser
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import annofabcli.common.cli
|
|
4
|
+
import annofabcli.task_count.list_by_phase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
8
|
+
subparsers = parser.add_subparsers(dest="subcommand_name")
|
|
9
|
+
|
|
10
|
+
# サブコマンドの定義
|
|
11
|
+
annofabcli.task_count.list_by_phase.add_parser(subparsers)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
|
|
15
|
+
subcommand_name = "task_count"
|
|
16
|
+
subcommand_help = "タスク数関係のサブコマンド"
|
|
17
|
+
description = "タスク数関係のサブコマンド"
|
|
18
|
+
|
|
19
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, is_subcommand=False)
|
|
20
|
+
parse_args(parser)
|
|
21
|
+
return parser
|
|
@@ -39,21 +39,34 @@ class ListTaskHistoryWithJsonMain:
|
|
|
39
39
|
filtered_task_history_dict[task_id] = task_history_list
|
|
40
40
|
return filtered_task_history_dict
|
|
41
41
|
|
|
42
|
-
def get_task_history_dict(self, project_id: str, task_history_json: Path | None = None, task_id_list: list[str] | None = None) -> TaskHistoryDict:
|
|
42
|
+
def get_task_history_dict(self, project_id: str, task_history_json: Path | None = None, task_id_list: list[str] | None = None, temp_dir: Path | None = None) -> TaskHistoryDict:
|
|
43
43
|
"""出力対象のタスク履歴情報を取得する"""
|
|
44
44
|
if task_history_json is None:
|
|
45
45
|
downloading_obj = DownloadingFile(self.service)
|
|
46
46
|
# `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
|
|
47
47
|
# https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
|
|
48
|
-
|
|
49
|
-
tmp_json_path =
|
|
50
|
-
|
|
51
|
-
with
|
|
52
|
-
|
|
48
|
+
if temp_dir is not None:
|
|
49
|
+
tmp_json_path = downloading_obj.download_task_history_json_to_dir(project_id, temp_dir)
|
|
50
|
+
else:
|
|
51
|
+
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
52
|
+
tmp_json_path = downloading_obj.download_task_history_json_to_dir(project_id, Path(str_temp_dir))
|
|
53
|
+
with tmp_json_path.open(encoding="utf-8") as f:
|
|
54
|
+
all_task_history_dict = json.load(f)
|
|
55
|
+
# 一時ディレクトリの場合はここでフィルタリング処理まで行う
|
|
56
|
+
task_history_dict = self.filter_task_history_dict(all_task_history_dict, task_id_list)
|
|
57
|
+
|
|
58
|
+
visualize = AddProps(self.service, project_id)
|
|
53
59
|
|
|
60
|
+
for task_history_list in task_history_dict.values():
|
|
61
|
+
for task_history in task_history_list:
|
|
62
|
+
visualize.add_properties_to_task_history(task_history)
|
|
63
|
+
|
|
64
|
+
return task_history_dict
|
|
54
65
|
else:
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
tmp_json_path = task_history_json
|
|
67
|
+
|
|
68
|
+
with tmp_json_path.open(encoding="utf-8") as f:
|
|
69
|
+
all_task_history_dict = json.load(f)
|
|
57
70
|
|
|
58
71
|
task_history_dict = self.filter_task_history_dict(all_task_history_dict, task_id_list)
|
|
59
72
|
|
|
@@ -80,6 +93,7 @@ class ListTaskHistoryWithJson(CommandLine):
|
|
|
80
93
|
task_history_json: Path | None,
|
|
81
94
|
task_id_list: list[str] | None,
|
|
82
95
|
arg_format: FormatArgument,
|
|
96
|
+
temp_dir: Path | None,
|
|
83
97
|
):
|
|
84
98
|
"""
|
|
85
99
|
タスク一覧を出力する
|
|
@@ -95,7 +109,7 @@ class ListTaskHistoryWithJson(CommandLine):
|
|
|
95
109
|
super().validate_project(project_id, project_member_roles=None)
|
|
96
110
|
|
|
97
111
|
main_obj = ListTaskHistoryWithJsonMain(self.service)
|
|
98
|
-
task_history_dict = main_obj.get_task_history_dict(project_id, task_history_json=task_history_json, task_id_list=task_id_list)
|
|
112
|
+
task_history_dict = main_obj.get_task_history_dict(project_id, task_history_json=task_history_json, task_id_list=task_id_list, temp_dir=temp_dir)
|
|
99
113
|
logger.debug(f"{len(task_history_dict)} 件のタスクの履歴情報を出力します。")
|
|
100
114
|
if arg_format == FormatArgument.CSV:
|
|
101
115
|
all_task_history_list = main_obj.to_all_task_history_list_from_dict(task_history_dict)
|
|
@@ -107,12 +121,14 @@ class ListTaskHistoryWithJson(CommandLine):
|
|
|
107
121
|
args = self.args
|
|
108
122
|
|
|
109
123
|
task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
|
|
124
|
+
temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
|
|
110
125
|
|
|
111
126
|
self.print_task_history_list(
|
|
112
127
|
args.project_id,
|
|
113
128
|
task_history_json=args.task_history_json,
|
|
114
129
|
task_id_list=task_id_list,
|
|
115
130
|
arg_format=FormatArgument(args.format),
|
|
131
|
+
temp_dir=temp_dir,
|
|
116
132
|
)
|
|
117
133
|
|
|
118
134
|
|
|
@@ -141,6 +157,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
141
157
|
"JSONファイルは ``$ annofabcli task_history download`` コマンドで取得できます。",
|
|
142
158
|
)
|
|
143
159
|
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--temp_dir",
|
|
162
|
+
type=str,
|
|
163
|
+
help="``--task_history_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
|
|
164
|
+
)
|
|
165
|
+
|
|
144
166
|
argument_parser.add_format(
|
|
145
167
|
choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
|
|
146
168
|
default=FormatArgument.CSV,
|
|
@@ -43,20 +43,32 @@ class ListTaskHistoryEventWithJsonMain:
|
|
|
43
43
|
|
|
44
44
|
return task_history_event_list
|
|
45
45
|
|
|
46
|
-
def get_task_history_event_list(self, project_id: str, task_history_event_json: Path | None = None, task_id_list: list[str] | None = None) -> list[dict[str, Any]]:
|
|
46
|
+
def get_task_history_event_list(self, project_id: str, task_history_event_json: Path | None = None, task_id_list: list[str] | None = None, temp_dir: Path | None = None) -> list[dict[str, Any]]:
|
|
47
47
|
if task_history_event_json is None:
|
|
48
48
|
downloading_obj = DownloadingFile(self.service)
|
|
49
49
|
# `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
|
|
50
50
|
# https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
|
|
51
|
-
|
|
52
|
-
tmp_json_file =
|
|
53
|
-
|
|
54
|
-
with
|
|
55
|
-
|
|
51
|
+
if temp_dir is not None:
|
|
52
|
+
tmp_json_file = downloading_obj.download_task_history_event_json_to_dir(project_id, temp_dir)
|
|
53
|
+
else:
|
|
54
|
+
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
55
|
+
tmp_json_file = downloading_obj.download_task_history_event_json_to_dir(project_id, Path(str_temp_dir))
|
|
56
|
+
with tmp_json_file.open(encoding="utf-8") as f:
|
|
57
|
+
all_task_history_event_list = json.load(f)
|
|
58
|
+
# 一時ディレクトリの場合はここでフィルタリング処理まで行う
|
|
59
|
+
filtered_task_history_event_list = self.filter_task_history_event(all_task_history_event_list, task_id_list)
|
|
60
|
+
|
|
61
|
+
visualize = AddProps(self.service, project_id)
|
|
56
62
|
|
|
63
|
+
for event in filtered_task_history_event_list:
|
|
64
|
+
visualize.add_properties_to_task_history_event(event)
|
|
65
|
+
|
|
66
|
+
return filtered_task_history_event_list
|
|
57
67
|
else:
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
tmp_json_file = task_history_event_json
|
|
69
|
+
|
|
70
|
+
with tmp_json_file.open(encoding="utf-8") as f:
|
|
71
|
+
all_task_history_event_list = json.load(f)
|
|
60
72
|
|
|
61
73
|
filtered_task_history_event_list = self.filter_task_history_event(all_task_history_event_list, task_id_list)
|
|
62
74
|
|
|
@@ -75,11 +87,12 @@ class ListTaskHistoryEventWithJson(CommandLine):
|
|
|
75
87
|
task_history_event_json: Path | None,
|
|
76
88
|
task_id_list: list[str] | None,
|
|
77
89
|
arg_format: FormatArgument,
|
|
90
|
+
temp_dir: Path | None,
|
|
78
91
|
) -> None:
|
|
79
92
|
super().validate_project(project_id, project_member_roles=None)
|
|
80
93
|
|
|
81
94
|
main_obj = ListTaskHistoryEventWithJsonMain(self.service)
|
|
82
|
-
task_history_event_list = main_obj.get_task_history_event_list(project_id, task_history_event_json=task_history_event_json, task_id_list=task_id_list)
|
|
95
|
+
task_history_event_list = main_obj.get_task_history_event_list(project_id, task_history_event_json=task_history_event_json, task_id_list=task_id_list, temp_dir=temp_dir)
|
|
83
96
|
|
|
84
97
|
logger.debug(f"{len(task_history_event_list)} 件のタスク履歴イベントの情報を出力します。")
|
|
85
98
|
|
|
@@ -114,12 +127,14 @@ class ListTaskHistoryEventWithJson(CommandLine):
|
|
|
114
127
|
args = self.args
|
|
115
128
|
|
|
116
129
|
task_id_list = get_list_from_args(args.task_id) if args.task_id is not None else None
|
|
130
|
+
temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
|
|
117
131
|
|
|
118
132
|
self.print_task_history_event_list(
|
|
119
133
|
args.project_id,
|
|
120
134
|
task_history_event_json=args.task_history_event_json,
|
|
121
135
|
task_id_list=task_id_list,
|
|
122
136
|
arg_format=FormatArgument(args.format),
|
|
137
|
+
temp_dir=temp_dir,
|
|
123
138
|
)
|
|
124
139
|
|
|
125
140
|
@staticmethod
|
|
@@ -156,6 +171,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
156
171
|
"JSONファイルは ``$ annofabcli task_history_event download`` コマンドで取得できます。",
|
|
157
172
|
)
|
|
158
173
|
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--temp_dir",
|
|
176
|
+
type=str,
|
|
177
|
+
help="``--task_history_event_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
|
|
178
|
+
)
|
|
179
|
+
|
|
159
180
|
argument_parser.add_format(
|
|
160
181
|
choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
|
|
161
182
|
default=FormatArgument.CSV,
|