annofabcli 1.105.0__py3-none-any.whl → 1.106.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 +3 -1
- annofabcli/annotation/change_annotation_attributes_per_annotation.py +33 -10
- annofabcli/common/cli.py +0 -27
- annofabcli/common/pandas.py +4 -1
- annofabcli/statistics/list_annotation_area.py +6 -1
- annofabcli/statistics/list_annotation_attribute.py +4 -0
- annofabcli/statistics/list_annotation_attribute_filled_count.py +5 -0
- annofabcli/statistics/list_annotation_count.py +7 -0
- annofabcli/statistics/list_annotation_duration.py +7 -0
- annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +0 -4
- annofabcli/statistics/visualization/dataframe/task.py +21 -1
- annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +13 -0
- annofabcli/statistics/visualization/dataframe/user_performance.py +6 -0
- annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +21 -4
- annofabcli/statistics/visualization/dataframe/worktime_per_date.py +14 -8
- annofabcli/statistics/visualization/filtering_query.py +5 -1
- annofabcli/statistics/visualization/model.py +32 -0
- annofabcli/statistics/visualization/project_dir.py +7 -1
- annofabcli/statistics/visualize_statistics.py +3 -9
- annofabcli/statistics/visualize_video_duration.py +1 -1
- annofabcli/task/list_tasks_added_task_history.py +28 -7
- {annofabcli-1.105.0.dist-info → annofabcli-1.106.0.dist-info}/METADATA +1 -1
- {annofabcli-1.105.0.dist-info → annofabcli-1.106.0.dist-info}/RECORD +26 -26
- {annofabcli-1.105.0.dist-info → annofabcli-1.106.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.105.0.dist-info → annofabcli-1.106.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.105.0.dist-info → annofabcli-1.106.0.dist-info}/licenses/LICENSE +0 -0
annofabcli/__main__.py
CHANGED
|
@@ -38,7 +38,9 @@ def warn_pandas_copy_on_write() -> None:
|
|
|
38
38
|
pandas 3.0で予期しない挙動になるのを防ぐため。
|
|
39
39
|
https://pandas.pydata.org/docs/user_guide/copy_on_write.html
|
|
40
40
|
"""
|
|
41
|
-
|
|
41
|
+
tmp = pandas.__version__.split(".")
|
|
42
|
+
major = tmp[0]
|
|
43
|
+
minor = tmp[1]
|
|
42
44
|
if int(major) >= 2 and int(minor) >= 2:
|
|
43
45
|
pandas.options.mode.copy_on_write = "warn"
|
|
44
46
|
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any, Optional, Union
|
|
10
10
|
|
|
11
11
|
import annofabapi
|
|
12
|
+
import pandas
|
|
12
13
|
from annofabapi.models import ProjectMemberRole
|
|
13
14
|
from annofabapi.pydantic_models.task_status import TaskStatus
|
|
14
15
|
from pydantic import BaseModel
|
|
@@ -177,7 +178,8 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
|
|
|
177
178
|
anno_list: 各アノテーションの変更内容リスト
|
|
178
179
|
"""
|
|
179
180
|
changed_task_count = 0
|
|
180
|
-
|
|
181
|
+
total_failed_to_change_annotation_count = 0
|
|
182
|
+
total_succeed_to_change_annotation_count = 0
|
|
181
183
|
annotation_list_per_task_id_input_data_id = get_annotation_list_per_task_id_input_data_id(anno_list)
|
|
182
184
|
|
|
183
185
|
total_task_count = len(annotation_list_per_task_id_input_data_id)
|
|
@@ -185,13 +187,16 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
|
|
|
185
187
|
|
|
186
188
|
for task_id, input_data_dict in annotation_list_per_task_id_input_data_id.items():
|
|
187
189
|
is_changeable_task, succeed_to_change_annotation_count, failed_to_change_annotation_count = self.change_annotation_attributes_for_task(task_id, input_data_dict)
|
|
190
|
+
total_succeed_to_change_annotation_count += succeed_to_change_annotation_count
|
|
191
|
+
total_failed_to_change_annotation_count += failed_to_change_annotation_count
|
|
192
|
+
|
|
188
193
|
if is_changeable_task:
|
|
189
194
|
changed_task_count += 1
|
|
190
195
|
|
|
191
196
|
logger.info(
|
|
192
|
-
f"{
|
|
197
|
+
f"{total_succeed_to_change_annotation_count}/{total_annotation_count} 件のアノテーションの属性値を変更しました。 :: "
|
|
193
198
|
f"アノテーションが変更されたタスク数は {changed_task_count}/{total_task_count} 件です。"
|
|
194
|
-
f"{
|
|
199
|
+
f"{total_failed_to_change_annotation_count} 件のアノテーションは変更できませんでした。"
|
|
195
200
|
)
|
|
196
201
|
|
|
197
202
|
|
|
@@ -205,12 +210,23 @@ class ChangeAttributesPerAnnotation(CommandLine):
|
|
|
205
210
|
def main(self) -> None:
|
|
206
211
|
args = self.args
|
|
207
212
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
if args.json is not None:
|
|
214
|
+
annotation_items = get_json_from_args(args.json)
|
|
215
|
+
if not isinstance(annotation_items, list):
|
|
216
|
+
print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
217
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
218
|
+
|
|
219
|
+
target_annotation_list = [TargetAnnotation.model_validate(anno) for anno in annotation_items]
|
|
212
220
|
|
|
213
|
-
|
|
221
|
+
elif args.csv is not None:
|
|
222
|
+
df_input = pandas.read_csv(args.csv)
|
|
223
|
+
target_annotation_list = [
|
|
224
|
+
TargetAnnotation(task_id=e["task_id"], input_data_id=e["input_data_id"], annotation_id=e["annotation_id"], attributes=json.loads(e["attributes"]))
|
|
225
|
+
for e in df_input.to_dict(orient="records")
|
|
226
|
+
]
|
|
227
|
+
else:
|
|
228
|
+
print(f"{self.COMMON_MESSAGE} argument '--json' または '--csv' のいずれかを指定してください。", file=sys.stderr) # noqa: T201
|
|
229
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
214
230
|
|
|
215
231
|
project_id = args.project_id
|
|
216
232
|
|
|
@@ -243,13 +259,20 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
243
259
|
argument_parser.add_project_id()
|
|
244
260
|
|
|
245
261
|
sample_json_obj = [{"task_id": "t1", "input_data_id": "i1", "annotation_id": "a1", "attributes": {"occluded": True}}]
|
|
246
|
-
parser.
|
|
262
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
263
|
+
input_group.add_argument(
|
|
247
264
|
"--json",
|
|
248
265
|
type=str,
|
|
249
|
-
required=True,
|
|
250
266
|
help="各アノテーションごとに変更内容を記載したJSONリストを指定します。 ``file://`` を先頭に付けるとJSON形式のファイルを指定できます。\n"
|
|
251
267
|
f"(例) '{json.dumps(sample_json_obj, ensure_ascii=False)}'",
|
|
252
268
|
)
|
|
269
|
+
input_group.add_argument(
|
|
270
|
+
"--csv",
|
|
271
|
+
type=str,
|
|
272
|
+
help="各アノテーションごとに変更内容を記載したCSVファイルを指定します。 \n"
|
|
273
|
+
"* `task_id`, `input_data_id`, `annotation_id`, `attributes` の4つのカラムが必要です。\n"
|
|
274
|
+
f"`attributes` カラムには、属性名と値を '{json.dumps({'occluded': True})}' のようにJSON形式で指定します。\n",
|
|
275
|
+
)
|
|
253
276
|
|
|
254
277
|
parser.add_argument(
|
|
255
278
|
"--force",
|
annofabcli/common/cli.py
CHANGED
|
@@ -524,33 +524,6 @@ class CommandLineWithoutWebapi:
|
|
|
524
524
|
|
|
525
525
|
return yes
|
|
526
526
|
|
|
527
|
-
def confirm_processing_task(self, task_id: str, confirm_message: str) -> bool:
|
|
528
|
-
"""
|
|
529
|
-
タスクに対して処理するかどうか問い合わせる。
|
|
530
|
-
`all_yes`属性も設定する。
|
|
531
|
-
|
|
532
|
-
Args:
|
|
533
|
-
task_id: 処理するtask_id
|
|
534
|
-
confirm_message: 確認メッセージ
|
|
535
|
-
|
|
536
|
-
Returns:
|
|
537
|
-
Trueならば対象のタスクを処理する。
|
|
538
|
-
|
|
539
|
-
"""
|
|
540
|
-
if self.all_yes:
|
|
541
|
-
return True
|
|
542
|
-
|
|
543
|
-
yes, all_yes = prompt_yesnoall(confirm_message)
|
|
544
|
-
|
|
545
|
-
if not yes:
|
|
546
|
-
logger.info(f"task_id = {task_id} をスキップします。")
|
|
547
|
-
return False
|
|
548
|
-
|
|
549
|
-
if all_yes:
|
|
550
|
-
self.all_yes = True
|
|
551
|
-
|
|
552
|
-
return True
|
|
553
|
-
|
|
554
527
|
def print_csv(self, df: pandas.DataFrame) -> None:
|
|
555
528
|
print_csv(df, output=self.output)
|
|
556
529
|
|
annofabcli/common/pandas.py
CHANGED
|
@@ -8,7 +8,10 @@ def get_frequency_of_monthend() -> str:
|
|
|
8
8
|
|
|
9
9
|
https://github.com/pandas-dev/pandas/issues/9586
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
tmp = pandas.__version__.split(".")
|
|
12
|
+
major = tmp[0]
|
|
13
|
+
minor = tmp[1]
|
|
14
|
+
|
|
12
15
|
if int(major) >= 2 and int(minor) >= 2:
|
|
13
16
|
return "ME"
|
|
14
17
|
return "M"
|
|
@@ -64,6 +64,9 @@ class AnnotationAreaInfo(DataClassJsonMixin):
|
|
|
64
64
|
input_data_id: str
|
|
65
65
|
input_data_name: str
|
|
66
66
|
|
|
67
|
+
updated_datetime: Optional[str]
|
|
68
|
+
"""アノテーションJSONに格納されているアノテーションの更新日時"""
|
|
69
|
+
|
|
67
70
|
label: str
|
|
68
71
|
annotation_id: str
|
|
69
72
|
annotation_area: int
|
|
@@ -77,7 +80,7 @@ def calculate_bounding_box_area(data: dict[str, Any]) -> int:
|
|
|
77
80
|
|
|
78
81
|
def calculate_segmentation_area(outer_file: Path) -> int:
|
|
79
82
|
nd_array = read_binary_image(outer_file)
|
|
80
|
-
return numpy.count_nonzero(nd_array)
|
|
83
|
+
return int(numpy.count_nonzero(nd_array))
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotation: dict[str, Any]) -> list[AnnotationAreaInfo]:
|
|
@@ -103,6 +106,7 @@ def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotat
|
|
|
103
106
|
label=detail["label"],
|
|
104
107
|
annotation_id=detail["annotation_id"],
|
|
105
108
|
annotation_area=annotation_area,
|
|
109
|
+
updated_datetime=simple_annotation["updated_datetime"],
|
|
106
110
|
)
|
|
107
111
|
)
|
|
108
112
|
|
|
@@ -144,6 +148,7 @@ def create_df(
|
|
|
144
148
|
"task_phase_stage",
|
|
145
149
|
"input_data_id",
|
|
146
150
|
"input_data_name",
|
|
151
|
+
"updated_datetime",
|
|
147
152
|
"label",
|
|
148
153
|
"annotation_id",
|
|
149
154
|
"annotation_area",
|
|
@@ -65,6 +65,8 @@ class AnnotationAttribute(pydantic.BaseModel):
|
|
|
65
65
|
|
|
66
66
|
input_data_id: str
|
|
67
67
|
input_data_name: str
|
|
68
|
+
updated_datetime: Optional[str]
|
|
69
|
+
"""アノテーションJSONに格納されているアノテーションの更新日時"""
|
|
68
70
|
annotation_id: str
|
|
69
71
|
label: str
|
|
70
72
|
attributes: dict[str, Union[str, int, bool]]
|
|
@@ -98,6 +100,7 @@ def get_annotation_attribute_list_from_annotation_json(simple_annotation: dict[s
|
|
|
98
100
|
label=detail["label"],
|
|
99
101
|
annotation_id=detail["annotation_id"],
|
|
100
102
|
attributes=detail["attributes"],
|
|
103
|
+
updated_datetime=simple_annotation["updated_datetime"],
|
|
101
104
|
)
|
|
102
105
|
)
|
|
103
106
|
return result
|
|
@@ -150,6 +153,7 @@ def print_annotation_attribute_list_as_csv(annotation_attribute_list: list, outp
|
|
|
150
153
|
"task_phase_stage",
|
|
151
154
|
"input_data_id",
|
|
152
155
|
"input_data_name",
|
|
156
|
+
"updated_datetime",
|
|
153
157
|
"annotation_id",
|
|
154
158
|
"label",
|
|
155
159
|
]
|
|
@@ -108,6 +108,8 @@ class AnnotationCountByInputData(DataClassJsonMixin, HasAnnotationAttributeCount
|
|
|
108
108
|
|
|
109
109
|
input_data_id: str
|
|
110
110
|
input_data_name: str
|
|
111
|
+
updated_datetime: Optional[str]
|
|
112
|
+
"""アノテーションJSONに格納されているアノテーションの更新日時"""
|
|
111
113
|
|
|
112
114
|
annotation_attribute_counts: dict[AttributeValueKey, int] = field(
|
|
113
115
|
metadata=config(
|
|
@@ -275,6 +277,7 @@ class ListAnnotationCounterByInputData:
|
|
|
275
277
|
input_data_name=simple_annotation["input_data_name"],
|
|
276
278
|
annotation_attribute_counts=annotation_count_by_attribute,
|
|
277
279
|
frame_no=frame_no,
|
|
280
|
+
updated_datetime=simple_annotation["updated_datetime"],
|
|
278
281
|
)
|
|
279
282
|
|
|
280
283
|
def get_annotation_count_list(
|
|
@@ -355,6 +358,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
355
358
|
("input_data_id", "", ""),
|
|
356
359
|
("input_data_name", "", ""),
|
|
357
360
|
("frame_no", "", ""),
|
|
361
|
+
("updated_datetime", "", ""),
|
|
358
362
|
]
|
|
359
363
|
value_columns = self._value_columns(annotation_count_list, prior_attribute_columns=prior_attribute_columns)
|
|
360
364
|
return basic_columns + value_columns
|
|
@@ -390,6 +394,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
390
394
|
("task_phase_stage", "", ""): c.task_phase_stage,
|
|
391
395
|
("input_data_id", "", ""): c.input_data_id,
|
|
392
396
|
("input_data_name", "", ""): c.input_data_name,
|
|
397
|
+
("updated_datetime", "", ""): c.updated_datetime,
|
|
393
398
|
("frame_no", "", ""): c.frame_no,
|
|
394
399
|
}
|
|
395
400
|
cell.update(c.annotation_attribute_counts) # type: ignore[arg-type]
|
|
@@ -133,6 +133,8 @@ class AnnotationCounterByInputData(AnnotationCounter, DataClassJsonMixin):
|
|
|
133
133
|
|
|
134
134
|
input_data_id: str
|
|
135
135
|
input_data_name: str
|
|
136
|
+
updated_datetime: Optional[str]
|
|
137
|
+
"""アノテーションJSONに格納されているアノテーションの更新日時"""
|
|
136
138
|
frame_no: Optional[int] = None
|
|
137
139
|
"""アノテーションJSONには含まれていない情報なので、Optionalにする"""
|
|
138
140
|
|
|
@@ -263,6 +265,7 @@ class ListAnnotationCounterByInputData:
|
|
|
263
265
|
task_status=TaskStatus(simple_annotation["task_status"]),
|
|
264
266
|
input_data_id=simple_annotation["input_data_id"],
|
|
265
267
|
input_data_name=simple_annotation["input_data_name"],
|
|
268
|
+
updated_datetime=simple_annotation["updated_datetime"],
|
|
266
269
|
annotation_count=sum(annotation_count_by_label.values()),
|
|
267
270
|
annotation_count_by_label=annotation_count_by_label,
|
|
268
271
|
annotation_count_by_attribute=annotation_count_by_attribute,
|
|
@@ -520,6 +523,7 @@ class AttributeCountCsv:
|
|
|
520
523
|
("input_data_id", "", ""),
|
|
521
524
|
("input_data_name", "", ""),
|
|
522
525
|
("frame_no", "", ""),
|
|
526
|
+
("updated_datetime", "", ""),
|
|
523
527
|
("annotation_count", "", ""),
|
|
524
528
|
]
|
|
525
529
|
value_columns = self._value_columns(counter_list, prior_attribute_columns)
|
|
@@ -531,6 +535,7 @@ class AttributeCountCsv:
|
|
|
531
535
|
("input_data_id", "", ""): c.input_data_id,
|
|
532
536
|
("input_data_name", "", ""): c.input_data_name,
|
|
533
537
|
("frame_no", "", ""): c.frame_no,
|
|
538
|
+
("updated_datetime", "", ""): c.updated_datetime,
|
|
534
539
|
("task_id", "", ""): c.task_id,
|
|
535
540
|
("task_status", "", ""): c.task_status.value,
|
|
536
541
|
("task_phase", "", ""): c.task_phase.value,
|
|
@@ -625,6 +630,7 @@ class LabelCountCsv:
|
|
|
625
630
|
"input_data_id",
|
|
626
631
|
"input_data_name",
|
|
627
632
|
"frame_no",
|
|
633
|
+
"updated_datetime",
|
|
628
634
|
"annotation_count",
|
|
629
635
|
]
|
|
630
636
|
value_columns = self._value_columns(counter_list, prior_label_columns)
|
|
@@ -636,6 +642,7 @@ class LabelCountCsv:
|
|
|
636
642
|
"input_data_id": c.input_data_id,
|
|
637
643
|
"input_data_name": c.input_data_name,
|
|
638
644
|
"frame_no": c.frame_no,
|
|
645
|
+
"updated_datetime": c.updated_datetime,
|
|
639
646
|
"task_id": c.task_id,
|
|
640
647
|
"task_status": c.task_status.value,
|
|
641
648
|
"task_phase": c.task_phase.value,
|
|
@@ -116,6 +116,10 @@ class AnnotationDuration(DataClassJsonMixin):
|
|
|
116
116
|
|
|
117
117
|
input_data_id: str
|
|
118
118
|
input_data_name: str
|
|
119
|
+
|
|
120
|
+
updated_datetime: Optional[str]
|
|
121
|
+
"""アノテーションJSONに格納されているアノテーションの更新日時"""
|
|
122
|
+
|
|
119
123
|
video_duration_second: Optional[float]
|
|
120
124
|
"""動画の長さ[秒]"""
|
|
121
125
|
|
|
@@ -242,6 +246,7 @@ class ListAnnotationDurationByInputData:
|
|
|
242
246
|
task_status=TaskStatus(simple_annotation["task_status"]),
|
|
243
247
|
input_data_id=simple_annotation["input_data_id"],
|
|
244
248
|
input_data_name=simple_annotation["input_data_name"],
|
|
249
|
+
updated_datetime=simple_annotation["updated_datetime"],
|
|
245
250
|
video_duration_second=video_duration_second,
|
|
246
251
|
annotation_duration_second=sum(annotation_duration_by_label.values()),
|
|
247
252
|
annotation_duration_second_by_label=annotation_duration_by_label,
|
|
@@ -434,6 +439,7 @@ class AnnotationDurationCsvByLabel:
|
|
|
434
439
|
"task_phase_stage",
|
|
435
440
|
"input_data_id",
|
|
436
441
|
"input_data_name",
|
|
442
|
+
"updated_datetime",
|
|
437
443
|
"video_duration_second",
|
|
438
444
|
"annotation_duration_second",
|
|
439
445
|
]
|
|
@@ -456,6 +462,7 @@ class AnnotationDurationCsvByLabel:
|
|
|
456
462
|
"task_phase_stage": c.task_phase_stage,
|
|
457
463
|
"video_duration_second": c.video_duration_second,
|
|
458
464
|
"annotation_duration_second": c.annotation_duration_second,
|
|
465
|
+
"updated_datetime": c.updated_datetime,
|
|
459
466
|
}
|
|
460
467
|
d.update(c.annotation_duration_second_by_label)
|
|
461
468
|
return d
|
|
@@ -62,10 +62,6 @@ def _create_cumulative_dataframe(task_worktime_by_phase_user: TaskWorktimeByPhas
|
|
|
62
62
|
# `YYYY-MM-DDThh:mm:ss.sss+09:00`から`YYYY-MM-DD`を取得する
|
|
63
63
|
# キャストする理由: 全部nanだとfloat型になって、".str"にアクセスできないため
|
|
64
64
|
df[f"first_{str_phase}_started_date"] = df["started_datetime"].astype("string").str[:10]
|
|
65
|
-
# bokeh3.0.3では、string型の列を持つpandas.DataFrameを描画できないため、改めてobject型に戻す
|
|
66
|
-
# TODO この問題が解決されたら、削除する
|
|
67
|
-
# https://qiita.com/yuji38kwmt/items/b5da6ed521e827620186
|
|
68
|
-
df[f"first_{str_phase}_started_date"] = df[f"first_{str_phase}_started_date"].astype("object")
|
|
69
65
|
return df
|
|
70
66
|
|
|
71
67
|
|
|
@@ -94,6 +94,21 @@ class Task:
|
|
|
94
94
|
df["acceptance_worktime_hour"] = 0
|
|
95
95
|
return Task(df, custom_production_volume_list=self.custom_production_volume_list)
|
|
96
96
|
|
|
97
|
+
def to_non_inspection_acceptance(self) -> Task:
|
|
98
|
+
"""
|
|
99
|
+
検査フェーズと受入フェーズの作業時間を0にした新しいインスタンスを生成します。
|
|
100
|
+
|
|
101
|
+
`--task_completion_criteria inspection_reached`を指定したときに利用します。
|
|
102
|
+
この場合、検査/受入フェーズの作業時間を無視して集計する必要があるためです。
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
df = self.df.copy()
|
|
106
|
+
df["first_inspection_worktime_hour"] = 0
|
|
107
|
+
df["first_acceptance_worktime_hour"] = 0
|
|
108
|
+
df["inspection_worktime_hour"] = 0
|
|
109
|
+
df["acceptance_worktime_hour"] = 0
|
|
110
|
+
return Task(df, custom_production_volume_list=self.custom_production_volume_list)
|
|
111
|
+
|
|
97
112
|
@property
|
|
98
113
|
def optional_columns(self) -> list[str]:
|
|
99
114
|
return [
|
|
@@ -104,6 +119,9 @@ class Task:
|
|
|
104
119
|
"post_rejection_annotation_worktime_hour",
|
|
105
120
|
"post_rejection_inspection_worktime_hour",
|
|
106
121
|
"post_rejection_acceptance_worktime_hour",
|
|
122
|
+
# 作業時間に関する内容
|
|
123
|
+
"completed_datetime",
|
|
124
|
+
"operation_updated_datetime",
|
|
107
125
|
]
|
|
108
126
|
|
|
109
127
|
@property
|
|
@@ -134,7 +152,8 @@ class Task:
|
|
|
134
152
|
"first_acceptance_username",
|
|
135
153
|
"first_acceptance_worktime_hour",
|
|
136
154
|
"first_acceptance_started_datetime",
|
|
137
|
-
#
|
|
155
|
+
# 初めて~になった日時
|
|
156
|
+
"first_inspection_reached_datetime",
|
|
138
157
|
"first_acceptance_reached_datetime",
|
|
139
158
|
"first_acceptance_completed_datetime",
|
|
140
159
|
# 作業時間に関する内容
|
|
@@ -254,6 +273,7 @@ class Task:
|
|
|
254
273
|
"first_annotation_user_id",
|
|
255
274
|
"first_annotation_username",
|
|
256
275
|
"first_annotation_started_datetime",
|
|
276
|
+
"first_inspection_reached_datetime",
|
|
257
277
|
"first_inspection_user_id",
|
|
258
278
|
"first_inspection_username",
|
|
259
279
|
"first_inspection_started_datetime",
|
|
@@ -63,6 +63,19 @@ class TaskWorktimeByPhaseUser:
|
|
|
63
63
|
df = df[df["phase"] != TaskPhase.ACCEPTANCE.value]
|
|
64
64
|
return TaskWorktimeByPhaseUser(df, custom_production_volume_list=self.custom_production_volume_list)
|
|
65
65
|
|
|
66
|
+
def to_non_inspection_acceptance(self) -> TaskWorktimeByPhaseUser:
|
|
67
|
+
"""
|
|
68
|
+
検査/受入フェーズの作業時間を0にした新しいインスタンスを生成します。
|
|
69
|
+
|
|
70
|
+
`--task_completion_criteria inspection_reached`を指定したときに利用します。
|
|
71
|
+
この場合、検査/受入フェーズの作業時間を無視して集計する必要があるためです。
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
df = self.df.copy()
|
|
75
|
+
# 教師付フェーズのみ残す
|
|
76
|
+
df = df[df["phase"] == TaskPhase.ANNOTATION.value]
|
|
77
|
+
return TaskWorktimeByPhaseUser(df, custom_production_volume_list=self.custom_production_volume_list)
|
|
78
|
+
|
|
66
79
|
@property
|
|
67
80
|
def columns(self) -> list[str]:
|
|
68
81
|
"""
|
|
@@ -553,6 +553,12 @@ class UserPerformance:
|
|
|
553
553
|
worktime_per_date = worktime_per_date.to_non_acceptance()
|
|
554
554
|
task_worktime_by_phase_user = task_worktime_by_phase_user.to_non_acceptance()
|
|
555
555
|
|
|
556
|
+
elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
557
|
+
# 検査フェーズに到達したらタスクの作業が完了したとみなす場合、
|
|
558
|
+
# 検査フェーズと受入フェーズの作業時間や生産量は不要な情報なので、検査作業時間と受入作業時間を0にする
|
|
559
|
+
worktime_per_date = worktime_per_date.to_non_inspection_acceptance()
|
|
560
|
+
task_worktime_by_phase_user = task_worktime_by_phase_user.to_non_inspection_acceptance()
|
|
561
|
+
|
|
556
562
|
# 実際の計測作業時間情報(集計タスクに影響されない作業時間)と実績作業時間を算出する
|
|
557
563
|
df = cls._create_df_real_worktime(worktime_per_date)
|
|
558
564
|
|
|
@@ -156,10 +156,12 @@ class WholeProductivityPerCompletedDate:
|
|
|
156
156
|
monitored_inspection_worktime_hour
|
|
157
157
|
monitored_acceptance_worktime_hour
|
|
158
158
|
"""
|
|
159
|
-
|
|
160
159
|
if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
161
160
|
# 受入フェーズに到達したらタスクの作業が完了したとみなす場合、受入フェーズの作業時間や生産量は不要な情報なので、受入作業時間を0にする
|
|
162
161
|
worktime_per_date = worktime_per_date.to_non_acceptance()
|
|
162
|
+
if task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
163
|
+
# 検査フェーズに到達したらタスクの作業が完了したとみなす場合、検査/受入フェーズの作業時間や生産量は不要な情報なので、検査/受入作業時間を0にする
|
|
164
|
+
worktime_per_date = worktime_per_date.to_non_inspection_acceptance()
|
|
163
165
|
|
|
164
166
|
# 生産量を表す列名
|
|
165
167
|
production_volume_columns = ["input_data_count", "annotation_count", *[e.value for e in task.custom_production_volume_list]]
|
|
@@ -168,6 +170,8 @@ class WholeProductivityPerCompletedDate:
|
|
|
168
170
|
datetime_column = "first_acceptance_completed_datetime"
|
|
169
171
|
elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
170
172
|
datetime_column = "first_acceptance_reached_datetime"
|
|
173
|
+
elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
174
|
+
datetime_column = "first_inspection_reached_datetime"
|
|
171
175
|
else:
|
|
172
176
|
assert_noreturn(task_completion_criteria)
|
|
173
177
|
|
|
@@ -271,15 +275,19 @@ class WholeProductivityPerCompletedDate:
|
|
|
271
275
|
HTMLページの先頭に付与するdiv要素を生成する。
|
|
272
276
|
"""
|
|
273
277
|
if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
|
|
274
|
-
str_task = "
|
|
278
|
+
str_task = "受入フェーズ完了状態になった"
|
|
275
279
|
elif self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
276
|
-
str_task = "
|
|
280
|
+
str_task = "受入フェーズに到達した"
|
|
281
|
+
elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
282
|
+
str_task = "検査フェーズに到達した"
|
|
277
283
|
else:
|
|
278
284
|
assert_noreturn(self.task_completion_criteria)
|
|
279
285
|
|
|
280
|
-
elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に初めて{str_task}
|
|
286
|
+
elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に初めて{str_task}タスクの個数です。</p>"]
|
|
281
287
|
if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
282
288
|
elm_list.append("<p>タスクが受入フェーズに到達したら作業が完了したとみなしているため、受入作業時間は0にしています。</p>")
|
|
289
|
+
elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
290
|
+
elm_list.append("<p>タスクが検査フェーズに到達したら作業が完了したとみなしているため、検査作業時間と受入作業時間は0にしています。</p>")
|
|
283
291
|
return Div(text=" ".join(elm_list))
|
|
284
292
|
|
|
285
293
|
def plot(
|
|
@@ -865,12 +873,17 @@ class WholeProductivityPerFirstAnnotationStartedDate:
|
|
|
865
873
|
if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
866
874
|
# 受入フェーズに到達したらタスクの作業が完了したとみなす場合、受入フェーズの作業時間や生産量は不要な情報なので、受入作業時間を0にする
|
|
867
875
|
task = task.to_non_acceptance()
|
|
876
|
+
elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
877
|
+
# 検査フェーズに到達したらタスクの作業が完了したとみなす場合、検査/受入フェーズの作業時間や生産量は不要な情報なので、検査/受入作業時間を0にする
|
|
878
|
+
task = task.to_non_inspection_acceptance()
|
|
868
879
|
|
|
869
880
|
df_task = task.df
|
|
870
881
|
if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
|
|
871
882
|
df_sub_task = df_task[df_task["status"] == TaskStatus.COMPLETE.value]
|
|
872
883
|
elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
873
884
|
df_sub_task = df_task[df_task["phase"] == TaskPhase.ACCEPTANCE.value]
|
|
885
|
+
elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
886
|
+
df_sub_task = df_task[df_task["phase"].isin([TaskPhase.INSPECTION.value, TaskPhase.ACCEPTANCE.value])]
|
|
874
887
|
else:
|
|
875
888
|
assert_noreturn(task_completion_criteria)
|
|
876
889
|
|
|
@@ -998,12 +1011,16 @@ class WholeProductivityPerFirstAnnotationStartedDate:
|
|
|
998
1011
|
str_task = "受入フェーズ完了状態"
|
|
999
1012
|
elif self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
1000
1013
|
str_task = "受入フェーズ"
|
|
1014
|
+
elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
1015
|
+
str_task = "検査フェーズまたは受入フェーズ"
|
|
1001
1016
|
else:
|
|
1002
1017
|
assert_noreturn(self.task_completion_criteria)
|
|
1003
1018
|
|
|
1004
1019
|
elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に教師付フェーズを開始したタスクの内、{str_task}であるタスクの個数です。</p>"]
|
|
1005
1020
|
if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
1006
1021
|
elm_list.append("<p>タスクが受入フェーズに到達したら作業が完了したとみなしているため、受入作業時間は0にしています。</p>")
|
|
1022
|
+
elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
1023
|
+
elm_list.append("<p>タスクが検査フェーズまたは受入フェーズに到達したら作業が完了したとみなしているため、検査作業時間と受入作業時間は0にしています。</p>")
|
|
1007
1024
|
|
|
1008
1025
|
return Div(text=" ".join(elm_list))
|
|
1009
1026
|
|
|
@@ -14,7 +14,6 @@ import annofabapi
|
|
|
14
14
|
import bokeh
|
|
15
15
|
import bokeh.layouts
|
|
16
16
|
import bokeh.palettes
|
|
17
|
-
import numpy
|
|
18
17
|
import pandas
|
|
19
18
|
from bokeh.models.ui import UIElement
|
|
20
19
|
from bokeh.plotting import ColumnDataSource
|
|
@@ -113,6 +112,20 @@ class WorktimePerDate:
|
|
|
113
112
|
df["monitored_worktime_hour"] = df["monitored_annotation_worktime_hour"] + df["monitored_inspection_worktime_hour"] + df["monitored_acceptance_worktime_hour"]
|
|
114
113
|
return WorktimePerDate(df)
|
|
115
114
|
|
|
115
|
+
def to_non_inspection_acceptance(self) -> WorktimePerDate:
|
|
116
|
+
"""
|
|
117
|
+
検査フェーズと受入フェーズの作業時間を0にした新しいインスタンスを生成します。
|
|
118
|
+
|
|
119
|
+
`--task_completion_criteria inspection_reached`を指定したときに利用します。
|
|
120
|
+
この場合、検査フェーズと受入フェーズの作業時間を無視して集計する必要があるためです。
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
df = self.df.copy()
|
|
124
|
+
df["monitored_inspection_worktime_hour"] = 0
|
|
125
|
+
df["monitored_acceptance_worktime_hour"] = 0
|
|
126
|
+
df["monitored_worktime_hour"] = df["monitored_annotation_worktime_hour"]
|
|
127
|
+
return WorktimePerDate(df)
|
|
128
|
+
|
|
116
129
|
def is_empty(self) -> bool:
|
|
117
130
|
"""
|
|
118
131
|
空のデータフレームを持つかどうかを返します。
|
|
@@ -442,13 +455,6 @@ class WorktimePerDate:
|
|
|
442
455
|
df_cumulative = self._get_cumulative_dataframe(df_continuous_date)
|
|
443
456
|
df_cumulative["dt_date"] = df_cumulative["date"].map(lambda e: datetime.datetime.fromisoformat(e).date())
|
|
444
457
|
|
|
445
|
-
# bokeh3.0.3では、string型の列を持つpandas.DataFrameを描画できないため、改めてobject型に戻す
|
|
446
|
-
# またpandas.NAを持つDataFrameも描画できないので、numpy.nanに変換する
|
|
447
|
-
# TODO この問題が解決されたら、削除する
|
|
448
|
-
# https://qiita.com/yuji38kwmt/items/b5da6ed521e827620186
|
|
449
|
-
df_cumulative = df_cumulative.astype({"date": "object", "account_id": "object", "user_id": "object", "username": "object", "biography": "object"})
|
|
450
|
-
df_cumulative.replace(pandas.NA, numpy.nan, inplace=True)
|
|
451
|
-
|
|
452
458
|
line_count = 0
|
|
453
459
|
plotted_users: list[tuple[str, str]] = []
|
|
454
460
|
for user_index, user_id in enumerate(user_id_list):
|
|
@@ -10,6 +10,7 @@ from annofabapi.models import Task
|
|
|
10
10
|
|
|
11
11
|
from annofabcli.common.facade import TaskQuery, match_task_with_query
|
|
12
12
|
from annofabcli.common.utils import isoduration_to_hour
|
|
13
|
+
from annofabcli.statistics.visualization.model import TaskCompletionCriteria
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -72,7 +73,7 @@ def filter_task_histories(task_histories: dict[str, list[dict[str, Any]]], *, st
|
|
|
72
73
|
return {task_id: sub_task_history_list for task_id, sub_task_history_list in task_histories.items() if pred(sub_task_history_list)}
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
def filter_tasks(tasks: list[dict[str, Any]], query: FilteringQuery,
|
|
76
|
+
def filter_tasks(tasks: list[dict[str, Any]], task_completion_criteria: TaskCompletionCriteria, query: FilteringQuery, task_histories: dict[str, list[dict[str, Any]]]) -> list[Task]:
|
|
76
77
|
"""
|
|
77
78
|
タスク一覧を絞り込みます。
|
|
78
79
|
|
|
@@ -95,6 +96,9 @@ def filter_tasks(tasks: list[dict[str, Any]], query: FilteringQuery, *, task_his
|
|
|
95
96
|
"""
|
|
96
97
|
|
|
97
98
|
flag = True
|
|
99
|
+
|
|
100
|
+
flag = flag and task_completion_criteria.is_task_completed(arg_task)
|
|
101
|
+
|
|
98
102
|
if query.task_query is not None:
|
|
99
103
|
flag = flag and match_task_with_query(DcTask.from_dict(arg_task), query.task_query)
|
|
100
104
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from annofabapi.pydantic_models.task_phase import TaskPhase
|
|
6
|
+
from annofabapi.pydantic_models.task_status import TaskStatus
|
|
7
|
+
|
|
8
|
+
from annofabcli.common.type_util import assert_noreturn
|
|
3
9
|
|
|
4
10
|
|
|
5
11
|
class WorktimeColumn(Enum):
|
|
@@ -32,3 +38,29 @@ class TaskCompletionCriteria(Enum):
|
|
|
32
38
|
"""タスクが受入フェーズの完了状態であれば「タスクの完了」とみなす"""
|
|
33
39
|
ACCEPTANCE_REACHED = "acceptance_reached"
|
|
34
40
|
"""タスクが受入フェーズに到達したら「タスクの完了」とみなす"""
|
|
41
|
+
INSPECTION_REACHED = "inspection_reached"
|
|
42
|
+
"""タスクが検査フェーズに到達したら「タスクの完了」とみなす"""
|
|
43
|
+
|
|
44
|
+
def is_task_completed(self, task: dict[str, Any]) -> bool:
|
|
45
|
+
"""指定したタスクが、タスクの完了条件に合致するかどうかを判定します。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
task: タスク情報。以下のキーを参照します。
|
|
49
|
+
* phase
|
|
50
|
+
* status
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
タスクの完了条件に合致する場合はTrue、そうでない場合はFalse
|
|
54
|
+
"""
|
|
55
|
+
if self == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
|
|
56
|
+
return task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value
|
|
57
|
+
|
|
58
|
+
elif self == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
59
|
+
return task["phase"] == TaskPhase.ACCEPTANCE.value
|
|
60
|
+
|
|
61
|
+
elif self == TaskCompletionCriteria.INSPECTION_REACHED:
|
|
62
|
+
# 受入フェーズも含む理由:検査フェーズに到達したタスクを「完了」とみなすならば、検査フェーズより後段フェーズである受入フェーズも「完了」とみなせるため
|
|
63
|
+
return task["phase"] in {TaskPhase.INSPECTION.value, TaskPhase.ACCEPTANCE.value}
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
assert_noreturn(self)
|
|
@@ -37,6 +37,12 @@ class ProjectDir(DataClassJsonMixin):
|
|
|
37
37
|
Args:
|
|
38
38
|
project_dir: ``annofabcli statistics visualize``コマンドによって出力されたプロジェクトディレクトリ
|
|
39
39
|
metadata:
|
|
40
|
+
|
|
41
|
+
タスクの完了条件(task_completion_criteria)は以下のいずれかです。
|
|
42
|
+
- acceptance_completed: 受入フェーズの完了状態
|
|
43
|
+
- acceptance_reached: 受入フェーズに到達
|
|
44
|
+
- inspection_reached: 検査フェーズまたは受入フェーズに到達
|
|
45
|
+
- annotation_started: 教師付フェーズを着手
|
|
40
46
|
"""
|
|
41
47
|
|
|
42
48
|
FILENAME_WHOLE_PERFORMANCE = "全体の生産性と品質.csv"
|
|
@@ -454,7 +460,7 @@ class ProjectInfo(DataClassJsonMixin):
|
|
|
454
460
|
measurement_datetime: str
|
|
455
461
|
"""計測日時。(2004-04-01T12:00+09:00形式)"""
|
|
456
462
|
task_completion_criteria: TaskCompletionCriteria
|
|
457
|
-
"""
|
|
463
|
+
"""タスクの完了条件(acceptance_completed / acceptance_reached / inspection_reached / annotation_started)"""
|
|
458
464
|
query: FilteringQuery
|
|
459
465
|
"""集計対象を絞り込むためのクエリ"""
|
|
460
466
|
|
|
@@ -12,7 +12,7 @@ from typing import Any, Callable, Optional
|
|
|
12
12
|
|
|
13
13
|
import annofabapi
|
|
14
14
|
import pandas
|
|
15
|
-
from annofabapi.models import ProjectMemberRole, TaskPhase
|
|
15
|
+
from annofabapi.models import ProjectMemberRole, TaskPhase
|
|
16
16
|
|
|
17
17
|
import annofabcli
|
|
18
18
|
from annofabcli.common.cli import (
|
|
@@ -24,7 +24,6 @@ from annofabcli.common.cli import (
|
|
|
24
24
|
get_list_from_args,
|
|
25
25
|
)
|
|
26
26
|
from annofabcli.common.facade import AnnofabApiFacade, TaskQuery
|
|
27
|
-
from annofabcli.common.type_util import assert_noreturn
|
|
28
27
|
from annofabcli.statistics.visualization.dataframe.actual_worktime import ActualWorktime
|
|
29
28
|
from annofabcli.statistics.visualization.dataframe.annotation_count import AnnotationCount
|
|
30
29
|
from annofabcli.statistics.visualization.dataframe.cumulative_productivity import (
|
|
@@ -123,7 +122,7 @@ class WriteCsvGraph:
|
|
|
123
122
|
|
|
124
123
|
tasks = self.visualize_source_files.read_tasks_json()
|
|
125
124
|
task_histories = self.visualize_source_files.read_task_histories_json()
|
|
126
|
-
new_tasks = filter_tasks(tasks, self.filtering_query, task_histories=task_histories)
|
|
125
|
+
new_tasks = filter_tasks(tasks, self.task_completion_criteria, self.filtering_query, task_histories=task_histories)
|
|
127
126
|
logger.debug(f"project_id='{self.project_id}' :: 集計対象タスクは {len(new_tasks)} / {len(tasks)} 件です。")
|
|
128
127
|
|
|
129
128
|
self.task = Task.from_api_content(
|
|
@@ -534,14 +533,9 @@ class VisualizeStatistics(CommandLine):
|
|
|
534
533
|
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
535
534
|
|
|
536
535
|
task_completion_criteria = TaskCompletionCriteria(args.task_completion_criteria)
|
|
537
|
-
if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
|
|
538
|
-
task_query = TaskQuery(phase=TaskPhase.ACCEPTANCE, status=TaskStatus.COMPLETE)
|
|
539
|
-
elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
|
|
540
|
-
task_query = TaskQuery(phase=TaskPhase.ACCEPTANCE)
|
|
541
|
-
else:
|
|
542
|
-
assert_noreturn(task_completion_criteria)
|
|
543
536
|
|
|
544
537
|
dict_task_query = annofabcli.common.cli.get_json_from_args(args.task_query)
|
|
538
|
+
task_query: Optional[TaskQuery] = None
|
|
545
539
|
if dict_task_query is not None:
|
|
546
540
|
task_query = TaskQuery.from_dict(dict_task_query)
|
|
547
541
|
logger.warning("引数 '--task_query' は非推奨です。代わりに '--task_completion_criteria' を指定してください。")
|
|
@@ -106,7 +106,7 @@ def plot_video_duration(
|
|
|
106
106
|
bins_sequence = numpy.arange(0, max_duration + bin_width, bin_width)
|
|
107
107
|
|
|
108
108
|
if bins_sequence[-1] == max_duration:
|
|
109
|
-
bins_sequence = numpy.append(bins_sequence, bins_sequence[-1] + bin_width)
|
|
109
|
+
bins_sequence = numpy.append(bins_sequence, bins_sequence[-1] + bin_width)
|
|
110
110
|
|
|
111
111
|
bins: Union[int, numpy.ndarray] = bins_sequence
|
|
112
112
|
else:
|
|
@@ -148,12 +148,33 @@ def get_first_acceptance_reached_datetime(task_histories: list[TaskHistory]) ->
|
|
|
148
148
|
if history["phase"] != TaskPhase.ACCEPTANCE.value:
|
|
149
149
|
continue
|
|
150
150
|
|
|
151
|
+
assert index > 0
|
|
151
152
|
first_acceptance_reached_datetime = task_histories[index - 1]["ended_datetime"]
|
|
152
153
|
assert first_acceptance_reached_datetime is not None
|
|
153
154
|
return first_acceptance_reached_datetime
|
|
154
155
|
return None
|
|
155
156
|
|
|
156
157
|
|
|
158
|
+
def get_first_inspection_reached_datetime(task_histories: list[TaskHistory]) -> Optional[str]:
|
|
159
|
+
"""
|
|
160
|
+
はじめて検査フェーズに到達した日時を取得する。
|
|
161
|
+
検査フェーズを着手した日時とは異なる。
|
|
162
|
+
必ず`first_inspection_started_datetime`よりも前の日時になる。
|
|
163
|
+
|
|
164
|
+
たとえば教師付フェーズで提出して検査フェーズに到達した場合、教師付フェーズを提出した日時が「検査フェーズに到達した日時」になる。
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
for index, history in enumerate(task_histories):
|
|
168
|
+
if history["phase"] != TaskPhase.INSPECTION.value:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
assert index > 0
|
|
172
|
+
first_inspection_reached_datetime = task_histories[index - 1]["ended_datetime"]
|
|
173
|
+
assert first_inspection_reached_datetime is not None
|
|
174
|
+
return first_inspection_reached_datetime
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
157
178
|
def is_acceptance_phase_skipped(task_histories: list[TaskHistory]) -> bool:
|
|
158
179
|
"""抜取受入によって、受入フェーズでスキップされたことがあるかを取得する。
|
|
159
180
|
|
|
@@ -332,7 +353,8 @@ class AddingAdditionalInfoToTask:
|
|
|
332
353
|
first_task_history = get_first_task_history(task_histories, phase)
|
|
333
354
|
self._add_task_history_info(task, first_task_history, column_prefix=f"first_{phase.value}")
|
|
334
355
|
|
|
335
|
-
#
|
|
356
|
+
# 初めて~になった日時
|
|
357
|
+
task["first_inspection_reached_datetime"] = get_first_inspection_reached_datetime(task_histories)
|
|
336
358
|
task["first_acceptance_reached_datetime"] = get_first_acceptance_reached_datetime(task_histories)
|
|
337
359
|
task["first_acceptance_completed_datetime"] = get_first_acceptance_completed_datetime(task_histories)
|
|
338
360
|
|
|
@@ -387,6 +409,7 @@ class TasksAddedTaskHistoryOutput:
|
|
|
387
409
|
def _get_output_target_columns() -> list[str]:
|
|
388
410
|
base_columns = [
|
|
389
411
|
# タスクの基本情報
|
|
412
|
+
"project_id",
|
|
390
413
|
"task_id",
|
|
391
414
|
"phase",
|
|
392
415
|
"phase_stage",
|
|
@@ -409,6 +432,7 @@ class TasksAddedTaskHistoryOutput:
|
|
|
409
432
|
# 差し戻し回数
|
|
410
433
|
"number_of_rejections_by_inspection",
|
|
411
434
|
"number_of_rejections_by_acceptance",
|
|
435
|
+
"first_inspection_reached_datetime",
|
|
412
436
|
"first_acceptance_reached_datetime",
|
|
413
437
|
"first_acceptance_completed_datetime",
|
|
414
438
|
"completed_datetime",
|
|
@@ -432,17 +456,14 @@ class TasksAddedTaskHistoryOutput:
|
|
|
432
456
|
|
|
433
457
|
def output(self, output_path: Path, output_format: FormatArgument) -> None:
|
|
434
458
|
task_list = self.task_list
|
|
435
|
-
|
|
436
|
-
logger.info("タスク一覧の件数が0件のため、出力しません。")
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
logger.debug(f"タスク一覧の件数: {len(task_list)}")
|
|
459
|
+
logger.debug(f"タスク {len(task_list)} 件の情報を出力します。")
|
|
440
460
|
if output_format == FormatArgument.CSV:
|
|
441
|
-
df_task = pandas.DataFrame(task_list)
|
|
461
|
+
df_task = pandas.DataFrame(task_list, columns=self._get_output_target_columns())
|
|
442
462
|
print_csv(
|
|
443
463
|
df_task[self._get_output_target_columns()],
|
|
444
464
|
output=output_path,
|
|
445
465
|
)
|
|
466
|
+
|
|
446
467
|
elif output_format == FormatArgument.JSON:
|
|
447
468
|
print_json(task_list, is_pretty=False, output=output_path)
|
|
448
469
|
elif output_format == FormatArgument.PRETTY_JSON:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
|
|
2
|
-
annofabcli/__main__.py,sha256=
|
|
2
|
+
annofabcli/__main__.py,sha256=YfuJE9E43xSo6iHTxVuQPHCz2eBaJS07QnVU42-0znQ,5293
|
|
3
3
|
annofabcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
annofabcli/annotation/annotation_query.py,sha256=VwfPWpLOpVa2SeEJ264LmCKkBGDJvpX8o7GbWIrDE0o,15712
|
|
6
6
|
annofabcli/annotation/change_annotation_attributes.py,sha256=Zjqax-sb7ujJnTUVv4io0GFmZzfqx0rpN2N1W86uVTs,17092
|
|
7
|
-
annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=
|
|
7
|
+
annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=iU199MlMfD-nfyRiHL44Zqrvr12vOeUHXyxCrDgaivI,14130
|
|
8
8
|
annofabcli/annotation/change_annotation_properties.py,sha256=Kp_LZ5sSoVmmjGE80ABVO3InxsXBIxiFFvVcIJNsOMk,18309
|
|
9
9
|
annofabcli/annotation/copy_annotation.py,sha256=Pih2k3vvpgfT3Ovb3gZw2L_8fK_ws_wKR7ARYG5hG_8,18407
|
|
10
10
|
annofabcli/annotation/delete_annotation.py,sha256=hQApNrx2Ci1bBWk0dRGA0oJkIgDHwl6Jy0-33gYF6jo,22989
|
|
@@ -48,14 +48,14 @@ annofabcli/comment/subcommand_comment.py,sha256=gd8w8ArXM1Tq9VUduDgn8m4G6HwevRWJ
|
|
|
48
48
|
annofabcli/comment/utils.py,sha256=aUj7U6MtGh64F3Ko83y4NKPKyWAqcg-c1XLqjkmIpSk,350
|
|
49
49
|
annofabcli/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
50
|
annofabcli/common/bokeh.py,sha256=x5-Siok3cIhq-biTOb3JG91_q--ob1m48TDgB72P1Hc,1071
|
|
51
|
-
annofabcli/common/cli.py,sha256=
|
|
51
|
+
annofabcli/common/cli.py,sha256=pmbieBx1-Eb-mlVqJAG0dJnalggczdbBly_u-qzHX84,21666
|
|
52
52
|
annofabcli/common/dataclasses.py,sha256=FRDihsB-H5P5ioVE3kfJtorsS5EbSMySlHrp8BJ3ktg,385
|
|
53
53
|
annofabcli/common/download.py,sha256=b3e5tVn1dmM1BMti67WwGzDCRH7hYJQEgk0xxkkuMIw,14003
|
|
54
54
|
annofabcli/common/enums.py,sha256=pnMZEk8ADK2qO2Hmujx6NxeCwvSAEDNhmgK4ajPSC9Q,1233
|
|
55
55
|
annofabcli/common/exceptions.py,sha256=trgC5eqvy7jgqOQ41pbAOC__uxy19GgrM9NAgkH_lBw,2051
|
|
56
56
|
annofabcli/common/facade.py,sha256=1d5DxC4O5yHr0gisqaqgkacY5Omn1X-L1syPnYPyiwQ,19401
|
|
57
57
|
annofabcli/common/image.py,sha256=qUsQXEYmttT443BOiaxLthccE7hLji5toLjgSdneNWA,16706
|
|
58
|
-
annofabcli/common/pandas.py,sha256=
|
|
58
|
+
annofabcli/common/pandas.py,sha256=Vyozu52UiZ0aCVtQ9jySxT_LrLMwpQvvpM8Uu3S2nCs,476
|
|
59
59
|
annofabcli/common/type_util.py,sha256=i3r5pFtRYQwJrYkl1-lVQi8XOePQxTUX_cAHgBTsagM,224
|
|
60
60
|
annofabcli/common/typing.py,sha256=_AcEogoaMbib0esfN2RvHshAZH0oyRb2Ro9-rbn7NJ8,330
|
|
61
61
|
annofabcli/common/utils.py,sha256=Eb4DS5j_EVdZW_YnsvIgjTbgWcjHd560plck2_WgrCI,9545
|
|
@@ -137,11 +137,11 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=0j78z5uRULo
|
|
|
137
137
|
annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
138
138
|
annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
|
|
139
139
|
annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
|
|
140
|
-
annofabcli/statistics/list_annotation_area.py,sha256=
|
|
141
|
-
annofabcli/statistics/list_annotation_attribute.py,sha256=
|
|
142
|
-
annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=
|
|
143
|
-
annofabcli/statistics/list_annotation_count.py,sha256=
|
|
144
|
-
annofabcli/statistics/list_annotation_duration.py,sha256=
|
|
140
|
+
annofabcli/statistics/list_annotation_area.py,sha256=RC6Pkhh_ps8fMZuDSQqhk2RdMpJ_z5TA9ax42tuJRFM,12567
|
|
141
|
+
annofabcli/statistics/list_annotation_attribute.py,sha256=lmvT3b1mVX4PRGWrBUfZafCn3c_hb_hx3WKXnHGcyyk,12754
|
|
142
|
+
annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=UF8wB-kfUnHCIPykb2Z2slyEcp9aHrzcbFbOs_3Z8-A,29341
|
|
143
|
+
annofabcli/statistics/list_annotation_count.py,sha256=4uTRR63Db-WDGRQt3AC8mPaRfFgbG5d27A036GNG3D0,53151
|
|
144
|
+
annofabcli/statistics/list_annotation_duration.py,sha256=WxHfzF4tYkveIdkaWoJZ6x7ddBMWkfz1YM_vxMub3jY,32165
|
|
145
145
|
annofabcli/statistics/list_video_duration.py,sha256=OCDXPOP2z7flE8NBaQD1lDkU9JG5mn3PfACijoR-5s0,9163
|
|
146
146
|
annofabcli/statistics/list_worktime.py,sha256=nr--GtFY-oyFuu8M0EsUqcVxX26gjeP09LYUcdeptyk,6456
|
|
147
147
|
annofabcli/statistics/scatter.py,sha256=C3hTlm_QfGBiY4KjZ-8D_u_Rk53a9f4jszx4iNZgp9w,10945
|
|
@@ -151,30 +151,30 @@ annofabcli/statistics/summarize_task_count_by_task_id_group.py,sha256=eRPl89Oh7_
|
|
|
151
151
|
annofabcli/statistics/summarize_task_count_by_user.py,sha256=rwhQwO6g2No9AqLEAbAfnlg_6CkgB3CRkZiaNnXCL0I,6857
|
|
152
152
|
annofabcli/statistics/visualize_annotation_count.py,sha256=7OATuGa2okq80unuTe-X30CBVkrlMLDN5Y-Q_5mB6eI,22138
|
|
153
153
|
annofabcli/statistics/visualize_annotation_duration.py,sha256=9JH9MirhOyCmjcChFJMtfnFIV2k4sucP9PAwNKMcbtE,21022
|
|
154
|
-
annofabcli/statistics/visualize_statistics.py,sha256=
|
|
155
|
-
annofabcli/statistics/visualize_video_duration.py,sha256=
|
|
154
|
+
annofabcli/statistics/visualize_statistics.py,sha256=YiT-KCCFQNF-v56hA8WjX37b_CGjMk2Ei-NbW7aHG-k,38384
|
|
155
|
+
annofabcli/statistics/visualize_video_duration.py,sha256=yY18H0-boNy4-MQWUM_xBTMzxDUQ51TrVtz8mbsB_dI,16623
|
|
156
156
|
annofabcli/statistics/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
157
|
-
annofabcli/statistics/visualization/filtering_query.py,sha256=
|
|
158
|
-
annofabcli/statistics/visualization/model.py,sha256=
|
|
159
|
-
annofabcli/statistics/visualization/project_dir.py,sha256
|
|
157
|
+
annofabcli/statistics/visualization/filtering_query.py,sha256=kqGOa1YdQ62kTLlkiIHFiYNju4Pg9rXpsC4Cph-QjLo,4355
|
|
158
|
+
annofabcli/statistics/visualization/model.py,sha256=sKk4gC7qkt6etCnbpCNZpJEhFJf5Vf7eVOG53gyfWvk,2426
|
|
159
|
+
annofabcli/statistics/visualization/project_dir.py,sha256=-60n29ySvgsfRTbFgTOYr6E7LHJezVQm3FON88LQMKU,24291
|
|
160
160
|
annofabcli/statistics/visualization/visualization_source_files.py,sha256=SFY7WXUtjECB8l7zP-exawocrTiZ0UI7Z5sjgq4J_g4,8641
|
|
161
161
|
annofabcli/statistics/visualization/dataframe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
162
162
|
annofabcli/statistics/visualization/dataframe/actual_worktime.py,sha256=7nsHlvN5cDzXIw-u_MSAZf4nlSSY56IlunSmnODXTbY,1916
|
|
163
163
|
annofabcli/statistics/visualization/dataframe/annotation_count.py,sha256=gv9iFpSLu_cB63G3Z9dGImT9jr_Q6U_mrmG8VXsxJnk,3940
|
|
164
|
-
annofabcli/statistics/visualization/dataframe/cumulative_productivity.py,sha256=
|
|
164
|
+
annofabcli/statistics/visualization/dataframe/cumulative_productivity.py,sha256=Z9gxGCfgQra0M0LAq3bUhGxXnwRqZ8Gt-zIo2DPs6Nc,15800
|
|
165
165
|
annofabcli/statistics/visualization/dataframe/custom_production_volume.py,sha256=5ELLiQJ5sNKdVKmYYVeZW4nedDg1CVGxMDdF5TUUX5c,2142
|
|
166
166
|
annofabcli/statistics/visualization/dataframe/input_data_count.py,sha256=wDRFtoIWw_Gy2bPZ7LBx3eMO3LdUdjbQKS9mncXav6I,1654
|
|
167
167
|
annofabcli/statistics/visualization/dataframe/inspection_comment_count.py,sha256=RxpQzRy4U2hKEpgbksUXotcxH2sKz__NO20mxpMqK1w,4382
|
|
168
168
|
annofabcli/statistics/visualization/dataframe/productivity_per_date.py,sha256=_AibOSwyeGM6KeYqknN40M8vS4Tl6l45tC0ua1AHFFc,27087
|
|
169
169
|
annofabcli/statistics/visualization/dataframe/project_performance.py,sha256=q480Lz1LwQD6RvyIOq-ZsXqeeKabsVpHRoM0nD_DaGI,8482
|
|
170
|
-
annofabcli/statistics/visualization/dataframe/task.py,sha256=
|
|
170
|
+
annofabcli/statistics/visualization/dataframe/task.py,sha256=v5vUbKJAuqqcKeumrAc4t2PKEX7jt86xFqqko0LrI98,25044
|
|
171
171
|
annofabcli/statistics/visualization/dataframe/task_history.py,sha256=FZ3MLqOiM6834fxWZKP0Hy9WDRSSLCYm6kNzdWW7-S8,2744
|
|
172
|
-
annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py,sha256=
|
|
172
|
+
annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py,sha256=khXqxQ-v7Q1PBtttQmPCCbVLraeqNCPi2tkhTxxaZPg,13641
|
|
173
173
|
annofabcli/statistics/visualization/dataframe/user.py,sha256=EHn7nlf6D6UX-gsVXy8m_3QaCsHsUhr0iy2rbNozOgc,1707
|
|
174
|
-
annofabcli/statistics/visualization/dataframe/user_performance.py,sha256=
|
|
174
|
+
annofabcli/statistics/visualization/dataframe/user_performance.py,sha256=X0jXV0wY8AZC8hoV62sJd9GRThqTf5RsaVciLeCvGlk,56628
|
|
175
175
|
annofabcli/statistics/visualization/dataframe/whole_performance.py,sha256=kw5Cww0fwDfwriTtxiT0l3wr7YAMA9eGjvp84G9MN9I,12449
|
|
176
|
-
annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py,sha256=
|
|
177
|
-
annofabcli/statistics/visualization/dataframe/worktime_per_date.py,sha256=
|
|
176
|
+
annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py,sha256=F6KhXvzrUx61E0jqOiU2IGb6ycLH82T80JyzYVpSaC8,53683
|
|
177
|
+
annofabcli/statistics/visualization/dataframe/worktime_per_date.py,sha256=wt0paPy2cVVzLUJhbV12-6bInikbQoxRFnOHY7BcI9o,21342
|
|
178
178
|
annofabcli/supplementary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
179
179
|
annofabcli/supplementary/delete_supplementary_data.py,sha256=dA4n61SSkTbDKDqjVldHIEAxbPQXyrQciwI4RDuC35U,13537
|
|
180
180
|
annofabcli/supplementary/list_supplementary_data.py,sha256=Rh9aOa3PwukVxnnbXTtMG0qAASc3JkaqRt7KrMGZQRk,7635
|
|
@@ -193,7 +193,7 @@ annofabcli/task/download_task_json.py,sha256=Ocjecmdf2WV_Sq3u1InfMLIsT3XSw0ojyJm
|
|
|
193
193
|
annofabcli/task/list_all_tasks.py,sha256=FpFzosCZs2EZNSQMHzWse4z_zjZeiVF5BFmbwb97UYg,6368
|
|
194
194
|
annofabcli/task/list_all_tasks_added_task_history.py,sha256=fkdiuo64iS7xxvIfGKzSiUPPEMiCVnJjjcAtMxe2Ngs,9551
|
|
195
195
|
annofabcli/task/list_tasks.py,sha256=cmfqbP2Sr1dlpvpJLJscKq31BUdXVaajr_agiecKWXg,10051
|
|
196
|
-
annofabcli/task/list_tasks_added_task_history.py,sha256=
|
|
196
|
+
annofabcli/task/list_tasks_added_task_history.py,sha256=JI3Rxn9MwXN07Cu2wDfaIXAzm3MlNm1kIh1SZHbxZr4,22488
|
|
197
197
|
annofabcli/task/put_tasks.py,sha256=HADm-sP8zs523MlDThlwrGIVIEtR4Md2KlpJbWhCCTk,13745
|
|
198
198
|
annofabcli/task/put_tasks_by_count.py,sha256=kIjvWtgZ53Smcm8N-raZedQCubx1TWoQT-BYyioYDrA,5999
|
|
199
199
|
annofabcli/task/reject_tasks.py,sha256=2mRxIsP63N2xcVXQ0xeFqF0VkuUQP6C_bsFEqG-qNf0,22027
|
|
@@ -209,8 +209,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
|
|
|
209
209
|
annofabcli/task_history_event/list_all_task_history_event.py,sha256=EeKMyPUxGwYCFtWQHHW954ZserGm8lUqrwNnV1iX9X4,6830
|
|
210
210
|
annofabcli/task_history_event/list_worktime.py,sha256=Y7Pu5DP7scPf7HPt6CTiTvB1_5_Nfi1bStUIaCpkhII,15507
|
|
211
211
|
annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
|
|
212
|
-
annofabcli-1.
|
|
213
|
-
annofabcli-1.
|
|
214
|
-
annofabcli-1.
|
|
215
|
-
annofabcli-1.
|
|
216
|
-
annofabcli-1.
|
|
212
|
+
annofabcli-1.106.0.dist-info/METADATA,sha256=FqGLzIdUm7grQhd5oMncx6H3AhyuoUC0Na385BczmS4,5286
|
|
213
|
+
annofabcli-1.106.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
214
|
+
annofabcli-1.106.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
|
|
215
|
+
annofabcli-1.106.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
|
|
216
|
+
annofabcli-1.106.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|