annofabcli 1.102.0__py3-none-any.whl → 1.103.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 (134) hide show
  1. annofabcli/annotation/annotation_query.py +9 -29
  2. annofabcli/annotation/change_annotation_attributes.py +6 -14
  3. annofabcli/annotation/change_annotation_properties.py +5 -12
  4. annofabcli/annotation/copy_annotation.py +9 -11
  5. annofabcli/annotation/delete_annotation.py +21 -26
  6. annofabcli/annotation/dump_annotation.py +1 -4
  7. annofabcli/annotation/import_annotation.py +16 -40
  8. annofabcli/annotation/list_annotation.py +1 -4
  9. annofabcli/annotation/merge_segmentation.py +10 -16
  10. annofabcli/annotation/remove_segmentation_overlap.py +14 -30
  11. annofabcli/annotation/restore_annotation.py +3 -9
  12. annofabcli/annotation_specs/add_attribute_restriction.py +2 -8
  13. annofabcli/annotation_specs/attribute_restriction.py +2 -10
  14. annofabcli/annotation_specs/export_annotation_specs.py +1 -3
  15. annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py +3 -10
  16. annofabcli/annotation_specs/get_annotation_specs_with_choice_id_replaced.py +4 -10
  17. annofabcli/annotation_specs/get_annotation_specs_with_label_id_replaced.py +1 -3
  18. annofabcli/annotation_specs/list_annotation_specs_attribute.py +7 -18
  19. annofabcli/annotation_specs/list_annotation_specs_choice.py +3 -8
  20. annofabcli/annotation_specs/list_annotation_specs_history.py +0 -1
  21. annofabcli/annotation_specs/list_annotation_specs_label.py +3 -8
  22. annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +4 -9
  23. annofabcli/annotation_specs/list_attribute_restriction.py +3 -9
  24. annofabcli/annotation_specs/put_label_color.py +1 -6
  25. annofabcli/comment/delete_comment.py +3 -9
  26. annofabcli/comment/list_all_comment.py +2 -4
  27. annofabcli/comment/list_comment.py +1 -4
  28. annofabcli/comment/put_comment.py +4 -13
  29. annofabcli/comment/put_comment_simply.py +2 -6
  30. annofabcli/comment/put_inspection_comment.py +2 -6
  31. annofabcli/comment/put_inspection_comment_simply.py +3 -6
  32. annofabcli/comment/put_onhold_comment.py +2 -6
  33. annofabcli/comment/put_onhold_comment_simply.py +2 -4
  34. annofabcli/common/cli.py +5 -43
  35. annofabcli/common/download.py +8 -25
  36. annofabcli/common/image.py +5 -9
  37. annofabcli/common/utils.py +1 -3
  38. annofabcli/common/visualize.py +2 -4
  39. annofabcli/filesystem/draw_annotation.py +8 -20
  40. annofabcli/filesystem/filter_annotation.py +7 -24
  41. annofabcli/filesystem/mask_user_info.py +3 -6
  42. annofabcli/filesystem/merge_annotation.py +2 -6
  43. annofabcli/input_data/change_input_data_name.py +3 -7
  44. annofabcli/input_data/copy_input_data.py +6 -14
  45. annofabcli/input_data/delete_input_data.py +7 -24
  46. annofabcli/input_data/delete_metadata_key_of_input_data.py +5 -16
  47. annofabcli/input_data/list_all_input_data.py +5 -14
  48. annofabcli/input_data/list_all_input_data_merged_task.py +8 -23
  49. annofabcli/input_data/list_input_data.py +5 -16
  50. annofabcli/input_data/put_input_data.py +7 -19
  51. annofabcli/input_data/update_metadata_of_input_data.py +6 -14
  52. annofabcli/instruction/list_instruction_history.py +0 -1
  53. annofabcli/instruction/upload_instruction.py +1 -4
  54. annofabcli/job/list_job.py +1 -2
  55. annofabcli/job/list_last_job.py +1 -3
  56. annofabcli/organization/list_organization.py +0 -1
  57. annofabcli/organization_member/change_organization_member.py +1 -3
  58. annofabcli/organization_member/delete_organization_member.py +32 -16
  59. annofabcli/organization_member/invite_organization_member.py +25 -14
  60. annofabcli/organization_member/list_organization_member.py +0 -1
  61. annofabcli/project/change_organization_of_project.py +257 -0
  62. annofabcli/project/change_project_status.py +2 -2
  63. annofabcli/project/copy_project.py +2 -7
  64. annofabcli/project/diff_projects.py +4 -16
  65. annofabcli/project/list_project.py +0 -1
  66. annofabcli/project/put_project.py +2 -6
  67. annofabcli/project/subcommand_project.py +2 -0
  68. annofabcli/project_member/change_project_members.py +2 -2
  69. annofabcli/project_member/copy_project_members.py +2 -7
  70. annofabcli/project_member/drop_project_members.py +1 -3
  71. annofabcli/project_member/invite_project_members.py +1 -3
  72. annofabcli/project_member/list_users.py +0 -1
  73. annofabcli/project_member/put_project_members.py +4 -12
  74. annofabcli/stat_visualization/mask_visualization_dir.py +6 -16
  75. annofabcli/stat_visualization/merge_visualization_dir.py +6 -18
  76. annofabcli/stat_visualization/summarize_whole_performance_csv.py +3 -7
  77. annofabcli/stat_visualization/write_graph.py +5 -15
  78. annofabcli/stat_visualization/write_performance_rating_csv.py +4 -12
  79. annofabcli/statistics/list_annotation_area.py +3 -7
  80. annofabcli/statistics/list_annotation_attribute.py +6 -15
  81. annofabcli/statistics/list_annotation_attribute_filled_count.py +9 -23
  82. annofabcli/statistics/list_annotation_count.py +18 -44
  83. annofabcli/statistics/list_annotation_duration.py +14 -40
  84. annofabcli/statistics/list_video_duration.py +2 -3
  85. annofabcli/statistics/list_worktime.py +0 -1
  86. annofabcli/statistics/scatter.py +3 -9
  87. annofabcli/statistics/summarize_task_count.py +7 -12
  88. annofabcli/statistics/summarize_task_count_by_task_id_group.py +3 -11
  89. annofabcli/statistics/summarize_task_count_by_user.py +1 -5
  90. annofabcli/statistics/visualization/dataframe/annotation_count.py +1 -3
  91. annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +3 -9
  92. annofabcli/statistics/visualization/dataframe/productivity_per_date.py +11 -23
  93. annofabcli/statistics/visualization/dataframe/project_performance.py +1 -3
  94. annofabcli/statistics/visualization/dataframe/task.py +2 -5
  95. annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +6 -20
  96. annofabcli/statistics/visualization/dataframe/user_performance.py +29 -88
  97. annofabcli/statistics/visualization/dataframe/whole_performance.py +4 -10
  98. annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +17 -49
  99. annofabcli/statistics/visualization/dataframe/worktime_per_date.py +3 -9
  100. annofabcli/statistics/visualization/filtering_query.py +2 -6
  101. annofabcli/statistics/visualization/project_dir.py +9 -26
  102. annofabcli/statistics/visualization/visualization_source_files.py +3 -10
  103. annofabcli/statistics/visualize_annotation_count.py +7 -21
  104. annofabcli/statistics/visualize_annotation_duration.py +7 -17
  105. annofabcli/statistics/visualize_statistics.py +17 -52
  106. annofabcli/statistics/visualize_video_duration.py +8 -19
  107. annofabcli/supplementary/delete_supplementary_data.py +7 -23
  108. annofabcli/supplementary/list_supplementary_data.py +1 -1
  109. annofabcli/supplementary/put_supplementary_data.py +5 -15
  110. annofabcli/task/cancel_acceptance.py +3 -4
  111. annofabcli/task/change_operator.py +3 -11
  112. annofabcli/task/change_status_to_break.py +1 -1
  113. annofabcli/task/change_status_to_on_hold.py +5 -18
  114. annofabcli/task/complete_tasks.py +8 -25
  115. annofabcli/task/copy_tasks.py +2 -3
  116. annofabcli/task/delete_metadata_key_of_task.py +2 -6
  117. annofabcli/task/delete_tasks.py +7 -25
  118. annofabcli/task/list_all_tasks.py +2 -4
  119. annofabcli/task/list_tasks.py +2 -6
  120. annofabcli/task/list_tasks_added_task_history.py +7 -21
  121. annofabcli/task/put_tasks.py +2 -3
  122. annofabcli/task/put_tasks_by_count.py +3 -7
  123. annofabcli/task/reject_tasks.py +7 -19
  124. annofabcli/task/update_metadata_of_task.py +1 -1
  125. annofabcli/task_history/list_all_task_history.py +2 -5
  126. annofabcli/task_history/list_task_history.py +0 -1
  127. annofabcli/task_history_event/list_all_task_history_event.py +4 -11
  128. annofabcli/task_history_event/list_worktime.py +4 -14
  129. {annofabcli-1.102.0.dist-info → annofabcli-1.103.0.dist-info}/METADATA +1 -1
  130. annofabcli-1.103.0.dist-info/RECORD +215 -0
  131. annofabcli-1.102.0.dist-info/RECORD +0 -214
  132. {annofabcli-1.102.0.dist-info → annofabcli-1.103.0.dist-info}/WHEEL +0 -0
  133. {annofabcli-1.102.0.dist-info → annofabcli-1.103.0.dist-info}/entry_points.txt +0 -0
  134. {annofabcli-1.102.0.dist-info → annofabcli-1.103.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
 
@@ -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)
@@ -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:
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  import annofabapi
9
9
  import more_itertools
