11import re
22import uuid
3+ from collections .abc import Callable
34from datetime import datetime , timedelta
45from inspect import signature
5- from typing import Any , Callable , Optional , Union
6+ from typing import Any
67
78import requests
89
@@ -37,24 +38,24 @@ class WealthsimpleAPIBase:
3738 'FetchBrokerageMonthlyStatementTransactions' : "query FetchBrokerageMonthlyStatementTransactions($period: String!, $accountId: String!) {\n brokerageMonthlyStatements(period: $period, accountId: $accountId) {\n id\n statementType\n createdAt\n data {\n ... on BrokerageMonthlyStatementObject {\n ...BrokerageMonthlyStatementObject\n __typename\n }\n __typename\n }\n __typename\n }\n }\n \n fragment BrokerageMonthlyStatementObject on BrokerageMonthlyStatementObject {\n custodianAccountId\n activitiesPerCurrency {\n currency\n currentTransactions {\n ...BrokerageMonthlyStatementTransactions\n __typename\n }\n __typename\n }\n currentTransactions {\n ...BrokerageMonthlyStatementTransactions\n __typename\n }\n isMultiCurrency\n __typename\n }\n \n fragment BrokerageMonthlyStatementTransactions on BrokerageMonthlyStatementTransactions {\n balance\n cashMovement\n unit\n description\n transactionDate\n transactionType\n __typename\n }" ,
3839 }
3940
40- def __init__ (self , sess : Optional [ WSAPISession ] = None ):
41+ def __init__ (self , sess : WSAPISession | None = None ):
4142 self .security_market_data_cache_getter = None
4243 self .security_market_data_cache_setter = None
4344 self .session = WSAPISession ()
4445 self .start_session (sess )
4546
46- user_agent = None
47+ user_agent : str | None = None
4748
4849 @staticmethod
49- def set_user_agent (user_agent : str ):
50+ def set_user_agent (user_agent : str ) -> None :
5051 WealthsimpleAPI .user_agent = user_agent
5152
5253 @staticmethod
5354 def uuidv4 () -> str :
5455 return str (uuid .uuid4 ())
5556
5657 def send_http_request (
57- self , url : str , method : str = 'POST' , data : Optional [ dict ] = None , headers : Optional [ dict ] = None , return_headers : bool = False
58+ self , url : str , method : str = 'POST' , data : dict | None = None , headers : dict | None = None , return_headers : bool = False
5859 ) -> Any :
5960 headers = headers or {}
6061 if method == 'POST' :
@@ -77,20 +78,20 @@ def send_http_request(
7778
7879 if return_headers :
7980 # Combine headers and body as a single string
80- headers = '\r \n ' .join (f"{ k } : { v } " for k , v in response .headers .items ())
81- return f"{ headers } \r \n \r \n { response .text } "
81+ response_headers = '\r \n ' .join (f"{ k } : { v } " for k , v in response .headers .items ())
82+ return f"{ response_headers } \r \n \r \n { response .text } "
8283
8384 return response .json ()
8485 except requests .exceptions .RequestException as e :
8586 raise CurlException (f"HTTP request failed: { e } " )
8687
87- def send_get (self , url : str , headers : Optional [ dict ] = None , return_headers : bool = False ) -> Any :
88+ def send_get (self , url : str , headers : dict | None = None , return_headers : bool = False ) -> Any :
8889 return self .send_http_request (url , 'GET' , headers = headers , return_headers = return_headers )
8990
90- def send_post (self , url : str , data : dict , headers : Optional [ dict ] = None , return_headers : bool = False ) -> Any :
91+ def send_post (self , url : str , data : dict , headers : dict | None = None , return_headers : bool = False ) -> Any :
9192 return self .send_http_request (url , 'POST' , data = data , headers = headers , return_headers = return_headers )
9293
93- def start_session (self , sess : WSAPISession = None ):
94+ def start_session (self , sess : WSAPISession | None = None ):
9495 if sess :
9596 self .session .access_token = sess .access_token
9697 self .session .wssdi = sess .wssdi
@@ -138,16 +139,25 @@ def start_session(self, sess: WSAPISession = None):
138139 if not self .session .session_id :
139140 self .session .session_id = str (uuid .uuid4 ())
140141
141- def check_oauth_token (self , persist_session_fct : Optional [Callable ] = None , username = None ):
142+ def search_security (self , query ):
143+ # Fetch security search results using GraphQL query
144+ return self .do_graphql_query (
145+ 'FetchSecuritySearchResult' ,
146+ {'query' : query },
147+ 'securitySearch.results' ,
148+ 'array' ,
149+ )
150+
151+ def check_oauth_token (self , persist_session_fct : Callable | None = None , username = None ):
142152 if self .session .access_token :
143153 try :
144- # noinspection PyUnresolvedReferences
145154 self .search_security ('XEQT' )
146- return
147155 except WSApiException as e :
148- if e .response [ 'message' ] != 'Not Authorized.' :
149- raise e
156+ if e .response is None or e . response . get ( 'message' ) != 'Not Authorized.' :
157+ raise
150158 # Access token expired; try to refresh it below
159+ else :
160+ return
151161
152162 if self .session .refresh_token :
153163 data = {
@@ -161,7 +171,7 @@ def check_oauth_token(self, persist_session_fct: Optional[Callable] = None, user
161171 }
162172 response = self .send_post (f"{ self .OAUTH_BASE_URL } /token" , data , headers )
163173 if 'access_token' not in response or 'refresh_token' not in response :
164- raise ManualLoginRequired (f"OAuth token invalid and cannot be refreshed: { response [ 'error' ] if 'error' in response else ' Invalid response from API'} " )
174+ raise ManualLoginRequired (f"OAuth token invalid and cannot be refreshed: { response . get ( 'error' , ' Invalid response from API') } " )
165175 self .session .access_token = response ['access_token' ]
166176 self .session .refresh_token = response ['refresh_token' ]
167177 if persist_session_fct :
@@ -179,8 +189,8 @@ def check_oauth_token(self, persist_session_fct: Optional[Callable] = None, user
179189 def login_internal (self ,
180190 username : str ,
181191 password : str ,
182- otp_answer : Optional [ str ] = None ,
183- persist_session_fct : Optional [ Callable ] = None ,
192+ otp_answer : str | None = None ,
193+ persist_session_fct : Callable | None = None ,
184194 scope : str = SCOPE_READ_ONLY
185195 ) -> WSAPISession :
186196 data = {
@@ -228,7 +238,7 @@ def login_internal(self,
228238 return self .session
229239
230240 def do_graphql_query (self , query_name : str , variables : dict , data_response_path : str , expect_type : str ,
231- filter_fn : Optional [ Callable [[Any ], bool ]] = None , * , load_all_pages : bool = False ):
241+ filter_fn : Callable [[Any ], bool ] | None = None , * , load_all_pages : bool = False ):
232242 query = {
233243 'operationName' : query_name ,
234244 'query' : self .GRAPHQL_QUERIES [query_name ],
@@ -305,8 +315,8 @@ def get_token_info(self):
305315 def login (
306316 username : str ,
307317 password : str ,
308- otp_answer : Optional [ str ] = None ,
309- persist_session_fct : Optional [ Callable ] = None ,
318+ otp_answer : str | None = None ,
319+ persist_session_fct : Callable | None = None ,
310320 scope : str = SCOPE_READ_ONLY
311321 ) -> WSAPISession :
312322 """Login to Wealthsimple API and return a session object.
@@ -329,13 +339,13 @@ def login(
329339 return ws .login_internal (username , password , otp_answer , persist_session_fct , scope )
330340
331341 @staticmethod
332- def from_token (sess : WSAPISession , persist_session_fct : callable = None , username : Optional [ str ] = None ):
342+ def from_token (sess : WSAPISession , persist_session_fct : Callable | None = None , username : str | None = None ):
333343 ws = WealthsimpleAPI (sess )
334344 ws .check_oauth_token (persist_session_fct , username )
335345 return ws
336346
337347class WealthsimpleAPI (WealthsimpleAPIBase ):
338- def __init__ (self , sess : WSAPISession = None ):
348+ def __init__ (self , sess : WSAPISession | None = None ) -> None :
339349 super ().__init__ (sess )
340350 self .account_cache = {}
341351
@@ -417,7 +427,7 @@ def get_account_balances(self, account_id):
417427 for account in accounts [0 ]['custodianAccounts' ]:
418428 for balance in account ['financials' ]['balance' ]:
419429 security = balance ['securityId' ]
420- if security != 'sec-c-cad' and security != 'sec-c-usd' :
430+ if security not in { 'sec-c-cad' , 'sec-c-usd' } :
421431 security = self .security_id_to_symbol (security )
422432 balances [security ] = balance ['quantity' ]
423433
@@ -457,12 +467,12 @@ def get_identity_historical_financials(self, account_ids = None, currency: str =
457467
458468 def get_activities (
459469 self ,
460- account_id : Union [ str , list [str ] ],
470+ account_id : str | list [str ],
461471 how_many : int = 50 ,
462472 order_by : str = 'OCCURRED_AT_DESC' ,
463473 ignore_rejected : bool = True ,
464- start_date : Optional [ datetime ] = None ,
465- end_date : Optional [ datetime ] = None ,
474+ start_date : datetime | None = None ,
475+ end_date : datetime | None = None ,
466476 load_all : bool = False
467477 ) -> list [Any ]:
468478 """Retrieve activities for a specific account or list of accounts.
@@ -587,24 +597,30 @@ def _activity_add_description(self, act):
587597 type_ = act ['type' ].lower ().capitalize ()
588598 direction = 'from' if act ['type' ] == 'DEPOSIT' else 'to'
589599 prop = 'source' if act ['type' ] == 'DEPOSIT' else 'destination'
590- bank_account = details [prop ]['bankAccount' ]
591- nickname = bank_account .get ('nickname' )
600+ if isinstance (details , dict ):
601+ bank_account_info = details .get (prop , {})
602+ if isinstance (bank_account_info , dict ):
603+ bank_account = bank_account_info .get ('bankAccount' , {})
604+ nickname = bank_account .get ('nickname' )
605+ account_number = bank_account .get ('accountNumber' )
592606 if not nickname :
593- nickname = bank_account ['accountName' ]
594- act ['description' ] = (
595- f"{ type_ } : EFT { direction } { nickname } { bank_account ['accountNumber' ]} "
596- )
607+ nickname = bank_account .get ('accountName' )
608+ act ['description' ] = f"{ type_ } : EFT { direction } { nickname } { account_number } "
597609
598610 elif act ['type' ] == 'REFUND' and act ['subType' ] == 'TRANSFER_FEE_REFUND' :
599611 act ['description' ] = "Reimbursement: account transfer fee"
600612
601613 elif act ['type' ] == 'INSTITUTIONAL_TRANSFER_INTENT' and act ['subType' ] == 'TRANSFER_IN' :
602614 details = self .get_transfer_details (act ['externalCanonicalId' ])
603- verb = details ['transferType' ].replace ('_' , '-' ).capitalize ()
615+ if isinstance (details , dict ):
616+ verb = details ['transferType' ].replace ('_' , '-' ).capitalize ()
617+ client_account_type = details ['clientAccountType' ].upper ()
618+ institution_name = details ['institutionName' ]
619+ redacted_account_number = details ['redactedInstitutionAccountNumber' ]
604620 act ['description' ] = (
605- f"Institutional transfer: { verb } { details [ 'clientAccountType' ]. upper () } "
606- f"account transfer from { details [ 'institutionName' ] } "
607- f"****{ details [ 'redactedInstitutionAccountNumber' ] } "
621+ f"Institutional transfer: { verb } { client_account_type } "
622+ f"account transfer from { institution_name } "
623+ f"****{ redacted_account_number } "
608624 )
609625 elif act ['type' ] == 'INSTITUTIONAL_TRANSFER_INTENT' and act ['subType' ] == 'TRANSFER_OUT' :
610626 act ['description' ] = (
@@ -646,8 +662,8 @@ def _activity_add_description(self, act):
646662
647663 elif act ['type' ] == 'P2P_PAYMENT' and act ['subType' ] in ('SEND' , 'SEND_RECEIVED' ):
648664 direction = 'sent to' if act ['subType' ] == 'SEND' else 'received from'
649- p2pHandle = act ['p2pHandle' ]
650- act ['description' ] = f"Cash { direction } { p2pHandle } "
665+ p2p_handle = act ['p2pHandle' ]
666+ act ['description' ] = f"Cash { direction } { p2p_handle } "
651667
652668 elif act ['type' ] == 'PROMOTION' and act ['subType' ] == 'INCENTIVE_BONUS' :
653669 type_ = act ['type' ].capitalize ()
@@ -685,7 +701,7 @@ def security_id_to_symbol(self, security_id: str) -> str:
685701 security_symbol = f"[{ security_id } ]"
686702 if self .security_market_data_cache_getter :
687703 market_data = self .get_security_market_data (security_id )
688- if market_data and 'stock' in market_data and market_data ['stock' ]:
704+ if isinstance ( market_data , dict ) and market_data ['stock' ]:
689705 stock = market_data ['stock' ]
690706 security_symbol = f"{ stock ['primaryExchange' ]} :{ stock ['symbol' ]} "
691707 return security_symbol
@@ -706,7 +722,7 @@ def get_transfer_details(self, transfer_id):
706722 'object' ,
707723 )
708724
709- def set_security_market_data_cache (self , security_market_data_cache_getter : callable , security_market_data_cache_setter : callable ) :
725+ def set_security_market_data_cache (self , security_market_data_cache_getter : Callable , security_market_data_cache_setter : Callable ) -> None :
710726 self .security_market_data_cache_getter = security_market_data_cache_getter
711727 self .security_market_data_cache_setter = security_market_data_cache_setter
712728
@@ -731,14 +747,7 @@ def get_security_market_data(self, security_id: str, use_cache: bool = True):
731747
732748 return value
733749
734- def search_security (self , query ):
735- # Fetch security search results using GraphQL query
736- return self .do_graphql_query (
737- 'FetchSecuritySearchResult' ,
738- {'query' : query },
739- 'securitySearch.results' ,
740- 'array' ,
741- )
750+
742751
743752 def get_security_historical_quotes (self , security_id , time_range = '1m' ):
744753 # Fetch historical quotes for a security using GraphQL query
@@ -765,7 +774,7 @@ def get_corporate_action_child_activities(self, activity_canonical_id):
765774
766775 def get_statement_transactions (self , account_id : str , period : str ) -> list [Any ]:
767776 """Retrieve transactions from account monthly statement.
768-
777+
769778 Args:
770779 account_id (str): The account ID to retrieve transactions for.
771780 period (str): The statement start date in 'YYYY-MM-DD' format.
@@ -795,5 +804,5 @@ def get_statement_transactions(self, account_id: str, period: str) -> list[Any]:
795804
796805 if not isinstance (transactions , list ):
797806 raise WSApiException (f"Unexpected response format: { self .get_statement_transactions .__name__ } " , transactions )
798-
807+
799808 return transactions
0 commit comments