From e4d1fc7ed22770b8aa9dbffaee3417de75aedc2b Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Tue, 12 Mar 2024 21:21:10 +0100 Subject: [PATCH 1/9] Updated API with timeline_transactions, timeline_activity_log and timeline_detail_v2 subscriptions. --- pytr/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pytr/api.py b/pytr/api.py index bbd9da1..a08d1ed 100644 --- a/pytr/api.py +++ b/pytr/api.py @@ -457,6 +457,15 @@ async def timeline_detail_order(self, order_id): async def timeline_detail_savings_plan(self, savings_plan_id): return await self.subscribe({'type': 'timelineDetail', 'savingsPlanId': savings_plan_id}) + async def timeline_transactions(self, after=None): + return await self.subscribe({'type': 'timelineTransactions', 'after': after}) + + async def timeline_activity_log(self, after=None): + return await self.subscribe({'type': 'timelineActivityLog', 'after': after}) + + async def timeline_detail_v2(self, timeline_id): + return await self.subscribe({'type': 'timelineDetailV2', 'id': timeline_id}) + async def search_tags(self): return await self.subscribe({'type': 'neonSearchTags'}) From 09f3ae31b7ee1132fd421dfc447aba99275d3ada Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Tue, 12 Mar 2024 22:44:37 +0100 Subject: [PATCH 2/9] Added download of timeline_transactions and timeline_activity_log instead of timeline subscriptions and timeline_detail_v2 instead of timeline_detail subscriptions. Adapted data extraction to new V2 format. --- pytr/dl.py | 10 +++-- pytr/utils.py | 112 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 80 insertions(+), 42 deletions(-) diff --git a/pytr/dl.py b/pytr/dl.py index f9de58d..eea2315 100644 --- a/pytr/dl.py +++ b/pytr/dl.py @@ -66,7 +66,7 @@ def load_history(self): self.log.info('Created history file') async def dl_loop(self): - await self.tl.get_next_timeline(max_age_timestamp=self.since_timestamp) + await self.tl.get_next_timeline_transactions(max_age_timestamp=self.since_timestamp) while True: try: @@ -74,9 +74,11 @@ async def dl_loop(self): except TradeRepublicError as e: self.log.fatal(str(e)) - if subscription['type'] == 'timeline': - await self.tl.get_next_timeline(response, max_age_timestamp=self.since_timestamp) - elif subscription['type'] == 'timelineDetail': + if subscription['type'] == 'timelineTransactions': + await self.tl.get_next_timeline_transactions(response, max_age_timestamp=self.since_timestamp) + elif subscription['type'] == 'timelineActivityLog': + await self.tl.get_next_timeline_activity_log(response, max_age_timestamp=self.since_timestamp) + elif subscription['type'] == 'timelineDetailV2': await self.tl.timelineDetail(response, self, max_age_timestamp=self.since_timestamp) else: self.log.warning(f"unmatched subscription of type '{subscription['type']}':\n{preview(response)}") diff --git a/pytr/utils.py b/pytr/utils.py index 2851a9f..a190074 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -184,8 +184,7 @@ def export_transactions(input_path, output_path, lang='auto'): f.write(header) for event in timeline: - event = event['data'] - dateTime = datetime.fromtimestamp(int(event['timestamp'] / 1000)) + dateTime = datetime.fromisoformat(event['timestamp']) date = dateTime.strftime('%Y-%m-%d') title = event['title'] @@ -219,60 +218,95 @@ def __init__(self, tr): self.num_timeline_details = 0 self.events_without_docs = [] self.events_with_docs = [] + self.num_timelines = 0 + self.timeline_events = {} + self.timeline_events_iter = None - async def get_next_timeline(self, response=None, max_age_timestamp=0): + async def get_next_timeline_transactions(self, response=None, max_age_timestamp=0): ''' - Get timelines and save time in list timelines. - Extract timeline events and save them in list timeline_events + Get timelines transactions and save time in list timelines. + Extract timeline transactions events and save them in list timeline_events ''' if response is None: # empty response / first timeline - self.log.info('Awaiting #1 timeline') - # self.timelines = [] + self.log.info('Awaiting #1 timeline transactions') self.num_timelines = 0 - self.timeline_events = [] - await self.tr.timeline() + await self.tr.timeline_transactions() else: - timestamp = response['data'][-1]['data']['timestamp'] + timestamp = response['items'][-1]['timestamp'] self.num_timelines += 1 # print(json.dumps(response)) - self.num_timeline_details += len(response['data']) - for event in response['data']: - self.timeline_events.append(event) + self.num_timeline_details += len(response['items']) + for event in response['items']: + self.timeline_events[event['id']] = event after = response['cursors'].get('after') if after is None: # last timeline is reached - self.log.info(f'Received #{self.num_timelines:<2} (last) timeline') + await self.get_next_timeline_activity_log() + else: + self.log.info( + f'Received #{self.num_timelines:<2} timeline transactions, awaiting #{self.num_timelines+1:<2} timeline transactions' + ) + await self.tr.timeline_transactions(after) + + + async def get_next_timeline_activity_log(self, response=None, max_age_timestamp=0): + ''' + Get timelines acvtivity log and save time in list timelines. + Extract timeline acvtivity log events and save them in list timeline_events + + ''' + + if response is None: + # empty response / first timeline + self.log.info('Awaiting #1 timeline activity log') + self.num_timelines = 0 + await self.tr.timeline_activity_log() + else: + timestamp = response['items'][-1]['timestamp'] + self.num_timelines += 1 + # print(json.dumps(response)) + self.num_timeline_details += len(response['items']) + for event in response['items']: + if event['id'] not in self.timeline_events: + self.timeline_events[event['id']] = event + + after = response['cursors'].get('after') + if after is None: + # last timeline is reached + self.log.info(f'Received #{self.num_timelines:<2} (last) timeline activity log') + self.timeline_events_iter = iter(self.timeline_events.values()) await self._get_timeline_details(5) elif max_age_timestamp != 0 and timestamp < max_age_timestamp: - self.log.info(f'Received #{self.num_timelines+1:<2} timeline') - self.log.info('Reached last relevant timeline') + self.log.info(f'Received #{self.num_timelines+1:<2} timeline activity log') + self.log.info('Reached last relevant timeline activity log') + self.timeline_events_iter = iter(self.timeline_events.values()) await self._get_timeline_details(5, max_age_timestamp=max_age_timestamp) else: self.log.info( - f'Received #{self.num_timelines:<2} timeline, awaiting #{self.num_timelines+1:<2} timeline' + f'Received #{self.num_timelines:<2} timeline activity log, awaiting #{self.num_timelines+1:<2} timeline activity log' ) - await self.tr.timeline(after) + await self.tr.timeline_activity_log(after) async def _get_timeline_details(self, num_torequest, max_age_timestamp=0): ''' request timeline details ''' while num_torequest > 0: - if len(self.timeline_events) == 0: + + try: + event = next(self.timeline_events_iter) + except StopIteration: self.log.info('All timeline details requested') return False - else: - event = self.timeline_events.pop() - - action = event['data'].get('action') - # icon = event['data'].get('icon') + action = event.get('action') + # icon = event.get('icon') msg = '' - if max_age_timestamp != 0 and event['data']['timestamp'] > max_age_timestamp: + if max_age_timestamp != 0 and event['timestamp'] > max_age_timestamp: msg += 'Skip: too old' # elif icon is None: # pass @@ -284,24 +318,24 @@ async def _get_timeline_details(self, num_torequest, max_age_timestamp=0): # msg += 'Skip: ExemptionOrderChanged' elif action is None: - if event['data'].get('actionLabel') is None: + if event.get('actionLabel') is None: msg += 'Skip: no action' elif action.get('type') != 'timelineDetail': msg += f"Skip: action type unmatched ({action['type']})" - elif action.get('payload') != event['data']['id']: + elif action.get('payload') != event['id']: msg += f"Skip: payload unmatched ({action['payload']})" if msg == '': self.events_with_docs.append(event) else: self.events_without_docs.append(event) - self.log.debug(f"{msg} {event['data']['title']}: {event['data'].get('body')} {json.dumps(event)}") + self.log.debug(f"{msg} {event['title']}: {event.get('body')} {json.dumps(event)}") self.num_timeline_details -= 1 continue num_torequest -= 1 self.requested_detail += 1 - await self.tr.timeline_detail(event['data']['id']) + await self.tr.timeline_detail_v2(event['id']) async def timelineDetail(self, response, dl, max_age_timestamp=0): ''' @@ -309,6 +343,8 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): ''' self.received_detail += 1 + event = self.timeline_events[response['id']] + event['details'] = response # when all requested timeline events are received request 5 new if self.received_detail == self.requested_detail: @@ -320,10 +356,10 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): # print(f'len timeline_events: {len(self.timeline_events)}') isSavingsPlan = False - if response['subtitleText'] == 'Sparplan': + if event['subtitle'] == 'Sparplan': isSavingsPlan = True else: - # some savingsPlan don't have the subtitleText == 'Sparplan' but there are actions just for savingsPans + # some savingsPlan don't have the subtitle == 'Sparplan' but there are actions just for savingsPans # but maybe these are unneeded duplicates for section in response['sections']: if section['type'] == 'actionButtons': @@ -332,7 +368,7 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): isSavingsPlan = True break - if response['subtitleText'] != 'Sparplan' and isSavingsPlan is True: + if event['subtitle'] != 'Sparplan' and isSavingsPlan is True: isSavingsPlan_fmt = ' -- SPARPLAN' else: isSavingsPlan_fmt = '' @@ -340,12 +376,12 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): max_details_digits = len(str(self.num_timeline_details)) self.log.info( f"{self.received_detail:>{max_details_digits}}/{self.num_timeline_details}: " - + f"{response['titleText']} -- {response['subtitleText']}{isSavingsPlan_fmt}" + + f"{event['title']} -- {event['subtitle']}{isSavingsPlan_fmt}" ) for section in response['sections']: if section['type'] == 'documents': - for doc in section['documents']: + for doc in section['data']: try: timestamp = datetime.strptime(doc['detail'], '%d.%m.%Y').timestamp() * 1000 except ValueError: @@ -353,14 +389,14 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): if max_age_timestamp == 0 or max_age_timestamp < timestamp: # save all savingsplan documents in a subdirectory if isSavingsPlan: - dl.dl_doc(doc, response['titleText'], response['subtitleText'], subfolder='Sparplan') + dl.dl_doc(doc, doc['title'], doc['detail'], subfolder='Sparplan') else: # In case of a stock transfer (Wertpapierübertrag) add additional information to the document title - if response['titleText'] == 'Wertpapierübertrag': + if event['title'] == 'Wertpapierübertrag': body = next(item['data']['body'] for item in self.events_with_docs if item['data']['id'] == response['id']) - dl.dl_doc(doc, response['titleText'] + " - " + body, response['subtitleText']) + dl.dl_doc(doc, doc['title'] + " - " + body, doc['detail']) else: - dl.dl_doc(doc, response['titleText'], response['subtitleText']) + dl.dl_doc(doc, doc['title'], doc['detail']) if self.received_detail == self.num_timeline_details: self.log.info('Received all details') From 2d87016879afd8e62a7fb56a5451342c78e391fa Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Tue, 12 Mar 2024 23:44:59 +0100 Subject: [PATCH 3/9] Adapted export_transactions() to new V2 data format. --- pytr/utils.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/pytr/utils.py b/pytr/utils.py index a190074..7d8ed48 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -169,6 +169,39 @@ def export_transactions(input_path, output_path, lang='auto'): "pt": 'Levantamento', "ru": '\u0421\u043F\u0438\u0441\u0430\u043D\u0438\u0435', }, + "interest": { + "cs": 'Úrokové poplatky', + "de": 'Zinsen', + "en": 'Interest', + "es": 'Interés', + "fr": 'L\'intérêts', + "it": 'Interessi', + "nl": 'Interest', + "pt": 'Odsetki', + "ru": '\u041f\u0440\u043e\u0446\u0435\u0301\u043d\u0442\u044b', + }, + "card transaction": { + "cs": 'Platba kartou', + "de": 'Kartentransaktion', + "en": 'Card Transaction', + "es": 'Transacción con tarjeta', + "fr": 'Transaction par carte', + "it": 'Transazione con carta', + "nl": 'Kaarttransactie', + "pt": 'Transakcja kartą', + "ru": '\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f\u0020\u043f\u043e\u0020\u043a\u0430\u0440\u0442\u0435', + }, + "decimal dot": { + "cs": ',', + "de": ',', + "en": '.', + "es": ',', + "fr": ',', + "it": ',', + "nl": ',', + "pt": ',', + "ru": ',', + }, } # Read relevant deposit timeline entries with open(input_path, encoding='utf-8') as f: @@ -196,15 +229,25 @@ def export_transactions(input_path, output_path, lang='auto'): if 'storniert' in body: continue + try: + decdot = i18n['decimal dot'][lang] + amount = str(event['amount']['value']).replace('.', decdot) + except (KeyError, TypeError): + continue + # Cash in - if title in ['Einzahlung', 'Bonuszahlung']: - f.write(csv_fmt.format(date=date, type=i18n['deposit'][lang], value=event['cashChangeAmount'])) - elif title == 'Auszahlung': - f.write(csv_fmt.format(date=date, type=i18n['removal'][lang], value=abs(event['cashChangeAmount']))) + if event["eventType"] in ("PAYMENT_INBOUND", "PAYMENT_INBOUND_SEPA_DIRECT_DEBIT"): + f.write(csv_fmt.format(date=date, type=i18n['deposit'][lang], value=amount)) + elif event["eventType"] == "PAYMENT_OUTBOUND": + f.write(csv_fmt.format(date=date, type=i18n['removal'][lang], value=abs(amount))) + elif event["eventType"] == "INTEREST_PAYOUT_CREATED": + f.write(csv_fmt.format(date=date, type=i18n['interest'][lang], value=amount)) # Dividend - Shares elif title == 'Reinvestierung': # TODO: Implement reinvestment log.warning('Detected reivestment, skipping... (not implemented yet)') + elif event["eventType"] == "card_successful_transaction": + f.write(csv_fmt.format(date=date, type=i18n['card transaction'][lang], value=abs(amount))) log.info('Deposit creation finished!') @@ -407,6 +450,6 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): with open(dl.output_path / 'events_with_documents.json', 'w', encoding='utf-8') as f: json.dump(self.events_with_docs, f, ensure_ascii=False, indent=2) - export_transactions(dl.output_path / 'other_events.json', dl.output_path / 'account_transactions.csv') + export_transactions(dl.output_path / 'events_with_documents.json', dl.output_path / 'account_transactions.csv') dl.work_responses() From 96025f4fa33bb32d20144f87a22ca571f67f312b Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Tue, 12 Mar 2024 23:46:36 +0100 Subject: [PATCH 4/9] Adapted export_transactions() to new V2 data format. --- pytr/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytr/utils.py b/pytr/utils.py index 7d8ed48..c3dc35e 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -231,7 +231,7 @@ def export_transactions(input_path, output_path, lang='auto'): try: decdot = i18n['decimal dot'][lang] - amount = str(event['amount']['value']).replace('.', decdot) + amount = str(abs(event['amount']['value'])).replace('.', decdot) except (KeyError, TypeError): continue @@ -239,7 +239,7 @@ def export_transactions(input_path, output_path, lang='auto'): if event["eventType"] in ("PAYMENT_INBOUND", "PAYMENT_INBOUND_SEPA_DIRECT_DEBIT"): f.write(csv_fmt.format(date=date, type=i18n['deposit'][lang], value=amount)) elif event["eventType"] == "PAYMENT_OUTBOUND": - f.write(csv_fmt.format(date=date, type=i18n['removal'][lang], value=abs(amount))) + f.write(csv_fmt.format(date=date, type=i18n['removal'][lang], value=amount)) elif event["eventType"] == "INTEREST_PAYOUT_CREATED": f.write(csv_fmt.format(date=date, type=i18n['interest'][lang], value=amount)) # Dividend - Shares @@ -247,7 +247,7 @@ def export_transactions(input_path, output_path, lang='auto'): # TODO: Implement reinvestment log.warning('Detected reivestment, skipping... (not implemented yet)') elif event["eventType"] == "card_successful_transaction": - f.write(csv_fmt.format(date=date, type=i18n['card transaction'][lang], value=abs(amount))) + f.write(csv_fmt.format(date=date, type=i18n['card transaction'][lang], value=amount)) log.info('Deposit creation finished!') From 3ead3f58d9feed89c2904e4849a760d5dff3905d Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Wed, 13 Mar 2024 22:19:42 +0100 Subject: [PATCH 5/9] Improved nameing of output directories and file names. Added extra information (subscription source, local filename) to stored event JSON. --- pytr/dl.py | 1 + pytr/utils.py | 48 +++++++++++++++++++++--------------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/pytr/dl.py b/pytr/dl.py index eea2315..db31dc0 100644 --- a/pytr/dl.py +++ b/pytr/dl.py @@ -143,6 +143,7 @@ def dl_doc(self, doc, titleText, subtitleText, subfolder=None): return else: filepath = filepath_with_doc_id + doc['local filepath'] = str(filepath) self.filepaths.append(filepath) if filepath.is_file() is False: diff --git a/pytr/utils.py b/pytr/utils.py index c3dc35e..64adfe8 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -283,6 +283,7 @@ async def get_next_timeline_transactions(self, response=None, max_age_timestamp= # print(json.dumps(response)) self.num_timeline_details += len(response['items']) for event in response['items']: + event['source'] = "timelineTransaction" self.timeline_events[event['id']] = event after = response['cursors'].get('after') @@ -315,6 +316,7 @@ async def get_next_timeline_activity_log(self, response=None, max_age_timestamp= self.num_timeline_details += len(response['items']) for event in response['items']: if event['id'] not in self.timeline_events: + event['source'] = "timelineActivity" self.timeline_events[event['id']] = event after = response['cursors'].get('after') @@ -397,24 +399,12 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): else: await self._get_timeline_details(5) - # print(f'len timeline_events: {len(self.timeline_events)}') - isSavingsPlan = False - if event['subtitle'] == 'Sparplan': - isSavingsPlan = True - else: - # some savingsPlan don't have the subtitle == 'Sparplan' but there are actions just for savingsPans - # but maybe these are unneeded duplicates - for section in response['sections']: - if section['type'] == 'actionButtons': - for button in section['data']: - if button['action']['type'] in ['editSavingsPlan', 'deleteSavingsPlan']: - isSavingsPlan = True - break - - if event['subtitle'] != 'Sparplan' and isSavingsPlan is True: - isSavingsPlan_fmt = ' -- SPARPLAN' - else: - isSavingsPlan_fmt = '' + isSavingsPlan = (event["eventType"] == "SAVINGS_PLAN_EXECUTED") + + isSavingsPlan_fmt = '' + if not isSavingsPlan and event['subtitle'] is not None: + isSavingsPlan = 'Sparplan' in event['subtitle'] + isSavingsPlan_fmt = ' -- SPARPLAN' if isSavingsPlan else '' max_details_digits = len(str(self.num_timeline_details)) self.log.info( @@ -422,6 +412,15 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): + f"{event['title']} -- {event['subtitle']}{isSavingsPlan_fmt}" ) + if isSavingsPlan: + subfolder = 'Sparplan' + else: + subfolder = { + 'benefits_saveback_execution': 'Saveback', + 'benefits_spare_change_execution': 'RoundUp', + 'INTEREST_PAYOUT_CREATED': 'Zinsen', + }.get(event["eventType"]) + for section in response['sections']: if section['type'] == 'documents': for doc in section['data']: @@ -431,15 +430,10 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): timestamp = datetime.now().timestamp() * 1000 if max_age_timestamp == 0 or max_age_timestamp < timestamp: # save all savingsplan documents in a subdirectory - if isSavingsPlan: - dl.dl_doc(doc, doc['title'], doc['detail'], subfolder='Sparplan') - else: - # In case of a stock transfer (Wertpapierübertrag) add additional information to the document title - if event['title'] == 'Wertpapierübertrag': - body = next(item['data']['body'] for item in self.events_with_docs if item['data']['id'] == response['id']) - dl.dl_doc(doc, doc['title'] + " - " + body, doc['detail']) - else: - dl.dl_doc(doc, doc['title'], doc['detail']) + title = f"{doc['title']} - {event['title']}" + if event['eventType'] in ["ACCOUNT_TRANSFER_INCOMING", "ACCOUNT_TRANSFER_OUTGOING"]: + title += f" - {event['subtitle']}" + dl.dl_doc(doc, title, doc['detail'], subfolder) if self.received_detail == self.num_timeline_details: self.log.info('Received all details') From 02bbcc3fe635f1298a57c86578dd26fa8e34c59b Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Fri, 7 Jun 2024 14:50:13 +0200 Subject: [PATCH 6/9] Fixed time format. Taken from https://github.com/Katzmann1983/pytr/tree/time_format_fix. --- pytr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytr/utils.py b/pytr/utils.py index 64adfe8..da3a475 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -217,7 +217,7 @@ def export_transactions(input_path, output_path, lang='auto'): f.write(header) for event in timeline: - dateTime = datetime.fromisoformat(event['timestamp']) + dateTime = datetime.fromisoformat(event['timestamp'][:19]) date = dateTime.strftime('%Y-%m-%d') title = event['title'] From 8d094fd6ab72dcea3f1b9572a68308bd458500ed Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Fri, 7 Jun 2024 21:36:44 +0200 Subject: [PATCH 7/9] Stability changes to avoid issues with missing details entry. --- pytr/dl.py | 23 ++++++++++++++++------- pytr/utils.py | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pytr/dl.py b/pytr/dl.py index db31dc0..5ae8734 100644 --- a/pytr/dl.py +++ b/pytr/dl.py @@ -88,17 +88,26 @@ def dl_doc(self, doc, titleText, subtitleText, subfolder=None): send asynchronous request, append future with filepath to self.futures ''' doc_url = doc['action']['payload'] - - date = doc['detail'] - iso_date = '-'.join(date.split('.')[::-1]) + if subtitleText is None: + subtitleText = '' + + try: + date = doc['detail'] + iso_date = '-'.join(date.split('.')[::-1]) + except KeyError: + date = '' + iso_date = '' doc_id = doc['id'] # extract time from subtitleText - time = re.findall('um (\\d+:\\d+) Uhr', subtitleText) - if time == []: + try: + time = re.findall('um (\\d+:\\d+) Uhr', subtitleText) + if time == []: + time = '' + else: + time = f' {time[0]}' + except TypeError: time = '' - else: - time = f' {time[0]}' if subfolder is not None: directory = self.output_path / subfolder diff --git a/pytr/utils.py b/pytr/utils.py index da3a475..d2645b9 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -426,14 +426,14 @@ async def timelineDetail(self, response, dl, max_age_timestamp=0): for doc in section['data']: try: timestamp = datetime.strptime(doc['detail'], '%d.%m.%Y').timestamp() * 1000 - except ValueError: + except (ValueError, KeyError): timestamp = datetime.now().timestamp() * 1000 if max_age_timestamp == 0 or max_age_timestamp < timestamp: # save all savingsplan documents in a subdirectory title = f"{doc['title']} - {event['title']}" if event['eventType'] in ["ACCOUNT_TRANSFER_INCOMING", "ACCOUNT_TRANSFER_OUTGOING"]: title += f" - {event['subtitle']}" - dl.dl_doc(doc, title, doc['detail'], subfolder) + dl.dl_doc(doc, title, doc.get('detail'), subfolder) if self.received_detail == self.num_timeline_details: self.log.info('Received all details') From 6d7bc9f6ac2c961578f193255261d9a488bff7b1 Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Sun, 9 Jun 2024 23:11:43 +0200 Subject: [PATCH 8/9] Fixed timestamp compare issue. --- pytr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytr/utils.py b/pytr/utils.py index d2645b9..4a8f27d 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -310,7 +310,7 @@ async def get_next_timeline_activity_log(self, response=None, max_age_timestamp= self.num_timelines = 0 await self.tr.timeline_activity_log() else: - timestamp = response['items'][-1]['timestamp'] + timestamp = datetime.fromisoformat(response['items'][-1]['timestamp']).timestamp() self.num_timelines += 1 # print(json.dumps(response)) self.num_timeline_details += len(response['items']) From b324b96693d8d09de07f530a11bbc6da087aee7c Mon Sep 17 00:00:00 2001 From: Martin Scharrer Date: Mon, 10 Jun 2024 10:27:24 +0200 Subject: [PATCH 9/9] Fixed timestamp code. --- pytr/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytr/utils.py b/pytr/utils.py index 4a8f27d..08865eb 100644 --- a/pytr/utils.py +++ b/pytr/utils.py @@ -310,7 +310,7 @@ async def get_next_timeline_activity_log(self, response=None, max_age_timestamp= self.num_timelines = 0 await self.tr.timeline_activity_log() else: - timestamp = datetime.fromisoformat(response['items'][-1]['timestamp']).timestamp() + timestamp = datetime.fromisoformat(response['items'][-1]['timestamp'][:19]).timestamp() self.num_timelines += 1 # print(json.dumps(response)) self.num_timeline_details += len(response['items'])