10
+ from annofabapi.models import OrganizationMemberRole
10
11
 
11
12
  import annofabcli
12
13
  import annofabcli.common.cli
@@ -35,8 +36,12 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
35
36
  def get_member(organization_member_list: list[dict[str, Any]], user_id: str) -> Optional[dict[str, Any]]:
36
37
  return more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id)
37
38
 
38
- def main(self, organization_name: str, user_ids: Collection[str]) -> None:
39
- logger.info(f"{len(user_ids)} 件のユーザを組織'{organization_name}'から脱退させます。")
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(organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}):
41
+ logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを脱退できるロールを持たないため、スキップします。")
42
+ return
43
+
44
+ logger.info(f"{len(user_ids)} 件のメンバーを組織'{organization_name}'から脱退させます。")
40
45
 
41
46
  member_list = self.service.wrapper.get_all_organization_members(organization_name)
42
47
 
@@ -45,24 +50,28 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
45
50
  for user_id in user_ids:
46
51
  member = self.get_member(member_list, user_id)
47
52
  if member is None:
48
- logger.warning(f"組織メンバにuser_id='{user_id}'のユーザが存在しません。")
53
+ logger.warning(f"組織'{organization_name}'に user_id='{user_id}'のメンバーが存在しません。")
49
54
  continue
