4
4
import time
5
5
from functools import wraps
6
6
from json import JSONDecodeError
7
- from typing import Any , Callable , Collection , Dict , Optional , Tuple
7
+ from typing import Any , Callable , Collection , Dict , Optional , Tuple , Union
8
8
9
9
import backoff
10
10
import requests
11
11
from requests .auth import AuthBase
12
12
from requests .cookies import RequestsCookieJar
13
13
14
+ from annofabapi .credentials import IdPass , Pat , Tokens
14
15
from annofabapi .exceptions import InvalidMfaCodeError , MfaEnabledUserExecutionError , NotLoggedInError
15
16
from annofabapi .generated_api import AbstractAnnofabApi
17
+ from annofabapi .util .type_util import assert_noreturn
16
18
17
19
logger = logging .getLogger (__name__ )
18
20
@@ -234,34 +236,34 @@ class AnnofabApi(AbstractAnnofabApi):
234
236
Web APIに対応したメソッドが存在するクラス。
235
237
236
238
Args:
237
- login_user_id: AnnofabにログインするときのユーザID
238
- login_password: Annofabにログインするときのパスワード
239
+ credentials: Annofabにログインするときの認証情報
239
240
endpoint_url: Annofab APIのエンドポイント。
240
241
input_mfa_code_via_stdin: MFAコードを標準入力から入力するかどうか
242
+ Falseを渡して且つMFAコードの入力を求められるアカウントを利用する場合、mfa_codeを引数にloginメソッドを直接呼び出さなければならず、そうしない場合は例外を送出する
241
243
242
244
Attributes:
243
- token_dict : login, refresh_tokenで取得したtoken情報
245
+ tokens : login, refresh_tokenで取得したtoken情報
244
246
cookies: Signed Cookie情報
245
247
"""
246
248
247
- def __init__ (
248
- self , login_user_id : str , login_password : str , * , endpoint_url : str = DEFAULT_ENDPOINT_URL , input_mfa_code_via_stdin : bool = False
249
- ) -> None :
250
- if not login_user_id or not login_password :
249
+ def __init__ (self , credentials : Union [IdPass , Pat ], * , endpoint_url : str = DEFAULT_ENDPOINT_URL , input_mfa_code_via_stdin : bool = False ) -> None :
250
+ if isinstance (credentials , IdPass ) and (not credentials .user_id or not credentials .password ):
251
251
raise ValueError ("login_user_id or login_password is empty." )
252
+ if isinstance (credentials , Pat ) and not credentials .token :
253
+ raise ValueError ("pat is empty." )
252
254
253
- self .login_user_id = login_user_id
254
- self .login_password = login_password
255
+ self .credentials = credentials
255
256
self .endpoint_url = endpoint_url
256
257
self .input_mfa_code_via_stdin = input_mfa_code_via_stdin
257
258
self .url_prefix = f"{ endpoint_url } /api/v1"
258
259
self .session = requests .Session ()
259
260
260
- self .token_dict : Optional [ Dict [ str , Any ] ] = None
261
+ self .tokens : Union [ Tokens , Pat , None ] = None
261
262
262
263
self .cookies : Optional [RequestsCookieJar ] = None
263
264
264
265
self .__account_id : Optional [str ] = None
266
+ self .__user_id : Optional [str ] = None
265
267
266
268
class _MyToken (AuthBase ):
267
269
"""
@@ -328,8 +330,9 @@ def _create_kwargs(
328
330
"params" : new_params ,
329
331
"headers" : headers ,
330
332
}
331
- if self .token_dict is not None :
332
- kwargs .update ({"auth" : self ._MyToken (self .token_dict ["id_token" ])})
333
+ if self .tokens is not None :
334
+ token = self .tokens .auth_token
335
+ kwargs .update ({"auth" : self ._MyToken (token )})
333
336
334
337
if request_body is not None :
335
338
if isinstance (request_body , (dict , list )):
@@ -495,6 +498,12 @@ def _request_wrapper(
495
498
else :
496
499
url = f"{ self .url_prefix } { url_path } "
497
500
501
+ # patを使う場合は最初にtokensをセットする
502
+ # def logoutの呼び出しでtokensがNoneになった後にAPIを呼び出しても問題ないように(IdPassの場合も、自動loginしているので、その代わり)
503
+ # IdPassと同じ処理に合流させてしまうと、patが無効なときに無限ループしてしまうので、ここで1回だけ呼び出す
504
+ if self .tokens is None and isinstance (self .credentials , Pat ):
505
+ self ._login_pat (self .credentials )
506
+
498
507
kwargs = self ._create_kwargs (query_params , header_params , request_body )
499
508
response = self .session .request (method = http_method .lower (), url = url , ** kwargs )
500
509
# response.requestよりメソッド引数のrequest情報の方が分かりやすいので、メソッド引数のrequest情報を出力する。
@@ -512,8 +521,8 @@ def _request_wrapper(
512
521
},
513
522
)
514
523
515
- # Unauthorized Errorならば、ログイン後に再度実行する
516
- if response .status_code == requests .codes .unauthorized :
524
+ # ID/PASSが指定されており、 Unauthorized Errorならば、ログイン後に再度実行する
525
+ if isinstance ( self . credentials , IdPass ) and response .status_code == requests .codes .unauthorized :
517
526
self .refresh_token ()
518
527
return self ._request_wrapper (
519
528
http_method ,
@@ -616,7 +625,7 @@ def _request_get_with_cookie(self, project_id: str, url: str) -> requests.Respon
616
625
#########################################
617
626
# Public Method : Login
618
627
#########################################
619
- def _login_respond_to_auth_challenge (self , mfa_code : str , session : str ) -> Dict [str , Any ]:
628
+ def _login_respond_to_auth_challenge (self , id_pass : IdPass , mfa_code : str , session : str ) -> Dict [str , Any ]:
620
629
"""
621
630
MFAコードによるログインを実行します。
622
631
@@ -629,7 +638,7 @@ def _login_respond_to_auth_challenge(self, mfa_code: str, session: str) -> Dict[
629
638
Raises:
630
639
InvalidMfaCodeError: ``self.input_mfa_code_via_stdin`` が ``False`` AND ``mfa_code`` が正しくない場合
631
640
"""
632
- request_body = {"user_id" : self . login_user_id , "mfa_code" : mfa_code , "session" : session }
641
+ request_body = {"user_id" : id_pass . user_id , "mfa_code" : mfa_code , "session" : session }
633
642
url = f"{ self .url_prefix } /login-respond-to-auth-challenge"
634
643
635
644
response = self ._execute_http_request ("post" , url , json = request_body , raise_for_status = False )
@@ -645,7 +654,7 @@ def _login_respond_to_auth_challenge(self, mfa_code: str, session: str) -> Dict[
645
654
if self .input_mfa_code_via_stdin :
646
655
logger .info (new_error_message )
647
656
new_mfa_code = _read_mfa_code_from_stdin ()
648
- return self ._login_respond_to_auth_challenge (new_mfa_code , session )
657
+ return self ._login_respond_to_auth_challenge (id_pass , new_mfa_code , session )
649
658
else :
650
659
raise InvalidMfaCodeError (new_error_message )
651
660
@@ -671,7 +680,15 @@ def login(self, mfa_code: Optional[str] = None) -> None:
671
680
InvalidMfaCodeError: ``self.input_mfa_code_via_stdin`` が ``False`` AND ``mfa_code`` が正しくない場合
672
681
MfaEnabledUserExecutionError: ``self.input_mfa_code_via_stdin`` が ``False`` AND ``mfa_code`` が未指定の場合
673
682
"""
674
- login_info = {"user_id" : self .login_user_id , "password" : self .login_password }
683
+ if isinstance (self .credentials , IdPass ):
684
+ self ._login_id_pass (self .credentials , mfa_code )
685
+ elif isinstance (self .credentials , Pat ):
686
+ self ._login_pat (self .credentials )
687
+ else :
688
+ assert_noreturn (self .credentials )
689
+
690
+ def _login_id_pass (self , id_pass : IdPass , mfa_code : Optional [str ] = None ) -> None :
691
+ login_info = {"user_id" : id_pass .user_id , "password" : id_pass .password }
675
692
676
693
url = f"{ self .url_prefix } /login"
677
694
@@ -683,21 +700,24 @@ def login(self, mfa_code: Optional[str] = None) -> None:
683
700
if self .input_mfa_code_via_stdin :
684
701
mfa_code = _read_mfa_code_from_stdin ()
685
702
else :
686
- raise MfaEnabledUserExecutionError (self . login_user_id )
703
+ raise MfaEnabledUserExecutionError (id_pass . user_id )
687
704
688
- mfa_json_obj = self ._login_respond_to_auth_challenge (mfa_code , login_json_obj ["session" ])
705
+ mfa_json_obj = self ._login_respond_to_auth_challenge (id_pass , mfa_code , login_json_obj ["session" ])
689
706
token_dict = mfa_json_obj ["token" ]
690
707
else :
691
708
# `login` APIのレスポンスのスキーマがloginRespondToAuthChallengeのとき
692
709
token_dict = login_json_obj ["token" ]
693
710
694
- self .token_dict = token_dict
695
- logger .debug ("Logged in successfully. user_id = %s" , self .login_user_id )
711
+ self .tokens = Tokens .from_dict (token_dict )
712
+ logger .debug ("Logged in successfully. user_id = %s" , id_pass .user_id )
713
+
714
+ def _login_pat (self , pat : Pat ) -> None :
715
+ self .tokens = pat
696
716
697
717
def logout (self ) -> None :
698
718
"""
699
719
ログアウトします。
700
- ログアウト後は、インスタンス変数 ``token_dict `` をNoneにします。
720
+ ログアウト後は、インスタンス変数 ``tokens `` をNoneにします。
701
721
702
722
703
723
@@ -708,27 +728,33 @@ def logout(self) -> None:
708
728
NotLoggedInError: ログインしてない状態で関数を呼び出したときのエラー
709
729
"""
710
730
711
- if self .token_dict is None :
731
+ if self .tokens is None :
712
732
raise NotLoggedInError
733
+ if isinstance (self .tokens , Pat ):
734
+ self .tokens = None
735
+ return
713
736
714
- request_body = self .token_dict
737
+ request_body = self .tokens . to_dict ()
715
738
url = f"{ self .url_prefix } /logout"
716
739
self ._execute_http_request ("POST" , url , json = request_body )
717
- self .token_dict = None
740
+ self .tokens = None
718
741
719
742
def refresh_token (self ) -> None :
720
743
"""
721
744
トークンを再発行して、新しいトークン情報をインスタンスに保持します。
745
+ パーソナルアクセストークンでのアクセスをしている場合はrefreshを行いません。
722
746
ログインしていない場合やリフレッシュトークンの有効期限が切れている場合は、login APIを実行して新しいトークン情報をインスタンスに保持します。
723
747
724
748
"""
725
749
726
- if self .token_dict is None :
750
+ if self .tokens is None :
727
751
# 一度もログインしていないときは、login APIを実行して、トークン情報をインスタンスに保持する(login関数内でインスタンスに保持している)
728
752
self .login ()
729
753
return
754
+ if isinstance (self .tokens , Pat ):
755
+ return
730
756
731
- request_body = {"refresh_token" : self .token_dict [ " refresh_token" ] }
757
+ request_body = {"refresh_token" : self .tokens . refresh_token }
732
758
url = f"{ self .url_prefix } /refresh-token"
733
759
response = self ._execute_http_request ("POST" , url , json = request_body )
734
760
@@ -737,11 +763,12 @@ def refresh_token(self) -> None:
737
763
self .login ()
738
764
return
739
765
740
- self .token_dict = response .json ()
766
+ self .tokens = Tokens . from_dict ( response .json () )
741
767
742
768
#########################################
743
769
# Public Method : Other
744
770
#########################################
771
+
745
772
@property
746
773
def account_id (self ) -> str :
747
774
"""
@@ -754,3 +781,19 @@ def account_id(self) -> str:
754
781
account_id = content ["account_id" ]
755
782
self .__account_id = account_id
756
783
return account_id
784
+
785
+ @property
786
+ def login_user_id (self ) -> str :
787
+ """
788
+ Annofabにログインするユーザのuser_id
789
+ """
790
+ if self .__user_id is not None :
791
+ return self .__user_id
792
+ if isinstance (self .credentials , IdPass ):
793
+ self .__user_id = self .credentials .user_id
794
+ return self .__user_id
795
+ else :
796
+ content , _ = self .get_my_account ()
797
+ user_id = content ["user_id" ]
798
+ self .__user_id = user_id
799
+ return user_id
0 commit comments