1+ import re
2+ import uuid
13from datetime import datetime , timedelta
4+ from inspect import signature
5+ from typing import Any , Callable , Optional
26
3- import re
47import requests
5- import uuid
6- from typing import Optional , Callable , Any
78
8- from ws_api .exceptions import CurlException , LoginFailedException , ManualLoginRequired , OTPRequiredException , UnexpectedException , WSApiException
9+ from ws_api .exceptions import (
10+ CurlException ,
11+ LoginFailedException ,
12+ ManualLoginRequired ,
13+ OTPRequiredException ,
14+ UnexpectedException ,
15+ WSApiException ,
16+ )
917from ws_api .session import WSAPISession
10- from inspect import signature
1118
1219
1320class WealthsimpleAPIBase :
@@ -130,7 +137,7 @@ def start_session(self, sess: WSAPISession = None):
130137 if not self .session .session_id :
131138 self .session .session_id = str (uuid .uuid4 ())
132139
133- def check_oauth_token (self , persist_session_fct : Optional [Callable [[ WSAPISession , Optional [ str ]], None ] ] = None , username = None ):
140+ def check_oauth_token (self , persist_session_fct : Optional [Callable ] = None , username = None ):
134141 if self .session .access_token :
135142 try :
136143 # noinspection PyUnresolvedReferences
@@ -152,7 +159,7 @@ def check_oauth_token(self, persist_session_fct: Optional[Callable[[WSAPISession
152159 'x-ws-profile' : 'invest'
153160 }
154161 response = self .send_post (f"{ self .OAUTH_BASE_URL } /token" , data , headers )
155- if not 'access_token' in response or not 'refresh_token' in response :
162+ if 'access_token' not in response or 'refresh_token' not in response :
156163 raise ManualLoginRequired (f"OAuth token invalid and cannot be refreshed: { response ['error' ] if 'error' in response else 'Invalid response from API' } " )
157164 self .session .access_token = response ['access_token' ]
158165 self .session .refresh_token = response ['refresh_token' ]
@@ -168,8 +175,13 @@ def check_oauth_token(self, persist_session_fct: Optional[Callable[[WSAPISession
168175 SCOPE_READ_ONLY = 'invest.read trade.read tax.read'
169176 SCOPE_READ_WRITE = 'invest.read trade.read tax.read invest.write trade.write tax.write'
170177
171- def login_internal (self , username : str , password : str , otp_answer : str = None ,
172- persist_session_fct : callable = None , scope : str = SCOPE_READ_ONLY ) -> WSAPISession :
178+ def login_internal (self ,
179+ username : str ,
180+ password : str ,
181+ otp_answer : str | None = None ,
182+ persist_session_fct : Optional [Callable ] = None ,
183+ scope : str = SCOPE_READ_ONLY
184+ ) -> WSAPISession :
173185 data = {
174186 'grant_type' : 'password' ,
175187 'username' : username ,
@@ -215,7 +227,7 @@ def login_internal(self, username: str, password: str, otp_answer: str = None,
215227 return self .session
216228
217229 def do_graphql_query (self , query_name : str , variables : dict , data_response_path : str , expect_type : str ,
218- filter_fn : callable = None , * , load_all_pages : bool = False ):
230+ filter_fn : Optional [ Callable [[ Any ], bool ]] = None , * , load_all_pages : bool = False ):
219231 query = {
220232 'operationName' : query_name ,
221233 'query' : self .GRAPHQL_QUERIES [query_name ],
@@ -289,7 +301,29 @@ def get_token_info(self):
289301 return self .session .token_info
290302
291303 @staticmethod
292- def login (username : str , password : str , otp_answer : str = None , persist_session_fct : callable = None , scope : str = SCOPE_READ_ONLY ):
304+ def login (
305+ username : str ,
306+ password : str ,
307+ otp_answer : str | None = None ,
308+ persist_session_fct : Optional [Callable ] = None ,
309+ scope : str = SCOPE_READ_ONLY
310+ ) -> WSAPISession :
311+ """Login to Wealthsimple API and return a session object.
312+
313+ Args:
314+ username (str): The username of the Wealthsimple account.
315+ password (str): The password of the Wealthsimple account.
316+ otp_answer (str, optional): The answer to the 2FA code. Defaults to None.
317+ persist_session_fct (callable, optional): A function to call to persist the session. Defaults to None.
318+ scope (str, optional): The OAuth scope for the session. Defaults to SCOPE_READ_ONLY.
319+
320+ Returns:
321+ WSAPISession: The session object.
322+
323+ Raises:
324+ LoginFailedException: If the login fails.
325+ OTPRequiredException: If 2FA code is required.
326+ """
293327 ws = WealthsimpleAPI ()
294328 return ws .login_internal (username , password , otp_answer , persist_session_fct , scope )
295329
@@ -420,7 +454,35 @@ def get_identity_historical_financials(self, account_ids = None, currency: str =
420454 'array' ,
421455 )
422456
423- def get_activities (self , account_id , how_many = 50 , order_by = 'OCCURRED_AT_DESC' , ignore_rejected = True , start_date = None , end_date = None , load_all = False ):
457+ def get_activities (
458+ self ,
459+ account_id : str | list [str ],
460+ how_many : int = 50 ,
461+ order_by : str = 'OCCURRED_AT_DESC' ,
462+ ignore_rejected : bool = True ,
463+ start_date : datetime | None = None ,
464+ end_date : datetime | None = None ,
465+ load_all : bool = False
466+ ) -> list [Any ]:
467+ """Retrieve activities for a specific account or list of accounts.
468+
469+ Args:
470+ account_id (str | list[str]): The account ID or list of account IDs to retrieve activities for.
471+ how_many (int): The maximum number of activities to retrieve.
472+ order_by (str): The order in which to sort the activities.
473+ ignore_rejected (bool): Whether to ignore rejected or cancelled activities.
474+ start_date (datetime | None): The start date for filtering activities.
475+ end_date (datetime | None): The end date for filtering activities.
476+ load_all (bool): Whether to load all pages of activities.
477+
478+ Returns:
479+ list[Any]: A list of activity objects.
480+
481+ Raises:
482+ WSApiException: If the response format is unexpected.
483+ """
484+ if isinstance (account_id , str ):
485+ account_id = [account_id ]
424486 # Calculate the end date for the condition
425487 end_date = (end_date if end_date else datetime .now () + timedelta (hours = 23 , minutes = 59 , seconds = 59 , milliseconds = 999 ))
426488
@@ -438,15 +500,17 @@ def filter_fn(activity):
438500 'condition' : {
439501 'startDate' : start_date .strftime ('%Y-%m-%dT%H:%M:%S.%fZ' ) if start_date else None ,
440502 'endDate' : end_date .strftime ('%Y-%m-%dT%H:%M:%S.%fZ' ),
441- 'accountIds' : [ account_id ] ,
503+ 'accountIds' : account_id ,
442504 },
443505 },
444506 'activityFeedItems.edges' ,
445507 'array' ,
446- filter_fn = filter_fn ,
447- load_all_pages = load_all ,
508+ filter_fn = filter_fn ,
509+ load_all_pages = load_all ,
448510 )
449511
512+ if not isinstance (activities , list ):
513+ raise WSApiException (f"Unexpected response format: { self .get_activities .__name__ } " , activities )
450514 for act in activities :
451515 self ._activity_add_description (act )
452516
@@ -528,7 +592,10 @@ def _activity_add_description(self, act):
528592 f"account transfer from { details ['institutionName' ]} "
529593 f"****{ details ['redactedInstitutionAccountNumber' ]} "
530594 )
531-
595+ elif act ['type' ] == 'INSTITUTIONAL_TRANSFER_INTENT' and act ['subType' ] == 'TRANSFER_OUT' :
596+ act ['description' ] = (
597+ f"Institutional transfer: transfer to { act ['institutionName' ]} "
598+ )
532599 elif act ['type' ] == 'INTEREST' :
533600 if act ['subType' ] == 'FPL_INTEREST' :
534601 act ['description' ] = "Stock Lending Earnings"
@@ -666,7 +733,7 @@ def get_security_historical_quotes(self, security_id, time_range='1m'):
666733 'security.historicalQuotes' ,
667734 'array' ,
668735 )
669-
736+
670737 def get_corporate_action_child_activities (self , activity_canonical_id ):
671738 # Fetch details about a corporate action (eg. a split) using GraphQL query
672739 return self .do_graphql_query (
0 commit comments