50
55
 
51
- if not self.confirm_processing(
52
- f"user_id='{user_id}'のユーザを組織から脱退させますか? :: username='{member['username']}', role='{member['role']}'"
53
- ):
56
+ if not self.confirm_processing(f"組織'{organization_name}'に所属する user_id='{user_id}'のメンバーを脱退させますか? :: username='{member['username']}'"):
54
57
  continue
55
58
 
56
- logger.debug(f"user_id='{user_id}'のユーザを組織から脱退させます。 :: username='{member['username']}', role='{member['role']}'")
57
59
  try:
58
60
  self.service.api.delete_organization_member(organization_name, user_id)
59
- logger.debug(f"user_id='{user_id}'のユーザを組織'{organization_name}'から脱退させました。")
61
+ logger.debug(f"組織'{organization_name}'から user_id='{user_id}'のメンバーを脱退させました。")
60
62
  success_count += 1
61
63
 
62
64
  except Exception: # pylint: disable=broad-except
63
- logger.warning(f"user_id='{user_id}'のユーザを組織'{organization_name}'から脱退させるのに失敗しました。", exc_info=True)
65
+ logger.warning(f"組織'{organization_name}'から user_id='{user_id}'のメンバーを脱退させるのに失敗しました。", exc_info=True)
66
+
67
+ logger.info(f"{success_count} / {len(user_ids)} 件のメンバーを組織'{organization_name}'から脱退させました。")
64
68
 
