1111
1212__version__ = '0.1.4.dev0' # Should be the same in setup.py
1313
14- _URL_GET_ACCOUNTS = "https://api.revolut.com/user/current/wallet"
15- _URL_GET_TRANSACTIONS = 'https://api.revolut.com/user/current/transactions'
16- _URL_QUOTE = "https://api.revolut.com/quote/"
17- _URL_EXCHANGE = "https://api.revolut.com/exchange"
18- _URL_GET_TOKEN_STEP1 = "https://api.revolut.com/signin"
19- _URL_GET_TOKEN_STEP2 = "https://api.revolut.com/signin/confirm"
14+ API_BASE = "https://api.revolut.com"
15+ _URL_GET_ACCOUNTS = API_BASE + "/user/current/wallet"
16+ _URL_GET_TRANSACTIONS_LAST = API_BASE + "/user/current/transactions/last"
17+ _URL_QUOTE = API_BASE + "/quote/"
18+ _URL_EXCHANGE = API_BASE + "/exchange"
19+ _URL_GET_TOKEN_STEP1 = API_BASE + "/signin"
20+ _URL_GET_TOKEN_STEP2 = API_BASE + "/signin/confirm"
2021
2122_DEFAULT_TOKEN_FOR_SIGNIN = "QXBwOlM5V1VuU0ZCeTY3Z1dhbjc="
2223
@@ -132,7 +133,7 @@ class Client:
132133 """ Do the requests with the Revolut servers """
133134 def __init__ (self , token , device_id ):
134135 self .session = requests .session ()
135- self .headers = {
136+ self .session . headers = {
136137 'Host' : 'api.revolut.com' ,
137138 'X-Api-Version' : '1' ,
138139 'X-Client-Version' : '6.34.3' ,
@@ -141,22 +142,20 @@ def __init__(self, token, device_id):
141142 'Authorization' : 'Basic ' + token ,
142143 }
143144
144- def _get (self , url , expected_status_code = 200 ):
145- ret = self .session .get (url = url , headers = self . headers )
145+ def _get (self , url , * , expected_status_code = 200 , ** kwargs ):
146+ ret = self .session .get (url = url , ** kwargs )
146147 if ret .status_code != expected_status_code :
147148 raise ConnectionError (
148- 'Status code {status } for url {url }\n {content }' .format (
149- status = ret .status_code , url = url , content = ret .text ))
149+ 'Status code {} for url {}\n {}' .format (
150+ ret .status_code , url , ret .text ))
150151 return ret
151152
152- def _post (self , url , post_data , expected_status_code = 200 ):
153- ret = self .session .post (url = url ,
154- headers = self .headers ,
155- json = post_data )
153+ def _post (self , url , * , expected_status_code = 200 , ** kwargs ):
154+ ret = self .session .post (url = url , ** kwargs )
156155 if ret .status_code != expected_status_code :
157156 raise ConnectionError (
158- 'Status code {status } for url {url }\n {content }' .format (
159- status = ret .status_code , url = url , content = ret .text ))
157+ 'Status code {} for url {}\n {}' .format (
158+ ret .status_code , url , ret .text ))
160159 return ret
161160
162161
@@ -168,7 +167,7 @@ def get_account_balances(self):
168167 """ Get the account balance for each currency
169168 and returns it as a dict {"balance":XXXX, "currency":XXXX} """
170169 ret = self .client ._get (_URL_GET_ACCOUNTS )
171- raw_accounts = json . loads ( ret .text )
170+ raw_accounts = ret .json ( )
172171
173172 account_balances = []
174173 for raw_account in raw_accounts .get ("pockets" ):
@@ -183,22 +182,29 @@ def get_account_balances(self):
183182 self .account_balances = Accounts (account_balances )
184183 return self .account_balances
185184
186- def get_account_transactions (self , from_date ):
187- """ Get the account transactions and return as json """
188- from_date_ts = from_date .timestamp ()
189- path = _URL_GET_TRANSACTIONS + '?from={from_date_ts}&walletId={wallet_id}' .format (
190- from_date_ts = int (from_date_ts ) * 1000 ,
191- wallet_id = self .get_wallet_id ()
192- )
193- ret = self .client ._get (path )
194- raw_transactions = json .loads (ret .text )
195- transactions = AccountTransactions (raw_transactions )
196- return transactions
185+ def get_account_transactions (self , from_date = None , to_date = None ):
186+ """Get the account transactions."""
187+ raw_transactions = []
188+ params = {}
189+ if to_date :
190+ params ['to' ] = int (to_date .timestamp ()) * 1000
191+ if from_date :
192+ params ['from' ] = int (from_date .timestamp ()) * 1000
193+
194+ while True :
195+ ret = self .client ._get (_URL_GET_TRANSACTIONS_LAST , params = params )
196+ ret_transactions = ret .json ()
197+ if not ret_transactions :
198+ break
199+ params ['to' ] = ret_transactions [- 1 ]['startedDate' ]
200+ raw_transactions .extend (ret_transactions )
201+
202+ return AccountTransactions (raw_transactions )
197203
198204 def get_wallet_id (self ):
199205 """ Get the main wallet_id """
200206 ret = self .client ._get (_URL_GET_ACCOUNTS )
201- raw = json . loads ( ret .text )
207+ raw = ret .json ( )
202208 return raw .get ('id' )
203209
204210 def quote (self , from_amount , to_currency ):
@@ -213,7 +219,7 @@ def quote(self, from_amount, to_currency):
213219 to_currency ,
214220 from_amount .revolut_amount ))
215221 ret = self .client ._get (url_quote )
216- raw_quote = json . loads ( ret .text )
222+ raw_quote = ret .json ( )
217223 quote_obj = Amount (revolut_amount = raw_quote ["to" ]["amount" ],
218224 currency = to_currency )
219225 return quote_obj
@@ -257,8 +263,8 @@ def exchange(self, from_amount, to_currency, simulate=False):
257263 "updatedDate":123456789}]'
258264 raw_exchange = json .loads (simu )
259265 else :
260- ret = self .client ._post (url = _URL_EXCHANGE , post_data = data )
261- raw_exchange = json . loads ( ret .text )
266+ ret = self .client ._post (_URL_EXCHANGE , json = data )
267+ raw_exchange = ret .json ( )
262268
263269 if raw_exchange [0 ]["state" ] == "COMPLETED" :
264270 amount = raw_exchange [0 ]["counterpart" ]["amount" ]
@@ -462,15 +468,12 @@ def csv(self, lang="fr", reverse=False):
462468
463469def get_token_step1 (device_id , phone , password , simulate = False ):
464470 """ Function to obtain a Revolut token (step 1 : send a code by sms/email) """
465- if not simulate :
466- c = Client (device_id = device_id , token = _DEFAULT_TOKEN_FOR_SIGNIN )
467- data = {"phone" : phone , "password" : password }
468- ret = c ._post (url = _URL_GET_TOKEN_STEP1 ,
469- post_data = data ,
470- expected_status_code = 200 )
471- channel = ret .json ().get ('channel' )
472- else :
473- channel = "SMS"
471+ if simulate :
472+ return "SMS"
473+ c = Client (device_id = device_id , token = _DEFAULT_TOKEN_FOR_SIGNIN )
474+ data = {"phone" : phone , "password" : password }
475+ ret = c ._post (_URL_GET_TOKEN_STEP1 , json = data )
476+ channel = ret .json ().get ("channel" )
474477 return channel
475478
476479
@@ -497,18 +500,25 @@ def get_token_step2(device_id, phone, code, simulate=False):
497500 c = Client (device_id = device_id , token = _DEFAULT_TOKEN_FOR_SIGNIN )
498501 code = code .replace ("-" , "" ) # If the user would put -
499502 data = {"phone" : phone , "code" : code }
500- ret = c ._post (url = _URL_GET_TOKEN_STEP2 , post_data = data )
503+ ret = c ._post (_URL_GET_TOKEN_STEP2 , json = data )
501504 raw_get_token = ret .json ()
505+ return raw_get_token
502506
503- if raw_get_token .get ("thirdFactorAuthAccessToken" ):
504- raise KeyError (
505- "Token generation with a third factor authentication (selfie) "
506- "is not currently supported by this package"
507- )
508507
509- user_id = raw_get_token ["user" ]["id" ]
510- access_token = raw_get_token ["accessToken" ]
511- token_to_encode = '{}:{}' .format (user_id , access_token ).encode ('ascii' )
508+ def extract_token (json_response ):
509+ user_id = json_response ["user" ]["id" ]
510+ access_token = json_response ["accessToken" ]
511+ token_to_encode = "{}:{}" .format (user_id , access_token ).encode ("ascii" )
512512 # Ascii encoding required by b64encode function : 8 bits char as input
513513 token = base64 .b64encode (token_to_encode )
514- return token .decode ('ascii' )
514+ return token .decode ("ascii" )
515+
516+
517+ def signin_biometric (device_id , phone , access_token , selfie_filepath ):
518+ files = {"selfie" : open (selfie_filepath , "rb" )}
519+ c = Client (device_id = device_id , token = _DEFAULT_TOKEN_FOR_SIGNIN )
520+ c .session .auth = (phone , access_token )
521+ res = c ._post (API_BASE + "/biometric-signin/selfie" , files = files )
522+ biometric_id = res .json ()["id" ]
523+ res = c ._post (API_BASE + "/biometric-signin/confirm/" + biometric_id )
524+ return res .json ()
0 commit comments