annofabcli 1.102.1__py3-none-any.whl → 1.104.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 +1 -1
- annofabcli/annotation/annotation_query.py +9 -29
- annofabcli/annotation/change_annotation_attributes.py +6 -14
- annofabcli/annotation/change_annotation_properties.py +5 -12
- annofabcli/annotation/copy_annotation.py +4 -10
- annofabcli/annotation/delete_annotation.py +10 -26
- annofabcli/annotation/dump_annotation.py +1 -4
- annofabcli/annotation/import_annotation.py +15 -39
- annofabcli/annotation/list_annotation.py +1 -4
- annofabcli/annotation/merge_segmentation.py +5 -15
- annofabcli/annotation/remove_segmentation_overlap.py +8 -29
- annofabcli/annotation/restore_annotation.py +3 -9
- annofabcli/annotation_specs/add_attribute_restriction.py +2 -8
- annofabcli/annotation_specs/attribute_restriction.py +2 -10
- annofabcli/annotation_specs/export_annotation_specs.py +1 -3
- annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py +3 -10
- annofabcli/annotation_specs/get_annotation_specs_with_choice_id_replaced.py +4 -10
- annofabcli/annotation_specs/get_annotation_specs_with_label_id_replaced.py +1 -3
- annofabcli/annotation_specs/list_annotation_specs_attribute.py +7 -18
- annofabcli/annotation_specs/list_annotation_specs_choice.py +3 -8
- annofabcli/annotation_specs/list_annotation_specs_history.py +0 -1
- annofabcli/annotation_specs/list_annotation_specs_label.py +3 -8
- annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +4 -9
- annofabcli/annotation_specs/list_attribute_restriction.py +3 -9
- annofabcli/annotation_specs/put_label_color.py +1 -6
- annofabcli/comment/delete_comment.py +3 -9
- annofabcli/comment/list_all_comment.py +15 -5
- annofabcli/comment/list_comment.py +46 -7
- annofabcli/comment/put_comment.py +4 -13
- annofabcli/comment/put_comment_simply.py +2 -6
- annofabcli/comment/put_inspection_comment.py +2 -6
- annofabcli/comment/put_inspection_comment_simply.py +3 -6
- annofabcli/comment/put_onhold_comment.py +2 -6
- annofabcli/comment/put_onhold_comment_simply.py +2 -4
- annofabcli/common/cli.py +5 -43
- annofabcli/common/download.py +8 -25
- annofabcli/common/image.py +3 -7
- annofabcli/common/utils.py +2 -4
- annofabcli/common/visualize.py +2 -4
- annofabcli/filesystem/draw_annotation.py +6 -18
- annofabcli/filesystem/filter_annotation.py +7 -24
- annofabcli/filesystem/mask_user_info.py +2 -5
- annofabcli/filesystem/merge_annotation.py +2 -6
- annofabcli/input_data/change_input_data_name.py +3 -7
- annofabcli/input_data/copy_input_data.py +6 -14
- annofabcli/input_data/delete_input_data.py +7 -24
- annofabcli/input_data/delete_metadata_key_of_input_data.py +5 -16
- annofabcli/input_data/list_all_input_data.py +5 -14
- annofabcli/input_data/list_all_input_data_merged_task.py +8 -23
- annofabcli/input_data/list_input_data.py +5 -16
- annofabcli/input_data/put_input_data.py +7 -19
- annofabcli/input_data/update_metadata_of_input_data.py +6 -14
- annofabcli/instruction/list_instruction_history.py +0 -1
- annofabcli/instruction/upload_instruction.py +4 -7
- annofabcli/job/list_job.py +2 -3
- annofabcli/job/list_last_job.py +1 -3
- annofabcli/organization/list_organization.py +0 -1
- annofabcli/organization_member/change_organization_member.py +1 -3
- annofabcli/organization_member/delete_organization_member.py +2 -6
- annofabcli/organization_member/invite_organization_member.py +1 -3
- annofabcli/organization_member/list_organization_member.py +0 -1
- annofabcli/project/change_organization_of_project.py +257 -0
- annofabcli/project/change_project_status.py +2 -2
- annofabcli/project/copy_project.py +2 -7
- annofabcli/project/diff_projects.py +4 -16
- annofabcli/project/list_project.py +0 -1
- annofabcli/project/put_project.py +2 -6
- annofabcli/project/subcommand_project.py +2 -0
- annofabcli/project_member/change_project_members.py +1 -1
- annofabcli/project_member/copy_project_members.py +2 -7
- annofabcli/project_member/drop_project_members.py +1 -3
- annofabcli/project_member/invite_project_members.py +2 -4
- annofabcli/project_member/list_users.py +0 -1
- annofabcli/project_member/put_project_members.py +4 -12
- annofabcli/stat_visualization/mask_visualization_dir.py +6 -16
- annofabcli/stat_visualization/merge_visualization_dir.py +7 -19
- annofabcli/stat_visualization/summarize_whole_performance_csv.py +3 -7
- annofabcli/stat_visualization/write_graph.py +5 -15
- annofabcli/stat_visualization/write_performance_rating_csv.py +4 -12
- annofabcli/statistics/list_annotation_area.py +3 -7
- annofabcli/statistics/list_annotation_attribute.py +6 -15
- annofabcli/statistics/list_annotation_attribute_filled_count.py +9 -23
- annofabcli/statistics/list_annotation_count.py +18 -44
- annofabcli/statistics/list_annotation_duration.py +14 -40
- annofabcli/statistics/list_video_duration.py +2 -3
- annofabcli/statistics/list_worktime.py +0 -1
- annofabcli/statistics/scatter.py +3 -9
- annofabcli/statistics/summarize_task_count.py +7 -12
- annofabcli/statistics/summarize_task_count_by_task_id_group.py +3 -11
- annofabcli/statistics/summarize_task_count_by_user.py +1 -5
- annofabcli/statistics/visualization/dataframe/annotation_count.py +2 -4
- annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +6 -12
- annofabcli/statistics/visualization/dataframe/productivity_per_date.py +10 -22
- annofabcli/statistics/visualization/dataframe/project_performance.py +1 -3
- annofabcli/statistics/visualization/dataframe/task.py +2 -5
- annofabcli/statistics/visualization/dataframe/task_history.py +1 -1
- annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +6 -20
- annofabcli/statistics/visualization/dataframe/user_performance.py +29 -88
- annofabcli/statistics/visualization/dataframe/whole_performance.py +6 -12
- annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +17 -49
- annofabcli/statistics/visualization/dataframe/worktime_per_date.py +4 -10
- annofabcli/statistics/visualization/filtering_query.py +2 -6
- annofabcli/statistics/visualization/project_dir.py +9 -26
- annofabcli/statistics/visualization/visualization_source_files.py +3 -10
- annofabcli/statistics/visualize_annotation_count.py +9 -23
- annofabcli/statistics/visualize_annotation_duration.py +5 -15
- annofabcli/statistics/visualize_statistics.py +18 -53
- annofabcli/statistics/visualize_video_duration.py +8 -19
- annofabcli/supplementary/delete_supplementary_data.py +7 -23
- annofabcli/supplementary/list_supplementary_data.py +1 -1
- annofabcli/supplementary/put_supplementary_data.py +5 -15
- annofabcli/task/cancel_acceptance.py +3 -4
- annofabcli/task/change_operator.py +3 -11
- annofabcli/task/change_status_to_break.py +1 -1
- annofabcli/task/change_status_to_on_hold.py +5 -18
- annofabcli/task/complete_tasks.py +8 -25
- annofabcli/task/copy_tasks.py +2 -3
- annofabcli/task/delete_metadata_key_of_task.py +2 -6
- annofabcli/task/delete_tasks.py +8 -26
- annofabcli/task/list_all_tasks.py +2 -4
- annofabcli/task/list_tasks.py +3 -7
- annofabcli/task/list_tasks_added_task_history.py +7 -21
- annofabcli/task/put_tasks.py +2 -3
- annofabcli/task/put_tasks_by_count.py +3 -7
- annofabcli/task/reject_tasks.py +7 -19
- annofabcli/task/update_metadata_of_task.py +2 -2
- annofabcli/task_history/list_all_task_history.py +2 -5
- annofabcli/task_history/list_task_history.py +0 -1
- annofabcli/task_history_event/list_all_task_history_event.py +4 -11
- annofabcli/task_history_event/list_worktime.py +4 -14
- {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/METADATA +1 -1
- annofabcli-1.104.0.dist-info/RECORD +215 -0
- annofabcli-1.102.1.dist-info/RECORD +0 -214
- {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -67,10 +67,7 @@ class UpdateMetadataMain(CommandLineWithConfirm):
|
|
|
67
67
|
logger.warning(f"{logging_prefix} 入力データは存在しないのでスキップします。 :: input_data_id='{input_data_id}'")
|
|
68
68
|
return False
|
|
69
69
|
|
|
70
|
-
logger.debug(
|
|
71
|
-
f"{logging_prefix} input_data_id='{input_data['input_data_id']}', "
|
|
72
|
-
f"input_data_name='{input_data['input_data_name']}', metadata='{json.dumps(input_data['metadata'])}'"
|
|
73
|
-
)
|
|
70
|
+
logger.debug(f"{logging_prefix} input_data_id='{input_data['input_data_id']}', input_data_name='{input_data['input_data_name']}', metadata='{json.dumps(input_data['metadata'])}'")
|
|
74
71
|
if not self.confirm_processing(get_confirm_message()):
|
|
75
72
|
return False
|
|
76
73
|
|
|
@@ -84,9 +81,7 @@ class UpdateMetadataMain(CommandLineWithConfirm):
|
|
|
84
81
|
logger.debug(f"{logging_prefix} 入力データのメタデータを更新しました。input_data_id='{input_data['input_data_id']}'")
|
|
85
82
|
return True
|
|
86
83
|
|
|
87
|
-
def set_metadata_to_input_data_wrapper(
|
|
88
|
-
self, tpl: tuple[int, InputDataMetadataInfo], project_id: str, *, overwrite_metadata: bool = False
|
|
89
|
-
) -> bool:
|
|
84
|
+
def set_metadata_to_input_data_wrapper(self, tpl: tuple[int, InputDataMetadataInfo], project_id: str, *, overwrite_metadata: bool = False) -> bool:
|
|
90
85
|
input_data_index, info = tpl
|
|
91
86
|
return self.set_metadata_to_input_data(
|
|
92
87
|
project_id,
|
|
@@ -202,9 +197,7 @@ class UpdateMetadata(CommandLine):
|
|
|
202
197
|
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
203
198
|
|
|
204
199
|
if input_data_id_list is not None:
|
|
205
|
-
metadata_by_input_data_id = {
|
|
206
|
-
input_data_id: metadata for input_data_id, metadata in metadata_by_input_data_id.items() if input_data_id in input_data_id_list
|
|
207
|
-
}
|
|
200
|
+
metadata_by_input_data_id = {input_data_id: metadata for input_data_id, metadata in metadata_by_input_data_id.items() if input_data_id in input_data_id_list}
|
|
208
201
|
else:
|
|
209
202
|
raise RuntimeError("'--metadata'か'--metadata_by_input_data_id'のどちらかを指定する必要があります。")
|
|
210
203
|
|
|
@@ -233,8 +226,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
233
226
|
metadata_group_parser.add_argument(
|
|
234
227
|
"--metadata",
|
|
235
228
|
type=str,
|
|
236
|
-
help="入力データに設定する ``metadata`` をJSON形式で指定してください。メタデータの値は文字列です。"
|
|
237
|
-
" ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
|
|
229
|
+
help="入力データに設定する ``metadata`` をJSON形式で指定してください。メタデータの値は文字列です。 ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
|
|
238
230
|
)
|
|
239
231
|
|
|
240
232
|
sample_metadata_by_input_data_id = {"input_data1": {"country": "japan"}}
|
|
@@ -251,14 +243,14 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
251
243
|
parser.add_argument(
|
|
252
244
|
"--overwrite",
|
|
253
245
|
action="store_true",
|
|
254
|
-
help="指定した場合、メタデータを上書きして更新します(すでに設定されているメタデータは削除されます)。指定しない場合、 ``--metadata`` に指定されたキーのみ更新されます。",
|
|
246
|
+
help="指定した場合、メタデータを上書きして更新します(すでに設定されているメタデータは削除されます)。指定しない場合、 ``--metadata`` に指定されたキーのみ更新されます。",
|
|
255
247
|
)
|
|
256
248
|
|
|
257
249
|
parser.add_argument(
|
|
258
250
|
"--parallelism",
|
|
259
251
|
type=int,
|
|
260
252
|
choices=PARALLELISM_CHOICES,
|
|
261
|
-
help="使用するプロセス数(並列度)を指定してください。指定する場合は必ず ``--yes`` を指定してください。指定しない場合は、逐次的に処理します。",
|
|
253
|
+
help="使用するプロセス数(並列度)を指定してください。指定する場合は必ず ``--yes`` を指定してください。指定しない場合は、逐次的に処理します。",
|
|
262
254
|
)
|
|
263
255
|
|
|
264
256
|
parser.set_defaults(subcommand_func=main)
|
|
@@ -41,7 +41,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
41
41
|
argument_parser.add_output()
|
|
42
42
|
|
|
43
43
|
argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV)
|
|
44
|
-
argument_parser.add_csv_format()
|
|
45
44
|
|
|
46
45
|
parser.set_defaults(subcommand_func=main)
|
|
47
46
|
|
|
@@ -70,7 +70,7 @@ class UploadInstruction(CommandLine):
|
|
|
70
70
|
if src_value.startswith("data:"):
|
|
71
71
|
img_path = save_image_from_data_uri_scheme(src_value, temp_dir=temp_dir)
|
|
72
72
|
else: # noqa: PLR5501
|
|
73
|
-
if src_value[0] == "/":
|
|
73
|
+
if src_value[0] == "/":
|
|
74
74
|
img_path = Path(src_value)
|
|
75
75
|
else:
|
|
76
76
|
img_path = html_path.parent / src_value
|
|
@@ -98,7 +98,7 @@ class UploadInstruction(CommandLine):
|
|
|
98
98
|
continue
|
|
99
99
|
|
|
100
100
|
# body要素があればその中身、なければhtmlファイルの中身をアップロードする
|
|
101
|
-
if len(pq_html("body")) > 0:
|
|
101
|
+
if len(pq_html("body")) > 0:
|
|
102
102
|
html_data = pq_html("body").html()
|
|
103
103
|
else:
|
|
104
104
|
html_data = pq_html.html()
|
|
@@ -108,7 +108,7 @@ class UploadInstruction(CommandLine):
|
|
|
108
108
|
|
|
109
109
|
def update_instruction(self, project_id: str, html_data: str) -> None:
|
|
110
110
|
histories, _ = self.service.api.get_instruction_history(project_id)
|
|
111
|
-
if len(histories) > 0:
|
|
111
|
+
if len(histories) > 0:
|
|
112
112
|
last_updated_datetime = histories[0]["updated_datetime"]
|
|
113
113
|
else:
|
|
114
114
|
last_updated_datetime = None
|
|
@@ -147,10 +147,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
147
147
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
148
148
|
subcommand_name = "upload"
|
|
149
149
|
subcommand_help = "HTMLファイルを作業ガイドとして登録します。"
|
|
150
|
-
description =
|
|
151
|
-
"HTMLファイルを作業ガイドとして登録します。"
|
|
152
|
-
"img要素のsrc属性がローカルの画像を参照している場合(http, https, dataスキーマが付与されていない)、画像もアップロードします。"
|
|
153
|
-
)
|
|
150
|
+
description = "HTMLファイルを作業ガイドとして登録します。img要素のsrc属性がローカルの画像を参照している場合(http, https, dataスキーマが付与されていない)、画像もアップロードします。"
|
|
154
151
|
epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。"
|
|
155
152
|
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
|
|
156
153
|
parse_args(parser)
|
annofabcli/job/list_job.py
CHANGED
|
@@ -23,7 +23,7 @@ class ListJob(CommandLine):
|
|
|
23
23
|
ジョブ一覧を取得する。
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
if job_query is not None:
|
|
26
|
+
if job_query is not None:
|
|
27
27
|
query_params = copy.deepcopy(job_query)
|
|
28
28
|
else:
|
|
29
29
|
query_params = {}
|
|
@@ -74,12 +74,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
74
74
|
type=str,
|
|
75
75
|
choices=job_choices,
|
|
76
76
|
required=True,
|
|
77
|
-
help="ジョブタイプを指定します。指定できる値については https://annofab-cli.readthedocs.io/ja/latest/user_guide/command_line_options.html#job-type を参照してください。",
|
|
77
|
+
help="ジョブタイプを指定します。指定できる値については https://annofab-cli.readthedocs.io/ja/latest/user_guide/command_line_options.html#job-type を参照してください。",
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV)
|
|
81
81
|
argument_parser.add_output()
|
|
82
|
-
argument_parser.add_csv_format()
|
|
83
82
|
|
|
84
83
|
parser.set_defaults(subcommand_func=main)
|
|
85
84
|
|
annofabcli/job/list_last_job.py
CHANGED
|
@@ -148,8 +148,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
148
148
|
"-org",
|
|
149
149
|
"--organization",
|
|
150
150
|
type=str,
|
|
151
|
-
help="
|
|
152
|
-
"自分が所属している進行中のプロジェクトが対象になります。",
|
|
151
|
+
help="組織配下のすべてのプロジェクトのジョブを出力したい場合は、組織名を指定してください。自分が所属している進行中のプロジェクトが対象になります。",
|
|
153
152
|
)
|
|
154
153
|
|
|
155
154
|
parser.add_argument(
|
|
@@ -160,7 +159,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
160
159
|
|
|
161
160
|
argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV)
|
|
162
161
|
argument_parser.add_output()
|
|
163
|
-
argument_parser.add_csv_format()
|
|
164
162
|
|
|
165
163
|
parser.set_defaults(subcommand_func=main)
|
|
166
164
|
|
|
@@ -49,9 +49,7 @@ class ChangeOrganizationMemberMain(CommandLineWithConfirm):
|
|
|
49
49
|
logger.warning(f"組織メンバにuser_id='{user_id}'のユーザが存在しません。")
|
|
50
50
|
continue
|
|
51
51
|
|
|
52
|
-
if not self.confirm_processing(
|
|
53
|
-
f"user_id='{user_id}'のユーザの組織メンバロールを'{role}'に変更しますか? :: username='{member['username']}', role='{member['role']}'"
|
|
54
|
-
):
|
|
52
|
+
if not self.confirm_processing(f"user_id='{user_id}'のユーザの組織メンバロールを'{role}'に変更しますか? :: username='{member['username']}', role='{member['role']}'"):
|
|
55
53
|
continue
|
|
56
54
|
|
|
57
55
|
try:
|
|
@@ -37,9 +37,7 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
|
|
|
37
37
|
return more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id)
|
|
38
38
|
|
|
39
39
|
def delete_organization_members_from_organization(self, organization_name: str, user_ids: Collection[str]) -> None:
|
|
40
|
-
if not self.facade.contains_any_organization_member_role(
|
|
41
|
-
organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}
|
|
42
|
-
):
|
|
40
|
+
if not self.facade.contains_any_organization_member_role(organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}):
|
|
43
41
|
logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを脱退できるロールを持たないため、スキップします。")
|
|
44
42
|
return
|
|
45
43
|
|
|
@@ -55,9 +53,7 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
|
|
|
55
53
|
logger.warning(f"組織'{organization_name}'に user_id='{user_id}'のメンバーが存在しません。")
|
|
56
54
|
continue
|
|
57
55
|
|
|
58
|
-
if not self.confirm_processing(
|
|
59
|
-
f"組織'{organization_name}'に所属する user_id='{user_id}'のメンバーを脱退させますか? :: username='{member['username']}'"
|
|
60
|
-
):
|
|
56
|
+
if not self.confirm_processing(f"組織'{organization_name}'に所属する user_id='{user_id}'のメンバーを脱退させますか? :: username='{member['username']}'"):
|
|
61
57
|
continue
|
|
62
58
|
|
|
63
59
|
try:
|
|
@@ -30,9 +30,7 @@ class InviteOrganizationMemberMain(CommandLineWithConfirm):
|
|
|
30
30
|
super().__init__(all_yes)
|
|
31
31
|
|
|
32
32
|
def invite_members_to_organization(self, organization_name: str, user_ids: Collection[str], role: str) -> None:
|
|
33
|
-
if not self.facade.contains_any_organization_member_role(
|
|
34
|
-
organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}
|
|
35
|
-
):
|
|
33
|
+
if not self.facade.contains_any_organization_member_role(organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}):
|
|
36
34
|
logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを招待できるロールを持たないため、スキップします。")
|
|
37
35
|
return
|
|
38
36
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import annofabapi
|
|
9
|
+
import more_itertools
|
|
10
|
+
import requests
|
|
11
|
+
from annofabapi.models import JobStatus, OrganizationMemberRole, ProjectJobType
|
|
12
|
+
|
|
13
|
+
import annofabcli
|
|
14
|
+
from annofabcli.common.cli import (
|
|
15
|
+
CommandLine,
|
|
16
|
+
CommandLineWithConfirm,
|
|
17
|
+
build_annofabapi_resource_and_login,
|
|
18
|
+
)
|
|
19
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChangeProjectOrganizationMain(CommandLineWithConfirm):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
service: annofabapi.Resource,
|
|
28
|
+
*,
|
|
29
|
+
is_force: bool = False,
|
|
30
|
+
all_yes: bool = False,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.service = service
|
|
33
|
+
self.is_force = is_force
|
|
34
|
+
self.facade = AnnofabApiFacade(service)
|
|
35
|
+
super().__init__(all_yes)
|
|
36
|
+
|
|
37
|
+
async def wait_until_jobs_finished_async(self, jobs: list[dict[str, Any]]) -> None:
|
|
38
|
+
tasks = [
|
|
39
|
+
self.wait_until_job_finished_async(
|
|
40
|
+
project_id=job["project_id"],
|
|
41
|
+
job_type=ProjectJobType(job["job_type"]),
|
|
42
|
+
job_id=job["job_id"],
|
|
43
|
+
job_access_interval=60,
|
|
44
|
+
max_job_access=360,
|
|
45
|
+
)
|
|
46
|
+
for job in jobs
|
|
47
|
+
]
|
|
48
|
+
results = await asyncio.gather(*tasks)
|
|
49
|
+
success_count = 0
|
|
50
|
+
for result in results:
|
|
51
|
+
if result is not None and result == JobStatus.SUCCEEDED:
|
|
52
|
+
success_count += 1
|
|
53
|
+
|
|
54
|
+
logger.info(f"{success_count} 件のプロジェクトの組織の変更が成功しました。")
|
|
55
|
+
|
|
56
|
+
async def wait_until_job_finished_async(
|
|
57
|
+
self,
|
|
58
|
+
project_id: str,
|
|
59
|
+
job_type: ProjectJobType,
|
|
60
|
+
job_id: str,
|
|
61
|
+
job_access_interval: int = 60,
|
|
62
|
+
max_job_access: int = 360,
|
|
63
|
+
) -> Optional["JobStatus"]:
|
|
64
|
+
"""
|
|
65
|
+
指定したジョブが終了するまで非同期で待つ。
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_id: プロジェクトID
|
|
69
|
+
job_type: ジョブ種別
|
|
70
|
+
job_id: ジョブID。Noneの場合は、現在進行中のジョブが終了するまで待つ。
|
|
71
|
+
job_access_interval: ジョブにアクセスする間隔[sec]
|
|
72
|
+
max_job_access: ジョブに最大何回アクセスするか
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
指定した時間(アクセス頻度と回数)待った後のジョブのステータスを返す。
|
|
76
|
+
指定したジョブ(job_idがNoneの場合は現在進行中のジョブ)が存在しない場合は、Noneを返す。
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def get_job_from_job_id(arg_job_id: str) -> Optional[dict[str, Any]]:
|
|
80
|
+
content, _ = self.service.api.get_project_job(project_id, query_params={"type": job_type.value})
|
|
81
|
+
job_list = content["list"]
|
|
82
|
+
return more_itertools.first_true(job_list, pred=lambda e: e["job_id"] == arg_job_id)
|
|
83
|
+
|
|
84
|
+
job_access_count = 0
|
|
85
|
+
while True:
|
|
86
|
+
# API呼び出しは同期なので、スレッドでラップ
|
|
87
|
+
job = get_job_from_job_id(job_id)
|
|
88
|
+
if job is None:
|
|
89
|
+
logger.info(
|
|
90
|
+
"project_id='%s', job_id='%s', job_type='%s' のジョブは存在しません。",
|
|
91
|
+
project_id,
|
|
92
|
+
job_type.value,
|
|
93
|
+
job_id,
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
job_access_count += 1
|
|
98
|
+
|
|
99
|
+
if job["job_status"] == JobStatus.SUCCEEDED.value:
|
|
100
|
+
logger.info(
|
|
101
|
+
"project_id='%s', job_id='%s', job_type='%s' のジョブが成功しました。",
|
|
102
|
+
project_id,
|
|
103
|
+
job_id,
|
|
104
|
+
job_type.value,
|
|
105
|
+
)
|
|
106
|
+
return JobStatus.SUCCEEDED
|
|
107
|
+
|
|
108
|
+
elif job["job_status"] == JobStatus.FAILED.value:
|
|
109
|
+
logger.info(
|
|
110
|
+
"project_id='%s', job_id='%s', job_type='%s' のジョブが失敗しました。:: errors='%s'",
|
|
111
|
+
project_id,
|
|
112
|
+
job_id,
|
|
113
|
+
job_type.value,
|
|
114
|
+
job["errors"],
|
|
115
|
+
)
|
|
116
|
+
return JobStatus.FAILED
|
|
117
|
+
|
|
118
|
+
elif job_access_count < max_job_access:
|
|
119
|
+
logger.info(
|
|
120
|
+
"project_id='%s', job_id='%s', job_type='%s' のジョブは進行中です。%d 秒間待ちます。",
|
|
121
|
+
project_id,
|
|
122
|
+
job_id,
|
|
123
|
+
job_type.value,
|
|
124
|
+
job_access_interval,
|
|
125
|
+
)
|
|
126
|
+
await asyncio.sleep(job_access_interval)
|
|
127
|
+
else:
|
|
128
|
+
logger.info(
|
|
129
|
+
"project_id='%s', job_id='%s', job_type='%s' のジョブは %.1f 分以上経過しても、終了しませんでした。",
|
|
130
|
+
project_id,
|
|
131
|
+
job["job_id"],
|
|
132
|
+
job_type.value,
|
|
133
|
+
job_access_interval * job_access_count / 60,
|
|
134
|
+
)
|
|
135
|
+
return JobStatus.PROGRESS
|
|
136
|
+
|
|
137
|
+
def change_organization_for_project(self, project_id: str, organization_name: str) -> Optional[dict[str, Any]]:
|
|
138
|
+
project = self.service.wrapper.get_project_or_none(project_id)
|
|
139
|
+
if project is None:
|
|
140
|
+
logger.warning(f"project_id='{project_id}'のプロジェクトは存在しないので、スキップします。")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
project_name = project["title"]
|
|
144
|
+
if project["project_status"] == "active":
|
|
145
|
+
if self.is_force:
|
|
146
|
+
if not self.confirm_processing(
|
|
147
|
+
f"project_id='{project_id}'のプロジェクトの状態を停止中にしたあと、所属する組織を'{organization_name}'に変更しますか? :: project_name='{project_name}'"
|
|
148
|
+
):
|
|
149
|
+
return None
|
|
150
|
+
request_body = copy.deepcopy(project)
|
|
151
|
+
request_body.update(
|
|
152
|
+
{
|
|
153
|
+
"status": "suspended",
|
|
154
|
+
"last_updated_datetime": project["updated_datetime"],
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
project, _ = self.service.api.put_project(project_id, request_body=request_body, query_params={"v": "2"})
|
|
158
|
+
logger.info(f"project_id='{project_id}'のプロジェクトのステータスを「停止中」に変更しました。 :: project_name='{project_name}'")
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"project_id='{project_id}'のプロジェクトのステータスは「進行中」のため、組織を変更できません。 `--force`オプションを指定すれば、停止中状態に変更した後組織を変更できます。"
|
|
162
|
+
)
|
|
163
|
+
return None
|
|
164
|
+
elif not self.confirm_processing(f"project_id='{project_id}'のプロジェクトの組織を'{organization_name}'に変更しますか? :: project_name='{project_name}'"):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
assert project is not None
|
|
168
|
+
request_body = copy.deepcopy(project)
|
|
169
|
+
request_body["organization_name"] = organization_name
|
|
170
|
+
request_body["last_updated_datetime"] = project["updated_datetime"]
|
|
171
|
+
request_body["status"] = project["project_status"]
|
|
172
|
+
|
|
173
|
+
content, _ = self.service.api.put_project(project_id, request_body=request_body, query_params={"v": "2"})
|
|
174
|
+
job = content["job"]
|
|
175
|
+
logger.info(f"project_id='{project_id}'のプロジェクトの所属先組織を'{organization_name}'に変更するジョブを発行しました。 :: project_name='{project_name}', job_id='{job['job_id']}'")
|
|
176
|
+
return job
|
|
177
|
+
|
|
178
|
+
def change_organization_for_project_list(self, project_id_list: list[str], organization_name: str) -> list[dict[str, Any]]:
|
|
179
|
+
if not self.facade.contains_any_organization_member_role(organization_name, [OrganizationMemberRole.OWNER, OrganizationMemberRole.ADMINISTRATOR]):
|
|
180
|
+
logger.warning(f"変更先組織'{organization_name}'に対して管理者ロールまたはオーナロールでないため、プロジェクトの所属する組織を変更できません。")
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
logger.info(f"{len(project_id_list)} 件のプロジェクトの組織を'{organization_name}'に変更するジョブを発行します。")
|
|
184
|
+
|
|
185
|
+
job_list = []
|
|
186
|
+
for project_id in project_id_list:
|
|
187
|
+
try:
|
|
188
|
+
result = self.change_organization_for_project(project_id, organization_name)
|
|
189
|
+
if result is not None:
|
|
190
|
+
job_list.append(result)
|
|
191
|
+
except requests.HTTPError:
|
|
192
|
+
logger.warning(f"project_id='{project_id}'の組織変更でHTTPエラーが発生しました。", exc_info=True)
|
|
193
|
+
logger.info(f"{len(job_list)}/{len(project_id_list)}件のプロジェクトの組織を'{organization_name}'に変更するジョブを発行しました。")
|
|
194
|
+
return job_list
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ChangeProjectOrganization(CommandLine):
|
|
198
|
+
def main(self) -> None:
|
|
199
|
+
args = self.args
|
|
200
|
+
project_id_list = annofabcli.common.cli.get_list_from_args(args.project_id)
|
|
201
|
+
main_obj = ChangeProjectOrganizationMain(self.service, all_yes=args.yes, is_force=args.force)
|
|
202
|
+
|
|
203
|
+
job_list = main_obj.change_organization_for_project_list(project_id_list=project_id_list, organization_name=args.organization)
|
|
204
|
+
if len(job_list) == 0:
|
|
205
|
+
logger.info("組織を変更するジョブは発行されませんでした。終了します。")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# APIリクエストを減らすため、とりあえず60秒待ちます
|
|
209
|
+
seconds = 60
|
|
210
|
+
logger.info(f"ジョブの完了を{seconds}秒待ちます。")
|
|
211
|
+
time.sleep(seconds)
|
|
212
|
+
|
|
213
|
+
# すべてのジョブが完了するまで待つ
|
|
214
|
+
asyncio.run(main_obj.wait_until_jobs_finished_async(job_list))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main(args: argparse.Namespace) -> None:
|
|
218
|
+
service = build_annofabapi_resource_and_login(args)
|
|
219
|
+
facade = AnnofabApiFacade(service)
|
|
220
|
+
ChangeProjectOrganization(service, facade, args).main()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"-p",
|
|
226
|
+
"--project_id",
|
|
227
|
+
type=str,
|
|
228
|
+
required=True,
|
|
229
|
+
nargs="+",
|
|
230
|
+
help="対象プロジェクトのproject_idを指定します。 ``file://`` を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"-org",
|
|
235
|
+
"--organization",
|
|
236
|
+
type=str,
|
|
237
|
+
required=True,
|
|
238
|
+
help="変更後の組織名を指定してください。",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--force",
|
|
243
|
+
action="store_true",
|
|
244
|
+
help="強制的に組織を変更します(将来拡張用)。",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
parser.set_defaults(subcommand_func=main)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
251
|
+
subcommand_name = "change_organization"
|
|
252
|
+
subcommand_help = "プロジェクトの所属する組織を変更します。"
|
|
253
|
+
epilog = "プロジェクトのオーナロール、変更先の組織の管理者またはオーナーロールを持つユーザで実行してください。"
|
|
254
|
+
|
|
255
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
256
|
+
parse_args(parser)
|
|
257
|
+
return parser
|
|
@@ -112,7 +112,7 @@ class ChanegProjectStatusMain:
|
|
|
112
112
|
return False
|
|
113
113
|
|
|
114
114
|
if not self.facade.contains_any_project_member_role(project_id, [ProjectMemberRole.OWNER]):
|
|
115
|
-
logger.warning(f"project_id={project_id}:
|
|
115
|
+
logger.warning(f"project_id={project_id}: オーナロールでないため、プロジェクトのステータスを変更できません。project_title={project['title']}")
|
|
116
116
|
return False
|
|
117
117
|
|
|
118
118
|
logger.debug(f"{project['title']} のステータスを{status.value} に変更します。project_id={project_id}")
|
|
@@ -187,7 +187,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
187
187
|
parser.add_argument(
|
|
188
188
|
"--force",
|
|
189
189
|
action="store_true",
|
|
190
|
-
help=f"`--status {ProjectStatus.SUSPENDED.value}`を指定している状態で、 ``--force`` を指定した場合、作業中タスクが残っていても停止状態に変更します。",
|
|
190
|
+
help=f"`--status {ProjectStatus.SUSPENDED.value}`を指定している状態で、 ``--force`` を指定した場合、作業中タスクが残っていても停止状態に変更します。",
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
parser.set_defaults(subcommand_func=main)
|
|
@@ -84,10 +84,7 @@ class CopyProject(CommandLine):
|
|
|
84
84
|
if copied_targets is not None:
|
|
85
85
|
logger.info(f"コピー対象: {[e.value for e in copied_targets]}")
|
|
86
86
|
|
|
87
|
-
confirm_message =
|
|
88
|
-
f"プロジェクト'{src_project_title}'(project_id='{src_project_id}')を、"
|
|
89
|
-
f"プロジェクト'{dest_title}'(project_id='{dest_project_id}') にコピーしますか?"
|
|
90
|
-
)
|
|
87
|
+
confirm_message = f"プロジェクト'{src_project_title}'(project_id='{src_project_id}')を、プロジェクト'{dest_title}'(project_id='{dest_project_id}') にコピーしますか?"
|
|
91
88
|
if not self.confirm_processing(confirm_message):
|
|
92
89
|
logger.info(f"プロジェクト'{src_project_title}'(project_id='{src_project_id}')をコピーせずに終了します。")
|
|
93
90
|
return
|
|
@@ -142,9 +139,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
142
139
|
|
|
143
140
|
argument_parser.add_project_id(help_message="コピー元のプロジェクトのproject_idを指定してください。")
|
|
144
141
|
|
|
145
|
-
parser.add_argument(
|
|
146
|
-
"--dest_project_id", type=str, help="新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。"
|
|
147
|
-
)
|
|
142
|
+
parser.add_argument("--dest_project_id", type=str, help="新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。")
|
|
148
143
|
parser.add_argument("--dest_title", type=str, required=True, help="新しいプロジェクトのタイトルを指定してください。")
|
|
149
144
|
parser.add_argument("--dest_overview", type=str, help="新しいプロジェクトの概要を指定してください。")
|
|
150
145
|
|
|
@@ -105,11 +105,7 @@ class DiffProjects(CommandLine):
|
|
|
105
105
|
user_ids2 = [e["user_id"] for e in sorted_members2]
|
|
106
106
|
|
|
107
107
|
if user_ids1 != user_ids2:
|
|
108
|
-
diff_message += (
|
|
109
|
-
f"### user_idのListに差分あり\n"
|
|
110
|
-
f"set(user_ids1) - set(user_ids2) = {set(user_ids1) - set(user_ids2)}\n"
|
|
111
|
-
f"set(user_ids2) - set(user_ids1) = {set(user_ids2) - set(user_ids1)}\n"
|
|
112
|
-
)
|
|
108
|
+
diff_message += f"### user_idのListに差分あり\nset(user_ids1) - set(user_ids2) = {set(user_ids1) - set(user_ids2)}\nset(user_ids2) - set(user_ids1) = {set(user_ids2) - set(user_ids1)}\n"
|
|
113
109
|
|
|
114
110
|
return True, diff_message
|
|
115
111
|
|
|
@@ -286,12 +282,8 @@ class DiffProjects(CommandLine):
|
|
|
286
282
|
diff_message += message
|
|
287
283
|
|
|
288
284
|
if DiffTarget.ANNOTATION_LABELS in diff_targets:
|
|
289
|
-
labels1_v1 = convert_annotation_specs_labels_v2_to_v1(
|
|
290
|
-
|
|
291
|
-
)
|
|
292
|
-
labels2_v1 = convert_annotation_specs_labels_v2_to_v1(
|
|
293
|
-
labels_v2=annotation_specs2["labels"], additionals_v2=annotation_specs2["additionals"]
|
|
294
|
-
)
|
|
285
|
+
labels1_v1 = convert_annotation_specs_labels_v2_to_v1(labels_v2=annotation_specs1["labels"], additionals_v2=annotation_specs1["additionals"])
|
|
286
|
+
labels2_v1 = convert_annotation_specs_labels_v2_to_v1(labels_v2=annotation_specs2["labels"], additionals_v2=annotation_specs2["additionals"])
|
|
295
287
|
|
|
296
288
|
bool_result, message = self.diff_labels_of_annotation_specs(labels1_v1, labels2_v1)
|
|
297
289
|
is_different = is_different or bool_result
|
|
@@ -393,11 +385,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
393
385
|
nargs="+",
|
|
394
386
|
choices=[e.value for e in choices],
|
|
395
387
|
default=["annotation_labels", "inspection_phrases", "members", "settings"],
|
|
396
|
-
help="比較する項目。指定しなければ全項目を比較する。"
|
|
397
|
-
"annotation_labels: アノテーション仕様のラベル情報, "
|
|
398
|
-
"inspection_phrases: 定型指摘,"
|
|
399
|
-
"members: プロジェクトメンバ,"
|
|
400
|
-
"settings: プロジェクト設定,",
|
|
388
|
+
help="比較する項目。指定しなければ全項目を比較する。annotation_labels: アノテーション仕様のラベル情報, inspection_phrases: 定型指摘,members: プロジェクトメンバ,settings: プロジェクト設定,",
|
|
401
389
|
)
|
|
402
390
|
|
|
403
391
|
parser.set_defaults(subcommand_func=main)
|
|
@@ -65,8 +65,7 @@ class PutProject(CommandLine):
|
|
|
65
65
|
}
|
|
66
66
|
new_project, _ = self.service.api.put_project(new_project_id, request_body=request_body)
|
|
67
67
|
logger.info(
|
|
68
|
-
f"'{organization}'組織に、project_id='{new_project['project_id']}'のプロジェクトを作成しました。 :: "
|
|
69
|
-
f"title='{new_project['title']}', input_data_type='{new_project['input_data_type']}'"
|
|
68
|
+
f"'{organization}'組織に、project_id='{new_project['project_id']}'のプロジェクトを作成しました。 :: title='{new_project['title']}', input_data_type='{new_project['input_data_type']}'"
|
|
70
69
|
)
|
|
71
70
|
|
|
72
71
|
COMMON_MESSAGE = "annofabcli project put: error:"
|
|
@@ -114,10 +113,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
114
113
|
type=str,
|
|
115
114
|
choices=[e.value for e in InputDataType],
|
|
116
115
|
required=True,
|
|
117
|
-
help="プロジェクトに登録する入力データの種類\n\n"
|
|
118
|
-
f" * {InputDataType.IMAGE.value} : 画像\n"
|
|
119
|
-
f" * {InputDataType.MOVIE.value} : 動画\n"
|
|
120
|
-
f" * {InputDataType.CUSTOM.value} : カスタム(点群など)",
|
|
116
|
+
help=f"プロジェクトに登録する入力データの種類\n\n * {InputDataType.IMAGE.value} : 画像\n * {InputDataType.MOVIE.value} : 動画\n * {InputDataType.CUSTOM.value} : カスタム(点群など)",
|
|
121
117
|
)
|
|
122
118
|
|
|
123
119
|
parser.add_argument("-p", "--project_id", type=str, required=False, help="作成するプロジェクトのproject_id。未指定の場合はUUIDv4になります。")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
+
import annofabcli.project.change_organization_of_project
|
|
4
5
|
import annofabcli.project.change_project_status
|
|
5
6
|
import annofabcli.project.copy_project
|
|
6
7
|
import annofabcli.project.diff_projects
|
|
@@ -13,6 +14,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
13
14
|
subparsers = parser.add_subparsers(dest="subcommand_name")
|
|
14
15
|
|
|
15
16
|
# サブコマンドの定義
|
|
17
|
+
annofabcli.project.change_organization_of_project.add_parser(subparsers)
|
|
16
18
|
annofabcli.project.change_project_status.add_parser(subparsers)
|
|
17
19
|
annofabcli.project.copy_project.add_parser(subparsers)
|
|
18
20
|
annofabcli.project.diff_projects.add_parser(subparsers)
|
|
@@ -193,7 +193,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
193
193
|
parser.add_argument(
|
|
194
194
|
"--member_info",
|
|
195
195
|
type=str,
|
|
196
|
-
help="プロジェクトメンバに対して設定するメンバ情報を、JSON形式で指定します。 ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。 "
|
|
196
|
+
help="プロジェクトメンバに対して設定するメンバ情報を、JSON形式で指定します。 ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。 "
|
|
197
197
|
"以下のキーが指定可能です。sampling_inspection_rate, sampling_acceptance_rate, "
|
|
198
198
|
"未設定にする場合は、値にnullを指定してください。"
|
|
199
199
|
"詳細は https://annofab.com/docs/api/#operation/putProjectMember を参照ください。 ",
|
|
@@ -120,10 +120,7 @@ class CopyProjectMembers(CommandLine):
|
|
|
120
120
|
account_id = member["account_id"]
|
|
121
121
|
if self.find_member(dest_organization_members, account_id) is None:
|
|
122
122
|
# コピー先の組織メンバでないので、コピーしない
|
|
123
|
-
logger.debug(
|
|
124
|
-
f"コピーしないメンバ: {member['user_id']} , {member['username']} : "
|
|
125
|
-
f"(コピー先の所属組織 {dest_organization_name} の組織メンバでないため)"
|
|
126
|
-
)
|
|
123
|
+
logger.debug(f"コピーしないメンバ: {member['user_id']} , {member['username']} : (コピー先の所属組織 {dest_organization_name} の組織メンバでないため)")
|
|
127
124
|
continue
|
|
128
125
|
|
|
129
126
|
added_members.append(member)
|
|
@@ -152,9 +149,7 @@ class CopyProjectMembers(CommandLine):
|
|
|
152
149
|
|
|
153
150
|
if len(added_members) > 0:
|
|
154
151
|
if self.confirm_processing(
|
|
155
|
-
f"'{self.src_project_title}' のプロジェクトのメンバを、"
|
|
156
|
-
f"'{self.dest_project_title}' にコピーしますか?"
|
|
157
|
-
f"追加対象: {len(added_members)} 件, 削除対象: {len(deleted_dest_members)} 件"
|
|
152
|
+
f"'{self.src_project_title}' のプロジェクトのメンバを、'{self.dest_project_title}' にコピーしますか?追加対象: {len(added_members)} 件, 削除対象: {len(deleted_dest_members)} 件"
|
|
158
153
|
):
|
|
159
154
|
self.put_project_members(dest_project_id, updated_members)
|
|
160
155
|
else:
|