Skip to content

Commit 9ae7c67

Browse files
authored
Merge pull request #142 from kurusugawa-computer/update-loging
update log mesasge. です・ます調に修正
2 parents 10e8a46 + b91f177 commit 9ae7c67

File tree

9 files changed

+305
-37
lines changed

9 files changed

+305
-37
lines changed

annofabapi/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.28.0'
1+
__version__ = '0.28.1'

annofabapi/generated_api.py

+2
Original file line numberDiff line numberDiff line change
@@ -1344,7 +1344,9 @@ def get_projects_of_organization(self, organization_name: str, query_params: Opt
13441344
limit (int): 1ページあたりの取得するデータ件数
13451345
account_id (str): 指定したアカウントIDをメンバーに持つプロジェクトで絞り込む。
13461346
except_account_id (str): 指定したアカウントIDをメンバーに持たないプロジェクトで絞り込む。
1347+
title (str): プロジェクトタイトルでの部分一致検索。1文字以上あれば使用します。利便性のため、大文字小文字は区別しません。
13471348
status (ProjectStatus): 指定した状態のプロジェクトで絞り込む。未指定時は全プロジェクト。
1349+
input_data_type (InputDataType): 指定した入力データ種別でプロジェクトを絞り込む。未指定時は全プロジェクト。
13481350
sort_by (str): `date` を指定することでプロジェクトの最新のタスク更新時間の順にソートして出力する。 未指定時はプロジェクト名でソートする。
13491351
13501352
Returns:

annofabapi/models.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3042,8 +3042,6 @@ class ProjectStatus(Enum):
30423042
30433043
* biography: str
30443044
人物紹介、略歴。 この属性は、AnnoFab外の所属先や肩書などを表すために用います。 AnnoFab上の「複数の組織」で活動する場合、本籍を示すのに便利です。
3045-
* token: Token
3046-
30473045
* last_updated_datetime: str
30483046
新規作成時は未指定、更新時は必須(更新前の日時)
30493047
@@ -3535,9 +3533,9 @@ class TaskAssignmentType(Enum):
35353533
* status: TaskStatus
35363534
次に遷移させるタスクの状態。[詳細はこちら](#section/TaskStatus)。
35373535
* last_updated_datetime: str
3538-
新規作成時は未指定、更新時は必須(更新前の日時)
3536+
タスクの最終更新日時
35393537
* account_id: str
3540-
変更後の担当者のアカウントID
3538+
変更後の担当者のアカウントID。担当者を未割り当てにする場合は未指定。
35413539
35423540
"""
35433541

@@ -3592,7 +3590,7 @@ class TaskPhase(Enum):
35923590

35933591
class TaskStatus(Enum):
35943592
"""
3595-
* `not_started` - 未着手。 * `working` - 作業中。誰かが実際にエディタ上で作業している状態。 * `on_hold` - 保留。作業ルールの確認などで作業できない状態。 * `break` - 休憩中。 * `complete` - 完了。次のフェーズへ進む * `rejected` - 差戻し。修正のため、`annotation`フェーズへ戻る。 * `cancelled` - 提出取消し。修正のため、前フェーズへ戻る。
3593+
* `not_started` - 未着手。 * `working` - 作業中。誰かが実際にエディタ上で作業している状態。 * `on_hold` - 保留。作業ルールの確認などで作業できない状態。 * `break` - 休憩中。 * `complete` - 完了。次のフェーズへ進む * `rejected` - 差戻し。修正のため、`annotation`フェーズへ戻る。[operateTask](#operation/operateTask) APIのリクエストボディに渡すときのみ利用する。その他のAPIのリクエストやレスポンスには使われない。 * `cancelled` - 提出取消し。修正のため、前フェーズへ戻る。[operateTask](#operation/operateTask) APIのリクエストボディに渡すときのみ利用する。その他のAPIのリクエストやレスポンスには使われない
35963594
"""
35973595

35983596
NOT_STARTED = "not_started"

annofabapi/resource.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def build_from_netrc(endpoint_url: str = DEFAULT_ENDPOINT_URL) -> Resource:
7474
if login_user_id is None or login_password is None:
7575
raise AnnofabApiException("User ID or password in the .netrc file are None.")
7676

77-
logger.debug(".netrcファイルからAnnoFab認証情報を読み込んだ。")
77+
logger.debug(".netrcファイルからAnnoFab認証情報を読み込みました。")
7878
return Resource(login_user_id, login_password, endpoint_url=endpoint_url)
7979

8080

@@ -94,5 +94,5 @@ def build_from_env(endpoint_url: str = DEFAULT_ENDPOINT_URL) -> Resource:
9494
if login_user_id is None or login_password is None:
9595
raise AnnofabApiException("`ANNOFAB_USER_ID` or `ANNOFAB_PASSWORD` environment variable are empty.")
9696

97-
logger.debug("環境変数からAnnoFab認証情報を読み込んだ。")
97+
logger.debug("環境変数からAnnoFab認証情報を読み込みました。")
9898
return Resource(login_user_id, login_password, endpoint_url=endpoint_url)

annofabapi/utils.py

+20
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,23 @@ def get_task_history_index_skipped_inspection(task_history_list: List[TaskHistor
220220
index_list.append(index)
221221

222222
return index_list
223+
224+
225+
def first_true(iterable, default=None, pred=None):
226+
"""
227+
Returns the first true value in the iterable.
228+
229+
If no true value is found, returns *default*
230+
231+
If *pred* is not None, returns the first item for which
232+
``pred(item) == True`` .
233+
234+
>>> first_true(range(10))
235+
1
236+
>>> first_true(range(10), pred=lambda x: x > 5)
237+
6
238+
>>> first_true(range(10), default='missing', pred=lambda x: x > 9)
239+
'missing'
240+
241+
"""
242+
return next(filter(pred, iterable), default)

annofabapi/wrapper.py

+232-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
1+
# pylint: disable=too-many-lines
12
import copy
23
import logging
34
import mimetypes
45
import time
56
import urllib
67
import urllib.parse
8+
import uuid
79
import warnings
10+
from dataclasses import dataclass
811
from typing import Any, Callable, Dict, List, Optional
912

1013
import annofabapi.utils
1114
from annofabapi import AnnofabApi
1215
from annofabapi.exceptions import AnnofabApiException
13-
from annofabapi.models import (AnnotationSpecsV1, InputData, Inspection, InspectionStatus, Instruction, JobInfo,
14-
JobStatus, JobType, MyOrganization, Organization, OrganizationMember, Project,
15-
ProjectMember, SupplementaryData, Task)
16-
from annofabapi.utils import allow_404_error
16+
from annofabapi.models import (AnnotationDataHoldingType, AnnotationSpecsV1, InputData, Inspection, InspectionStatus,
17+
Instruction, JobInfo, JobStatus, JobType, MyOrganization, Organization,
18+
OrganizationMember, Project, ProjectMember, SupplementaryData, Task)
19+
from annofabapi.utils import allow_404_error, first_true
1720

1821
logger = logging.getLogger(__name__)
1922

2023

24+
@dataclass(frozen=True)
25+
class TaskFrameKey:
26+
project_id: str
27+
task_id: str
28+
input_data_id: str
29+
30+
31+
@dataclass(frozen=True)
32+
class ChoiceKey:
33+
additional_data_definition_id: str
34+
choice_id: str
35+
36+
37+
@dataclass(frozen=True)
38+
class AnnotationSpecsRelation:
39+
label_id: Dict[str, str]
40+
additional_data_definition_id: Dict[str, str]
41+
choice_id: Dict[ChoiceKey, ChoiceKey]
42+
43+
2144
class Wrapper:
2245
"""
2346
AnnofabApiのラッパー.
@@ -68,7 +91,7 @@ def _get_all_objects(func_get_list: Callable, limit: int, **kwargs_for_func_get_
6891
"""
6992
get_all_XXX関数の共通処理
7093
71-
Args:
94+
Args:
7295
func_get_list: AnnofabApiのget_XXX関数
7396
limit: 1ページあたりの取得するデータ件数
7497
**kwargs_for_func_get_list: `func_get_list`に渡す引数。
@@ -158,12 +181,143 @@ def get_all_annotation_list(self, project_id: str,
158181
project_id: プロジェクトID
159182
query_params: `api.get_annotation_list` メソッドのQuery Parameter
160183
161-
Returns:
184+
Returns:l
162185
すべてのアノテーション一覧
163186
"""
164187
return self._get_all_objects(self.api.get_annotation_list, limit=200, project_id=project_id,
165188
query_params=query_params)
166189

190+
@staticmethod
191+
def __create_annotation_id(detail: Dict[str, Any]) -> str:
192+
if detail["data_holding_type"] == AnnotationDataHoldingType.INNER.value and detail["data"] is None:
193+
# annotation_typeがclassificationのときは、label_idとannotation_idを一致させる必要がある。
194+
return detail["label_id"]
195+
else:
196+
return str(uuid.uuid4())
197+
198+
@staticmethod
199+
def __replace_annotation_specs_id(detail: Dict[str, Any],
200+
annotation_specs_relation: AnnotationSpecsRelation) -> Optional[Dict[str, Any]]:
201+
"""
202+
アノテーション仕様関係のIDを、新しいIDに置換する。
203+
204+
Args:
205+
detail: (IN/OUT) 1個のアノテーション詳細情報
206+
207+
Returns:
208+
IDを置換した後のアノテーション詳細情報.
209+
"""
210+
label_id = detail["label_id"]
211+
212+
new_label_id = annotation_specs_relation.label_id.get(label_id)
213+
if new_label_id is None:
214+
return None
215+
else:
216+
detail["label_id"] = new_label_id
217+
218+
additional_data_list = detail["additional_data_list"]
219+
new_additional_data_list = []
220+
for additional_data in additional_data_list:
221+
additional_data_definition_id = additional_data["additional_data_definition_id"]
222+
new_additional_data_definition_id = annotation_specs_relation.additional_data_definition_id.get(
223+
additional_data_definition_id)
224+
if new_additional_data_definition_id is None:
225+
continue
226+
additional_data["additional_data_definition_id"] = new_additional_data_definition_id
227+
228+
if additional_data["choice"] is not None:
229+
new_choice = annotation_specs_relation.choice_id.get(
230+
ChoiceKey(additional_data_definition_id, additional_data["choice"]))
231+
additional_data["choice"] = new_choice.choice_id if new_choice is not None else None
232+
233+
new_additional_data_list.append(additional_data)
234+
235+
detail["additional_data_list"] = new_additional_data_list
236+
return detail
237+
238+
def __to_dest_annotation_detail(
239+
self,
240+
dest_project_id: str,
241+
detail: Dict[str, Any],
242+
account_id: str,
243+
) -> Dict[str, Any]:
244+
"""
245+
コピー元の1個のアノテーションを、コピー先用に変換する。
246+
塗りつぶし画像の場合、S3にアップロードする。
247+
248+
Notes:
249+
annotation_id をUUIDv4で生成すると、アノテーションリンク属性をコピーしたときに対応できないので、暫定的にannotation_idは維持するようにする。
250+
"""
251+
dest_detail = detail
252+
dest_detail["account_id"] = account_id
253+
254+
if detail["data_holding_type"] == AnnotationDataHoldingType.OUTER.value:
255+
outer_file_url = detail["url"]
256+
src_response = self.api.session.get(outer_file_url)
257+
s3_path = self.upload_data_to_s3(dest_project_id, data=src_response.content,
258+
content_type=src_response.headers["Content-Type"])
259+
logger.debug("%s に塗りつぶし画像をアップロードしました。", s3_path)
260+
dest_detail["path"] = s3_path
261+
dest_detail["url"] = None
262+
dest_detail["etag"] = None
263+
264+
return dest_detail
265+
266+
def __create_request_body_for_copy_annotation(
267+
self, project_id: str, task_id: str, input_data_id: str, src_details: List[Dict[str, Any]], account_id: str,
268+
annotation_specs_relation: Optional[AnnotationSpecsRelation] = None) -> Dict[str, Any]:
269+
dest_details: List[Dict[str, Any]] = []
270+
271+
for src_detail in src_details:
272+
if annotation_specs_relation is not None:
273+
tmp_detail = self.__replace_annotation_specs_id(src_detail, annotation_specs_relation)
274+
if tmp_detail is None:
275+
continue
276+
src_detail = tmp_detail
277+
278+
dest_detail = self.__to_dest_annotation_detail(project_id, src_detail, account_id=account_id)
279+
dest_details.append(dest_detail)
280+
281+
request_body = {
282+
"project_id": project_id,
283+
"task_id": task_id,
284+
"input_data_id": input_data_id,
285+
"details": dest_details,
286+
}
287+
return request_body
288+
289+
def copy_annotation(self, src: TaskFrameKey, dest: TaskFrameKey, account_id: str,
290+
annotation_specs_relation: Optional[AnnotationSpecsRelation] = None) -> bool:
291+
"""
292+
アノテーションをコピーする。
293+
294+
Args:
295+
src: コピー元のTaskFrame情報
296+
dest: コピー先のTaskFrame情報
297+
account_id: アノテーションを登録するユーザのアカウントID
298+
annotation_specs_relation: アノテーション仕様間の紐付け情報
299+
300+
Returns:
301+
アノテーションのコピー実施したかどうか
302+
303+
"""
304+
src_annotation, _ = self.api.get_editor_annotation(src.project_id, src.task_id, src.input_data_id)
305+
src_annotation_details: List[Dict[str, Any]] = src_annotation["details"]
306+
307+
if len(src_annotation_details) == 0:
308+
logger.debug("コピー元にアノテーションが1つもないため、アノテーションのコピーをスキップします。")
309+
return False
310+
311+
old_dest_annotation, _ = self.api.get_editor_annotation(dest.project_id, dest.task_id, dest.input_data_id)
312+
updated_datetime = old_dest_annotation["updated_datetime"]
313+
314+
request_body = self.__create_request_body_for_copy_annotation(
315+
dest.project_id, dest.task_id, dest.input_data_id, src_details=src_annotation_details,
316+
account_id=account_id, annotation_specs_relation=annotation_specs_relation)
317+
request_body["updated_datetime"] = updated_datetime
318+
self.api.put_annotation(dest.project_id, dest.task_id, dest.input_data_id, request_body=request_body)
319+
return True
320+
167321
#########################################
168322
# Public Method : AnnotationSpecs
169323
#########################################
@@ -195,6 +349,78 @@ def copy_annotation_specs(self, src_project_id: str, dest_project_id: str,
195349
}
196350
return self.api.put_annotation_specs(dest_project_id, request_body=request_body)[0]
197351

352+
@staticmethod
353+
def __get_label_name_en(label: Dict[str, Any]) -> str:
354+
"""label情報から英語名を取得する"""
355+
label_name_messages = label["label_name"]["messages"]
356+
return [e["message"] for e in label_name_messages if e["lang"] == "en-US"][0]
357+
358+
@staticmethod
359+
def __get_additional_data_definition_name_en(additional_data_definition: Dict[str, Any]) -> str:
360+
"""additional_data_definitionから英語名を取得する"""
361+
messages = additional_data_definition["name"]["messages"]
362+
return [e["message"] for e in messages if e["lang"] == "en-US"][0]
363+
364+
@staticmethod
365+
def __get_choice_name_en(choice: Dict[str, Any]) -> str:
366+
"""choiceから英語名を取得する"""
367+
messages = choice["name"]["messages"]
368+
return [e["message"] for e in messages if e["lang"] == "en-US"][0]
369+
370+
def get_annotation_specs_relation(self, src_project_id: str, dest_project_id: str) -> AnnotationSpecsRelation:
371+
"""
372+
プロジェクト間のアノテーション仕様の紐付け情報を取得する。ラベル、属性、選択肢の英語名で紐付ける。
373+
紐付け先がない場合は無視する。
374+
``copy_annotation`` メソッドで利用する。
375+
376+
Args:
377+
src_project_id: 紐付け元のプロジェクトID
378+
dest_project_id: 紐付け先のプロジェクトID
379+
380+
Returns:
381+
アノテーション仕様の紐付け情報
382+
383+
"""
384+
src_annotation_specs, _ = self.api.get_annotation_specs(src_project_id, query_params={"v": "2"})
385+
dest_annotation_specs, _ = self.api.get_annotation_specs(dest_project_id, query_params={"v": "2"})
386+
dest_labels = dest_annotation_specs["labels"]
387+
dest_additionals = dest_annotation_specs["additionals"]
388+
389+
dict_label_id: Dict[str, str] = {}
390+
for src_label in src_annotation_specs["labels"]:
391+
src_label_name_en = self.__get_label_name_en(src_label)
392+
dest_label = first_true(dest_labels, pred=lambda e, f=src_label_name_en: self.__get_label_name_en(e) == f)
393+
if dest_label is not None:
394+
dict_label_id[src_label["label_id"]] = dest_label["label_id"]
395+
396+
dict_additional_data_definition_id: Dict[str, str] = {}
397+
dict_choice_id: Dict[ChoiceKey, ChoiceKey] = {}
398+
for src_additional in src_annotation_specs["additionals"]:
399+
src_additional_name_en = self.__get_additional_data_definition_name_en(src_additional)
400+
dest_additional = first_true(
401+
dest_additionals,
402+
pred=lambda e, f=src_additional_name_en: self.__get_additional_data_definition_name_en(e) == f)
403+
if dest_additional is None:
404+
continue
405+
406+
dict_additional_data_definition_id[
407+
src_additional["additional_data_definition_id"]] = dest_additional["additional_data_definition_id"]
408+
409+
dest_choices = dest_additional["choices"]
410+
for src_choice in src_additional["choices"]:
411+
src_choice_name_en = self.__get_choice_name_en(src_choice)
412+
dest_choice = first_true(dest_choices,
413+
pred=lambda e, f=src_choice_name_en: self.__get_choice_name_en(e) == f)
414+
if dest_choice is not None:
415+
dict_choice_id[ChoiceKey(src_additional["additional_data_definition_id"],
416+
src_choice["choice_id"])] = ChoiceKey(
417+
dest_additional["additional_data_definition_id"],
418+
dest_choice["choice_id"])
419+
420+
return AnnotationSpecsRelation(label_id=dict_label_id,
421+
additional_data_definition_id=dict_additional_data_definition_id,
422+
choice_id=dict_choice_id)
423+
198424
#########################################
199425
# Public Method : Input
200426
#########################################

0 commit comments

Comments
 (0)