65
- logger.info(f"{success_count} / {len(user_ids)} 件のユーザを組織'{organization_name}'から脱退させました。")
69
+ def delete_organization_members_from_organizations(self, organization_names: list[str], user_ids: Collection[str]) -> None:
70
+ for organization_name in organization_names:
71
+ try:
72
+ self.delete_organization_members_from_organization(organization_name, user_ids)
73
+ except Exception: # pylint
74
+ logger.warning(f"組織'{organization_name}'からメンバーを脱退させるのに失敗しました。", exc_info=True)
66
75
 
67
76
 
68
77
  class DeleteOrganizationMember(CommandLine):
@@ -70,9 +79,10 @@ class DeleteOrganizationMember(CommandLine):
70
79
  args = self.args
71
80
 
72
81
  user_id_list = annofabcli.common.cli.get_list_from_args(args.user_id)
82
+ organization_name_list = annofabcli.common.cli.get_list_from_args(args.organization)
73
83
 
74
84
  main_obj = DeleteOrganizationMemberMain(self.service, all_yes=args.yes)
75
- main_obj.main(organization_name=args.organization, user_ids=user_id_list)
85
+ main_obj.delete_organization_members_from_organizations(organization_name_list, user_id_list)
76
86
 
77
87
 
78
88
  def main(args: argparse.Namespace) -> None:
@@ -82,7 +92,14 @@ def main(args: argparse.Namespace) -> None:
82
92
 
83
93
 
84
94
  def parse_args(parser: argparse.ArgumentParser) -> None:
85
- parser.add_argument("-org", "--organization", required=True, type=str, help="対象の組織の組織名を指定してください。")
95
+ parser.add_argument(
96
+ "-org",
97
+ "--organization",
98
+ nargs="+",
99
+ required=True,
100
+ type=str,
101
+ help="対象の組織名を指定してます。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
102
+ )
86
103
 
87
104
  parser.add_argument(
88
105
  "-u",
@@ -90,7 +107,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
90
107
  type=str,
91
108
  nargs="+",
92
109
  required=True,
93
- help="組織から脱退させるユーザのuser_idを指定してください。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
110
+ help="組織から脱退させるメンバーのuser_idを指定します。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
94
111
  )
95
112
 
96
113
  parser.set_defaults(subcommand_func=main)
@@ -98,10 +115,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
98
115
 
99
116
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
100
117
  subcommand_name = "delete"
101
- subcommand_help = "組織からユーザを脱退させます。"
102
- description = "組織からユーザを脱退させます。"
118
+ subcommand_help = "組織からメンバーを脱退させます。"
103
119
  epilog = "組織オーナまたは組織管理者ロールを持つユーザで実行してください。"
104
120
 
105
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, description=description, epilog=epilog)
121
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, epilog=epilog)
106
122
  parse_args(parser)
107
123
  return parser
@@ -29,8 +29,12 @@ class InviteOrganizationMemberMain(CommandLineWithConfirm):
29
29
  self.facade = AnnofabApiFacade(service)
30
30
  super().__init__(all_yes)
31
31
 
32
- def main(self, organization_name: str, user_ids: Collection[str], role: str) -> None:
33
- logger.info(f"{len(user_ids)} 件のユーザを組織'{organization_name}'に招待して、ロール'{role}'を付与します。")
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(organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}):
34
+ logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを招待できるロールを持たないため、スキップします。")
35
+ return
36
+
37
+ logger.info(f"{len(user_ids)} 件のメンバーを組織'{organization_name}'に招待して、ロール'{role}'を付与します。")
34
38
 
35
39
  organization_member_list = self.service.wrapper.get_all_organization_members(organization_name)
36
40
  all_user_ids = {e["user_id"] for e in organization_member_list}
@@ -39,21 +43,28 @@ class InviteOrganizationMemberMain(CommandLineWithConfirm):
39
43
  success_count = 0
40
44
  for user_id in user_ids:
41
45
  if user_id in all_user_ids:
