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.
Files changed (136) hide show
  1. annofabcli/__main__.py +1 -1
  2. annofabcli/annotation/annotation_query.py +9 -29
  3. annofabcli/annotation/change_annotation_attributes.py +6 -14
  4. annofabcli/annotation/change_annotation_properties.py +5 -12
  5. annofabcli/annotation/copy_annotation.py +4 -10
  6. annofabcli/annotation/delete_annotation.py +10 -26
  7. annofabcli/annotation/dump_annotation.py +1 -4
  8. annofabcli/annotation/import_annotation.py +15 -39
  9. annofabcli/annotation/list_annotation.py +1 -4
  10. annofabcli/annotation/merge_segmentation.py +5 -15
  11. annofabcli/annotation/remove_segmentation_overlap.py +8 -29
  12. annofabcli/annotation/restore_annotation.py +3 -9
  13. annofabcli/annotation_specs/add_attribute_restriction.py +2 -8
  14. annofabcli/annotation_specs/attribute_restriction.py +2 -10
  15. annofabcli/annotation_specs/export_annotation_specs.py +1 -3
  16. annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py +3 -10
  17. annofabcli/annotation_specs/get_annotation_specs_with_choice_id_replaced.py +4 -10
  18. annofabcli/annotation_specs/get_annotation_specs_with_label_id_replaced.py +1 -3
  19. annofabcli/annotation_specs/list_annotation_specs_attribute.py +7 -18
  20. annofabcli/annotation_specs/list_annotation_specs_choice.py +3 -8
  21. annofabcli/annotation_specs/list_annotation_specs_history.py +0 -1
  22. annofabcli/annotation_specs/list_annotation_specs_label.py +3 -8
  23. annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +4 -9
  24. annofabcli/annotation_specs/list_attribute_restriction.py +3 -9
  25. annofabcli/annotation_specs/put_label_color.py +1 -6
  26. annofabcli/comment/delete_comment.py +3 -9
  27. annofabcli/comment/list_all_comment.py +15 -5
  28. annofabcli/comment/list_comment.py +46 -7
  29. annofabcli/comment/put_comment.py +4 -13
  30. annofabcli/comment/put_comment_simply.py +2 -6
  31. annofabcli/comment/put_inspection_comment.py +2 -6
  32. annofabcli/comment/put_inspection_comment_simply.py +3 -6
  33. annofabcli/comment/put_onhold_comment.py +2 -6
  34. annofabcli/comment/put_onhold_comment_simply.py +2 -4
  35. annofabcli/common/cli.py +5 -43
  36. annofabcli/common/download.py +8 -25
  37. annofabcli/common/image.py +3 -7
  38. annofabcli/common/utils.py +2 -4
  39. annofabcli/common/visualize.py +2 -4
  40. annofabcli/filesystem/draw_annotation.py +6 -18
  41. annofabcli/filesystem/filter_annotation.py +7 -24
  42. annofabcli/filesystem/mask_user_info.py +2 -5
  43. annofabcli/filesystem/merge_annotation.py +2 -6
  44. annofabcli/input_data/change_input_data_name.py +3 -7
  45. annofabcli/input_data/copy_input_data.py +6 -14
  46. annofabcli/input_data/delete_input_data.py +7 -24
  47. annofabcli/input_data/delete_metadata_key_of_input_data.py +5 -16
  48. annofabcli/input_data/list_all_input_data.py +5 -14
  49. annofabcli/input_data/list_all_input_data_merged_task.py +8 -23
  50. annofabcli/input_data/list_input_data.py +5 -16
  51. annofabcli/input_data/put_input_data.py +7 -19
  52. annofabcli/input_data/update_metadata_of_input_data.py +6 -14
  53. annofabcli/instruction/list_instruction_history.py +0 -1
  54. annofabcli/instruction/upload_instruction.py +4 -7
  55. annofabcli/job/list_job.py +2 -3
  56. annofabcli/job/list_last_job.py +1 -3
  57. annofabcli/organization/list_organization.py +0 -1
  58. annofabcli/organization_member/change_organization_member.py +1 -3
  59. annofabcli/organization_member/delete_organization_member.py +2 -6
  60. annofabcli/organization_member/invite_organization_member.py +1 -3
  61. annofabcli/organization_member/list_organization_member.py +0 -1
  62. annofabcli/project/change_organization_of_project.py +257 -0
  63. annofabcli/project/change_project_status.py +2 -2
  64. annofabcli/project/copy_project.py +2 -7
  65. annofabcli/project/diff_projects.py +4 -16
  66. annofabcli/project/list_project.py +0 -1
  67. annofabcli/project/put_project.py +2 -6
  68. annofabcli/project/subcommand_project.py +2 -0
  69. annofabcli/project_member/change_project_members.py +1 -1
  70. annofabcli/project_member/copy_project_members.py +2 -7
  71. annofabcli/project_member/drop_project_members.py +1 -3
  72. annofabcli/project_member/invite_project_members.py +2 -4
  73. annofabcli/project_member/list_users.py +0 -1
  74. annofabcli/project_member/put_project_members.py +4 -12
  75. annofabcli/stat_visualization/mask_visualization_dir.py +6 -16
  76. annofabcli/stat_visualization/merge_visualization_dir.py +7 -19
  77. annofabcli/stat_visualization/summarize_whole_performance_csv.py +3 -7
  78. annofabcli/stat_visualization/write_graph.py +5 -15
  79. annofabcli/stat_visualization/write_performance_rating_csv.py +4 -12
  80. annofabcli/statistics/list_annotation_area.py +3 -7
  81. annofabcli/statistics/list_annotation_attribute.py +6 -15
  82. annofabcli/statistics/list_annotation_attribute_filled_count.py +9 -23
  83. annofabcli/statistics/list_annotation_count.py +18 -44
  84. annofabcli/statistics/list_annotation_duration.py +14 -40
  85. annofabcli/statistics/list_video_duration.py +2 -3
  86. annofabcli/statistics/list_worktime.py +0 -1
  87. annofabcli/statistics/scatter.py +3 -9
  88. annofabcli/statistics/summarize_task_count.py +7 -12
  89. annofabcli/statistics/summarize_task_count_by_task_id_group.py +3 -11
  90. annofabcli/statistics/summarize_task_count_by_user.py +1 -5
  91. annofabcli/statistics/visualization/dataframe/annotation_count.py +2 -4
  92. annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +6 -12
  93. annofabcli/statistics/visualization/dataframe/productivity_per_date.py +10 -22
  94. annofabcli/statistics/visualization/dataframe/project_performance.py +1 -3
  95. annofabcli/statistics/visualization/dataframe/task.py +2 -5
  96. annofabcli/statistics/visualization/dataframe/task_history.py +1 -1
  97. annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +6 -20
  98. annofabcli/statistics/visualization/dataframe/user_performance.py +29 -88
  99. annofabcli/statistics/visualization/dataframe/whole_performance.py +6 -12
  100. annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +17 -49
  101. annofabcli/statistics/visualization/dataframe/worktime_per_date.py +4 -10
  102. annofabcli/statistics/visualization/filtering_query.py +2 -6
  103. annofabcli/statistics/visualization/project_dir.py +9 -26
  104. annofabcli/statistics/visualization/visualization_source_files.py +3 -10
  105. annofabcli/statistics/visualize_annotation_count.py +9 -23
  106. annofabcli/statistics/visualize_annotation_duration.py +5 -15
  107. annofabcli/statistics/visualize_statistics.py +18 -53
  108. annofabcli/statistics/visualize_video_duration.py +8 -19
  109. annofabcli/supplementary/delete_supplementary_data.py +7 -23
  110. annofabcli/supplementary/list_supplementary_data.py +1 -1
  111. annofabcli/supplementary/put_supplementary_data.py +5 -15
  112. annofabcli/task/cancel_acceptance.py +3 -4
  113. annofabcli/task/change_operator.py +3 -11
  114. annofabcli/task/change_status_to_break.py +1 -1
  115. annofabcli/task/change_status_to_on_hold.py +5 -18
  116. annofabcli/task/complete_tasks.py +8 -25
  117. annofabcli/task/copy_tasks.py +2 -3
  118. annofabcli/task/delete_metadata_key_of_task.py +2 -6
  119. annofabcli/task/delete_tasks.py +8 -26
  120. annofabcli/task/list_all_tasks.py +2 -4
  121. annofabcli/task/list_tasks.py +3 -7
  122. annofabcli/task/list_tasks_added_task_history.py +7 -21
  123. annofabcli/task/put_tasks.py +2 -3
  124. annofabcli/task/put_tasks_by_count.py +3 -7
  125. annofabcli/task/reject_tasks.py +7 -19
  126. annofabcli/task/update_metadata_of_task.py +2 -2
  127. annofabcli/task_history/list_all_task_history.py +2 -5
  128. annofabcli/task_history/list_task_history.py +0 -1
  129. annofabcli/task_history_event/list_all_task_history_event.py +4 -11
  130. annofabcli/task_history_event/list_worktime.py +4 -14
  131. {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/METADATA +1 -1
  132. annofabcli-1.104.0.dist-info/RECORD +215 -0
  133. annofabcli-1.102.1.dist-info/RECORD +0 -214
  134. {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/WHEEL +0 -0
  135. {annofabcli-1.102.1.dist-info → annofabcli-1.104.0.dist-info}/entry_points.txt +0 -0
  136. {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`` に指定されたキーのみ更新されます。", # noqa: E501
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`` を指定してください。指定しない場合は、逐次的に処理します。", # noqa: E501
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] == "/": # noqa: SIM108
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: # noqa: SIM108
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: # noqa: SIM108
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)
@@ -23,7 +23,7 @@ class ListJob(CommandLine):
23
23
  ジョブ一覧を取得する。
24
24
  """
25
25
 
26
- if job_query is not None: # noqa: SIM108
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 を参照してください。", # noqa: E501
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
 
@@ -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
 
@@ -35,7 +35,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
35
35
  default=FormatArgument.CSV,
36
36
  )
37
37
  argument_parser.add_output()
38
- argument_parser.add_csv_format()
39
38
 
40
39
  parser.set_defaults(subcommand_func=main)
41
40
 
@@ -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
 
@@ -83,7 +83,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
83
83
  default=FormatArgument.CSV,
84
84
  )
85
85
  argument_parser.add_output()
86
- argument_parser.add_csv_format()
87
86
 
88
87
  parser.set_defaults(subcommand_func=main)
89
88
 
@@ -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}: オーナロールでないため、アノテーションzipを更新できません。project_title={project['title']}")
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`` を指定した場合、作業中タスクが残っていても停止状態に変更します。", # noqa: E501
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
- labels_v2=annotation_specs1["labels"], additionals_v2=annotation_specs1["additionals"]
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)
@@ -250,7 +250,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
250
250
  default=FormatArgument.CSV,
251
251
  )
252
252
  argument_parser.add_output()
253
- argument_parser.add_csv_format()
254
253
 
255
254
  parser.set_defaults(subcommand_func=main)
256
255
 
@@ -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形式のファイルを指定できます。 " # noqa: E501
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: