From e54e7201147bc3fd373fd6e4df61aa8c4ddbb96a Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:24:15 +0200 Subject: [PATCH 1/9] added cards cli command --- n26/cli.py | 44 ++++++++++++++++++++++++++++++++++++++++---- n26/const.py | 2 ++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/n26/cli.py b/n26/cli.py index 8f899ea..eb39c47 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -5,7 +5,7 @@ from tabulate import tabulate import n26.api as api -from n26.const import AMOUNT, CURRENCY, REFERENCE_TEXT, ATM_WITHDRAW +from n26.const import AMOUNT, CURRENCY, REFERENCE_TEXT, ATM_WITHDRAW, CARD_STATUS_ACTIVE API_CLIENT = api.Api() @@ -135,6 +135,26 @@ def spaces(): click.echo(text) +@cli.command() +def cards(): + """ Shows a list of cards """ + cards_data = API_CLIENT.get_cards() + + headers = ['Id', 'Type', 'Design', 'Status', 'Activated', 'Pin defined', 'Expires'] + keys = [ + 'id', + 'cardType', + 'design', + lambda x: "active" if (x.get('status') == CARD_STATUS_ACTIVE) else x.get('status'), + _datetime_extractor('cardActivated'), + _datetime_extractor('pinDefined'), + _datetime_extractor('expirationDate', date_only=True), + ] + text = _create_table_from_dict(headers=headers, value_functions=keys, data=cards_data, numalign='right') + + click.echo(text.strip()) + + @cli.command() @click.option('--card', default=None, type=str, help='ID of the card to block. Omitting this will block all cards.') def card_block(card: str): @@ -313,7 +333,11 @@ def statistics(param_from: int, to: int): def _timestamp_ms_to_date(epoch_ms: int) -> datetime or None: - """Convert millisecond timestamp to datetime.""" + """ + Convert millisecond timestamp to datetime. + + :param epoch_ms: milliseconds since 1970 in CET + """ if epoch_ms: return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc) @@ -351,13 +375,25 @@ def _create_table_from_dict(headers: list, value_functions: list, data: list, ** return tabulate(tabular_data=lines, headers=headers, **tabulate_args) -def _datetime_extractor(key: str): +def _datetime_extractor(key: str, date_only: bool = False): """ Helper function to extract a datetime value from a dict :param key: the dictionary key used to access the value + :param date_only: removes the time from the output :return: an extractor function """ - return lambda x: _timestamp_ms_to_date(x.get(key)) + + if date_only: + fmt = "%x" + else: + fmt = "%x %X" + + def extractor(dictionary: dict): + value = dictionary.get(key) + time = _timestamp_ms_to_date(value) + return time.strftime(fmt) + + return extractor def _insert_newlines(text: str, n=40): diff --git a/n26/const.py b/n26/const.py index b0683b7..1f0f476 100644 --- a/n26/const.py +++ b/n26/const.py @@ -12,3 +12,5 @@ CURRENCY = 'currencyCode' AMOUNT = 'amount' REFERENCE_TEXT = 'referenceText' + +CARD_STATUS_ACTIVE = 'M_ACTIVE' From 368d3de3d5a49863e5e0752f111525db5925f3a7 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:29:15 +0200 Subject: [PATCH 2/9] added test added maskedPan to output --- n26/cli.py | 3 ++- tests/test_cards.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/n26/cli.py b/n26/cli.py index eb39c47..19bef12 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -140,9 +140,10 @@ def cards(): """ Shows a list of cards """ cards_data = API_CLIENT.get_cards() - headers = ['Id', 'Type', 'Design', 'Status', 'Activated', 'Pin defined', 'Expires'] + headers = ['Id', 'Masked Pan', 'Type', 'Design', 'Status', 'Activated', 'Pin defined', 'Expires'] keys = [ 'id', + 'maskedPan', 'cardType', 'design', lambda x: "active" if (x.get('status') == CARD_STATUS_ACTIVE) else x.get('status'), diff --git a/tests/test_cards.py b/tests/test_cards.py index 22155c5..a4785d3 100644 --- a/tests/test_cards.py +++ b/tests/test_cards.py @@ -5,10 +5,15 @@ class CardsTests(N26TestBase): """Cards tests""" + @mock_config() @mock_requests(method=GET, response_file="cards.json") - def test_get_cards(self): - result = self._underTest.get_cards() - self.assertIsNotNone(result) + def test_cards_cli(self): + from n26.cli import cards + result = self._run_cli_cmd(cards) + self.assertIn('MASTERCARD', result.output) + self.assertIn('MAESTRO', result.output) + self.assertIn('active', result.output) + self.assertIn('123456******1234', result.output) @mock_config() @mock_requests(method=GET, response_file="cards.json") From b4d5d0c0d91784b22284ba811050d521d52e897d Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:31:23 +0200 Subject: [PATCH 3/9] use date_only where it makes sense --- n26/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/n26/cli.py b/n26/cli.py index 19bef12..9709561 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -293,10 +293,10 @@ def standing_orders(): values = ['partnerName', lambda x: "{} {}".format(x.get('amount'), x.get('currencyCode').get('currencyCode')), 'executionFrequency', - _datetime_extractor('stopTS'), + _datetime_extractor('stopTS', date_only=True), 'initialDayOfMonth', - _datetime_extractor('firstExecutingTS'), - _datetime_extractor('nextExecutingTS'), + _datetime_extractor('firstExecutingTS', date_only=True), + _datetime_extractor('nextExecutingTS', date_only=True), 'executionCounter', _datetime_extractor('created'), _datetime_extractor('updated')] From e606cbb3c3ddb46973d4a91c81f22fa66c353f62 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:35:42 +0200 Subject: [PATCH 4/9] None fix --- n26/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/n26/cli.py b/n26/cli.py index 9709561..e374aa7 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -392,7 +392,10 @@ def _datetime_extractor(key: str, date_only: bool = False): def extractor(dictionary: dict): value = dictionary.get(key) time = _timestamp_ms_to_date(value) - return time.strftime(fmt) + if time is None: + return "N/A" + else: + return time.strftime(fmt) return extractor From 6391aa5fb1f30a9f16c874a93a333f6a2f962821 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:39:47 +0200 Subject: [PATCH 5/9] convert time to local timezone before displaying --- n26/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/n26/cli.py b/n26/cli.py index e374aa7..57b396e 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -395,6 +395,7 @@ def extractor(dictionary: dict): if time is None: return "N/A" else: + time = time.astimezone() return time.strftime(fmt) return extractor From 02d7aa2a0a01366bc232c5c347771e6ce395718f Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:41:17 +0200 Subject: [PATCH 6/9] simplifications --- n26/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/n26/cli.py b/n26/cli.py index 57b396e..c044929 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -1,5 +1,5 @@ import webbrowser -from datetime import datetime, timezone +from datetime import datetime import click from tabulate import tabulate @@ -335,12 +335,13 @@ def statistics(param_from: int, to: int): def _timestamp_ms_to_date(epoch_ms: int) -> datetime or None: """ - Convert millisecond timestamp to datetime. + Convert millisecond timestamp to UTC datetime. :param epoch_ms: milliseconds since 1970 in CET + :return: a UTC datetime object """ if epoch_ms: - return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc) + return datetime.utcfromtimestamp(epoch_ms / 1000) def _create_table_from_dict(headers: list, value_functions: list, data: list, **tabulate_args) -> str: From 6ac467667fae45de7d26ccb47bb3da7cfa1c2c13 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:49:08 +0200 Subject: [PATCH 7/9] return None instead of "N/A" --- n26/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/n26/cli.py b/n26/cli.py index c044929..1df8ca5 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -394,7 +394,7 @@ def extractor(dictionary: dict): value = dictionary.get(key) time = _timestamp_ms_to_date(value) if time is None: - return "N/A" + return None else: time = time.astimezone() return time.strftime(fmt) From c63586740e58775862375a59e8a3f3092e3cd343 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 03:55:15 +0200 Subject: [PATCH 8/9] Revert "simplifications" This reverts commit 02d7aa2a --- n26/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/n26/cli.py b/n26/cli.py index 1df8ca5..ffc6ae7 100644 --- a/n26/cli.py +++ b/n26/cli.py @@ -1,5 +1,5 @@ import webbrowser -from datetime import datetime +from datetime import datetime, timezone import click from tabulate import tabulate @@ -341,7 +341,7 @@ def _timestamp_ms_to_date(epoch_ms: int) -> datetime or None: :return: a UTC datetime object """ if epoch_ms: - return datetime.utcfromtimestamp(epoch_ms / 1000) + return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc) def _create_table_from_dict(headers: list, value_functions: list, data: list, **tabulate_args) -> str: From 85e2c053407752ab548e4661e9cca361fe64116b Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Wed, 31 Jul 2019 04:04:59 +0200 Subject: [PATCH 9/9] test fix --- tests/test_standing_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_standing_orders.py b/tests/test_standing_orders.py index abe1ecf..22f46cd 100644 --- a/tests/test_standing_orders.py +++ b/tests/test_standing_orders.py @@ -18,4 +18,4 @@ def test_standing_orders_cli(self): self.assertIn('WEEKLY', result.output) self.assertIn('MONTHLY', result.output) self.assertIn('YEARLY', result.output) - self.assertIn('2019-04-01 00:29:49.461000+00:00', result.output) + self.assertIn('10/30/18', result.output)