42
- logger.warning(f"user_id='{user_id}'のユーザは、すでに組織'{organization_name}'に存在しています。")
46
+ logger.warning(f"user_id='{user_id}'のメンバーは、すでに組織'{organization_name}'に存在しているため、スキップします。")
43
47
  continue
44
48
 
45
- if not self.confirm_processing(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待しますか? :: role='{role}'"):
49
+ if not self.confirm_processing(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待して、ロール'{role}'を付与しますか?"):
46
50
  continue
47
51
 
48
52
  try:
49
53
  self.service.api.invite_organization_member(organization_name, user_id, request_body={"role": role})
50
- logger.debug(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待しました。")
54
+ logger.debug(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待しました。")
51
55
  success_count += 1
52
56
 
53
57
  except Exception: # pylint: disable=broad-except
54
- logger.warning(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待するのに失敗しました。", exc_info=True)
58
+ logger.warning(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待するのに失敗しました。", exc_info=True)
59
+
60
+ logger.info(f"{success_count} / {len(user_ids)} 件のメンバーを組織'{organization_name}'に招待しました。")
55
61
 
56
- logger.info(f"{success_count} / {len(user_ids)} 件のユーザを組織'{organization_name}'に招待しました。")
62
+ def invite_members_to_organizations(self, organization_names: list[str], user_ids: Collection[str], role: str) -> None:
63
+ for organization_name in organization_names:
64
+ try:
65
+ self.invite_members_to_organization(organization_name, user_ids, role)
66
+ except Exception: # pylint
67
+ logger.warning(f"組織'{organization_name}'にメンバーを招待するのに失敗しました。", exc_info=True)
57
68
 
58
69
 
59
70
  class InviteOrganizationMember(CommandLine):
@@ -61,9 +72,10 @@ class InviteOrganizationMember(CommandLine):
61
72
  args = self.args
62
73
 
63
74
  user_id_list = annofabcli.common.cli.get_list_from_args(args.user_id)
75
+ organization_name_list = annofabcli.common.cli.get_list_from_args(args.organization)
64
76
 
65
77
  main_obj = InviteOrganizationMemberMain(self.service, all_yes=args.yes)
66
- main_obj.main(organization_name=args.organization, user_ids=user_id_list, role=args.role)
78
+ main_obj.invite_members_to_organizations(organization_name_list, user_id_list, role=args.role)
67
79
 
68
80
 
69
81
  def main(args: argparse.Namespace) -> None:
@@ -73,7 +85,7 @@ def main(args: argparse.Namespace) -> None:
73
85
 
74
86
 
75
87
  def parse_args(parser: argparse.ArgumentParser) -> None:
76
- parser.add_argument("-org", "--organization", required=True, type=str, help="対象の組織の組織名を指定してください。")
88
+ parser.add_argument("-org", "--organization", nargs="+", required=True, type=str, help="招待先の組織名を指定してください。")
77
89
 
78
90
  parser.add_argument(
79
91
  "-u",
@@ -81,7 +93,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
81
93
  type=str,
82
94
  nargs="+",
83
95
  required=True,
84
- help="組織に招待するユーザのuser_idを指定してください。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
96
+ help="組織に招待するメンバーのuser_idを指定してます。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
85
97
  )
86
98
 
87
99
  role_choices = [e.value for e in OrganizationMemberRole]
@@ -90,7 +102,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
90
102
  type=str,
91
103
  choices=role_choices,
92
104
  required=True,
93
- help="招待するユーザに割り当てる組織メンバのロールを指定してください。",
105
+ help="招待するメンバーに割り当てるロールを指定してください。",
94
106
  )
95
107
 
96
108
  parser.set_defaults(subcommand_func=main)
@@ -98,10 +110,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
98
110
 
99
111
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
100
112
  subcommand_name = "invite"
101
- subcommand_help = "組織にユーザを招待します。"
102
- description = "組織にユーザを招待します。"
113
+ subcommand_help = "組織にメンバーを招待します。"
103
114
  epilog = "組織オーナまたは組織管理者ロールを持つユーザで実行してください。"
104
115
 
105
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, description=description, epilog=epilog)
116
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, epilog=epilog)
106
117
  parse_args(parser)
107
118
  return parser
@@ -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)