From c38dec5ed8449f9e75be8bc1fe7fdb1919207909 Mon Sep 17 00:00:00 2001 From: Avihai Berkovitz Date: Tue, 24 Dec 2019 21:58:17 +0200 Subject: [PATCH 1/3] Added statistics framework --- assets/bank_beinleumi.py | 15 +++-- assets/bank_discount.py | 20 ++++--- assets/bank_leumi.py | 7 ++- assets/bank_otsar.py | 6 +- assets/card_cal.py | 19 +++--- assets/card_isracard.py | 4 +- assets/card_leumi.py | 4 +- assets/common.py | 58 ++++++++++++++---- assets/stats.py | 105 +++++++++++++++++++++++++++++++++ assets/stock_esop.py | 10 ++-- assets/stock_fidelity.py | 10 ++-- assets/stock_morgan_stanley.py | 10 ++-- bank.py | 41 +++---------- 13 files changed, 223 insertions(+), 86 deletions(-) create mode 100644 assets/stats.py diff --git a/assets/bank_beinleumi.py b/assets/bank_beinleumi.py index f4d4de8..44a7093 100644 --- a/assets/bank_beinleumi.py +++ b/assets/bank_beinleumi.py @@ -2,7 +2,8 @@ import re import requests from collections import OrderedDict -from .common import BankBase, format_value +from . import stats +from .common import BankBase, format_value, print_value from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By @@ -54,7 +55,9 @@ def _get_accounts(self): def _switch_account(self, account): main_html = self._session.get(self.HOME_URL, headers=headers).text base_href = re.search('', main_html).group(1) - form_action = re.search('
', main_html).group(1) + form_action = re.search( + '', main_html).group( + 1) data = dict(PortletForm_ACTION_NAME="changeAccount", portal_current_account=account) self._session.post(base_href + form_action, data=data, headers=headers) @@ -74,13 +77,15 @@ def _get_stock_value(self): NIA = match_obj.group(1) return format_value(NIA) - def get_values(self): + def get_values(self, stats_dict): bank = 0 stock = 0 for account in self._get_accounts(): self._switch_account(account) bank += self._get_values_from_main_page() stock += self._get_stock_value() - print("OSH: {:10,.2f}".format(bank)) - print("NIA: {:10,.2f}".format(stock)) + print_value(bank, "OSH") + print_value(stock, "NIA") + stats_dict[stats.StatType.STAT_BANK].add(bank) + stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) diff --git a/assets/bank_discount.py b/assets/bank_discount.py index 1f9dee9..81ca43b 100644 --- a/assets/bank_discount.py +++ b/assets/bank_discount.py @@ -1,13 +1,15 @@ -import re import requests from collections import OrderedDict -from .common import BankBase, format_value + +from . import stats +from .common import BankBase, print_value from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium import webdriver from selenium.webdriver.firefox.options import Options as WebDriverOptions + # username is in format , class BankDiscount(BankBase): @@ -42,14 +44,18 @@ def _establish_session(self, username, password): return session - def get_values(self): + def get_values(self, stats_dict): accounts_data = self._session.get(self.ACCOUNTS_JSON_URL).json() account_numbers = [account['FormatAccountID'] for account in accounts_data['UserAccountsData']['UserAccounts']] bank = 0 stock = 0 for account_number in account_numbers: - bank += self._session.get(self.BALANCE_JSON_URL.format(account_number)).json()['AccountDetails']['AccountBalance'] - stock += self._session.post(self.STOCK_JSON_URL, json={"AccountNumber": account_number}).json()['CurrentSecuritiesPortfolio']['PortfolioValue'] - print("OSH: {:10,.2f}".format(bank)) - print("NIA: {:10,.2f}".format(stock)) + bank += self._session.get(self.BALANCE_JSON_URL.format(account_number)).json()['AccountDetails'][ + 'AccountBalance'] + stock += self._session.post(self.STOCK_JSON_URL, json={"AccountNumber": account_number}).json()[ + 'CurrentSecuritiesPortfolio']['PortfolioValue'] + print_value(bank, "OSH") + print_value(stock, "NIA") + stats_dict[stats.StatType.STAT_BANK].add(bank) + stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) diff --git a/assets/bank_leumi.py b/assets/bank_leumi.py index b9279da..fe2a75e 100644 --- a/assets/bank_leumi.py +++ b/assets/bank_leumi.py @@ -3,7 +3,10 @@ import json import re from collections import OrderedDict + import requests + +from . import stats from .common import BankBase, HEADERS_USER_AGENT, print_value @@ -75,10 +78,12 @@ def _establish_session(self, username, password): s.post(self.LOGIN_POST_URL, data=post_data, headers=HEADERS_USER_AGENT) return s - def get_values(self): + def get_values(self, stats_dict): checking = self.__total_values['Checking'] holdings = self.__total_values['Holdings'] deposit = self.__total_values['Deposit'] + stats_dict[stats.StatType.STAT_BANK].add(checking + deposit) + stats_dict[stats.StatType.STAT_STOCK_BROKER].add(holdings) return OrderedDict([("Checking", checking), ("Holdings", holdings), ("Deposit", deposit)]) def get_total_values(self): diff --git a/assets/bank_otsar.py b/assets/bank_otsar.py index 29bc0f1..b714856 100644 --- a/assets/bank_otsar.py +++ b/assets/bank_otsar.py @@ -1,6 +1,8 @@ import re import requests from collections import OrderedDict + +from . import stats from .common import BankBase, format_value @@ -27,7 +29,9 @@ def _get_stock_value(self): NIA = NIA[-1] return format_value(NIA, 'NIA') - def get_values(self): + def get_values(self, stats_dict): bank = self._get_values_from_main_page() stock = self._get_stock_value() + stats_dict[stats.StatType.STAT_BANK].add(bank) + stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) diff --git a/assets/card_cal.py b/assets/card_cal.py index 6b5b8ce..58cd001 100644 --- a/assets/card_cal.py +++ b/assets/card_cal.py @@ -7,7 +7,7 @@ from selenium.webdriver.support import expected_conditions as EC from selenium import webdriver from selenium.webdriver.firefox.options import Options as WebDriverOptions -import time + class CardCal(CardBase): CARD_LOGIN_URL = "https://services.cal-online.co.il/card-holders/Screens/AccountManagement/Login.aspx" @@ -47,21 +47,22 @@ def _establish_session(self, username, password): return session - def _get_card_value(self, card_data, card_code, print_name=None): + def __get_card_value(self, card_data, card_code, print_name=None): val = re.search(self.CARD_VALUE_RE % (card_code,), card_data).group(1) return format_value(val, print_name) - def _get_balance(self, card_code): + def __get_balance(self, card_code): home_data = self._session.get(self.CARD_HOME_URL) - card_details_queries = re.findall("(\?cardUniqueID=\d+)", home_data.text) + card_details_queries = re.findall(r"(\?cardUniqueID=\d+)", home_data.text) card_datas = [self._session.get(self.CARD_DETAIL_URL + card_details_query) for card_details_query in card_details_queries] - return sum(self._get_card_value(card_data.text, card_code) for card_data in card_datas) + return sum(self.__get_card_value(card_data.text, card_code) for card_data in card_datas) - def get_credit(self): - card_total = self._get_balance("lblTotalRemainingSum") + def _get_credit(self): + card_total = self.__get_balance("lblTotalRemainingSum") print_value(0 - card_total, "Credit") return 0 - card_total - def get_next(self): - return self._get_balance("lblNextDebitSum") + def _get_next(self): + card_next = self.__get_balance("lblNextDebitSum") + return 0 - card_next diff --git a/assets/card_isracard.py b/assets/card_isracard.py index 1fc872f..c04a7ac 100644 --- a/assets/card_isracard.py +++ b/assets/card_isracard.py @@ -45,11 +45,11 @@ def _establish_session(self, username, password): s.post(self.LOGIN_POST_URL, data=post_data_str, headers=headers) return s - def get_credit(self): + def _get_credit(self): card_data_raw = self._session.get(self.CARD_DATA_URL).text card_data = json.loads(card_data_raw) upcoming_billing = card_data["DashboardChargesBean"]["inOut"][0]["nextTotalsInOut"][0]["billingSumSekelInOut"] return format_value("-" + upcoming_billing, 'Credit') - def get_next(self): + def _get_next(self): return 0 diff --git a/assets/card_leumi.py b/assets/card_leumi.py index 5c213be..53c2308 100644 --- a/assets/card_leumi.py +++ b/assets/card_leumi.py @@ -12,10 +12,10 @@ def __init__(self, asset_section, **asset_options): def _establish_session(self, username, password): return None - def get_credit(self): + def _get_credit(self): credit = self.__bank_instance.get_total_values()['Creditcard'] print_value(credit, 'Credit') return credit - def get_next(self): + def _get_next(self): return 0 diff --git a/assets/common.py b/assets/common.py index e10ea89..8c83d18 100644 --- a/assets/common.py +++ b/assets/common.py @@ -2,9 +2,12 @@ import json from abc import ABCMeta, abstractmethod +from collections import OrderedDict import requests +from . import stats + HEADERS_USER_AGENT = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" } @@ -65,6 +68,12 @@ def get_stock_value(stock_name): class AssetBase(object): __metaclass__ = ABCMeta + @abstractmethod + def get_values(self, stats_dict): + raise NotImplementedError() + + +class AuthenticatedAssetBase(AssetBase): def __init__(self, asset_section, user=None, password=None, **asset_options): self._username = user self._password = password @@ -78,31 +87,58 @@ def _establish_session(self, username, password): raise NotImplementedError() -class BankBase(AssetBase): - @abstractmethod - def get_values(self): - raise NotImplementedError() +class BankBase(AuthenticatedAssetBase): + pass -class CardBase(AssetBase): +class CardBase(AuthenticatedAssetBase): @abstractmethod - def get_credit(self): + def _get_credit(self): raise NotImplementedError() @abstractmethod - def get_next(self): + def _get_next(self): raise NotImplementedError() + def get_values(self, stats_dict): + credit_value = self._get_credit() + card_next = self._get_next() + stats_dict[stats.StatType.STAT_CARD].add(credit_value, card_next) + return OrderedDict([("Credit", credit_value)]) + -class StockBrokerBase(AssetBase): +class WorkStockBase(AuthenticatedAssetBase): @abstractmethod - def get_exercisable(self): + def _get_exercisable(self): raise NotImplementedError() @abstractmethod - def get_vested(self): + def _get_vested(self): raise NotImplementedError() @abstractmethod - def get_unvested(self): + def _get_unvested(self): raise NotImplementedError() + + def get_values(self, stats_dict): + exercisable = self._get_exercisable() + vested = self._get_vested() + unvested = self._get_unvested() + stats_dict[stats.StatType.STAT_WORK_STOCK].add(exercisable, vested, unvested) + return OrderedDict([("Exercisable", exercisable)]) + + +class CommodityBase(AssetBase): + def __init__(self, asset_section, amount=None, **asset_options): + if not amount: + raise Exception("{} amount missing".format(asset_section.capitalize())) + self._amount = float(amount) + + @abstractmethod + def _get_value(self): + raise NotImplementedError() + + def get_values(self, stats_dict): + value = self._get_value() + stats_dict[stats.StatType.STAT_NONE].add(value) + return OrderedDict([("Value", value)]) diff --git a/assets/stats.py b/assets/stats.py new file mode 100644 index 0000000..636614a --- /dev/null +++ b/assets/stats.py @@ -0,0 +1,105 @@ +from __future__ import print_function + +from abc import ABCMeta, abstractmethod +from enum import Enum + + +class StatBase(object): + __metaclass__ = ABCMeta + + def __init__(self): + self.__total = 0 + + @abstractmethod + def print_stat(self): + raise NotImplementedError() + + def add(self, amount): + self.__total += amount + + def get_total_amount(self): + return self.__total + + +class StatNone(StatBase): + + def print_stat(self): + pass + + +class StatBank(StatBase): + + def __init__(self): + super(StatBank, self).__init__() + + def print_stat(self): + print("All banks: {:,.2f}".format(self.get_total_amount())) + + +class StatCard(StatBase): + + def __init__(self): + super(StatCard, self).__init__() + self.__next = 0 + + def add(self, amount, next_amount=0): + super(StatCard, self).add(amount) + self.__next += next_amount + + def print_stat(self): + print("All cards: {:,.2f} (next: {:,.2f})".format(self.get_total_amount(), self.__next)) + + +class StatStockBroker(StatBase): + + def __init__(self): + super(StatStockBroker, self).__init__() + + def print_stat(self): + print("All stocks: {:,.2f}".format(self.get_total_amount())) + + +class StatWorkStock(StatBase): + + def __init__(self): + super(StatWorkStock, self).__init__() + self.__vested = 0 + self.__unvested = 0 + + def add(self, exercisable, vested=0, unvested=0): + super(StatWorkStock, self).add(exercisable) + self.__vested += vested + self.__unvested += unvested + + def print_stat(self): + print("All work stocks: {:,.2f} (vested: {:,.2f}, unvested {:,.2f})" + .format(self.get_total_amount(), self.__vested, self.__unvested)) + + +class StatType(Enum): + STAT_NONE = (0, StatNone) + STAT_BANK = (1, StatBank) + STAT_CARD = (2, StatCard) + STAT_STOCK_BROKER = (3, StatStockBroker) + STAT_WORK_STOCK = (4, StatWorkStock) + + def __init__(self, order, stat_class): + self.order = order + self.__stat_class = stat_class + + def create_stat_class(self): + return self.__stat_class() + + +class StatsDict(dict): + + def __getitem__(self, key): + if not isinstance(key, StatType): + raise Exception("Invalid stats type, must be one of the StatType enum values") + return super(StatsDict, self).setdefault(key, key.create_stat_class()) + + def get_all_stats_ordered(self): + return [x[1] for x in sorted(self.items(), key=lambda pair: pair[0].order)] + + def get_total(self): + return sum([x.get_total_amount() for x in self.values()]) diff --git a/assets/stock_esop.py b/assets/stock_esop.py index 6324bff..78e2905 100644 --- a/assets/stock_esop.py +++ b/assets/stock_esop.py @@ -2,10 +2,10 @@ import json import requests from datetime import datetime -from .common import StockBrokerBase, get_stock_value, convert_usd_to_ils, print_value +from .common import WorkStockBase, get_stock_value, convert_usd_to_ils, print_value -class StockEsop(StockBrokerBase): +class StockEsop(WorkStockBase): LOGIN_URL = "https://www.capital-m.co.il/C-MClient/j_security_check" SERVLET_URL = "https://www.capital-m.co.il/C-MClient/theme/js/gwt/optionsPlanDetails/gwtservlet" PLAN_OBJECT_DATA_LENGTH = 49 @@ -123,11 +123,11 @@ def __get_total_value(self, value_name): print_value(result, value_name) return result - def get_exercisable(self): + def _get_exercisable(self): return self.__get_total_value("Exercisable") - def get_vested(self): + def _get_vested(self): return self.__get_total_value("Vested") - def get_unvested(self): + def _get_unvested(self): return self.__get_total_value("Unvested") diff --git a/assets/stock_fidelity.py b/assets/stock_fidelity.py index 353d08b..dfbad42 100644 --- a/assets/stock_fidelity.py +++ b/assets/stock_fidelity.py @@ -1,10 +1,10 @@ import json import re import requests -from .common import StockBrokerBase, format_value, HEADERS_USER_AGENT +from .common import WorkStockBase, format_value, HEADERS_USER_AGENT -class StockFidelityNetBenefits(StockBrokerBase): +class StockFidelityNetBenefits(WorkStockBase): LOGIN_URL = "https://nb.fidelity.com/public/nb/default/home" SENSOR_DATA_URL = "https://nb.fidelity.com/_bm/_data" PRE_LOGIN_URL = "https://nb.fidelity.com/public/nb/api/prelogin/default" @@ -38,13 +38,13 @@ def _establish_session(self, username, password): return s - def get_exercisable(self): + def _get_exercisable(self): summary_data_str = self._session.get(self.SUMMARY_URL).text match = re.search(""".+?(.+?)""", summary_data_str) return format_value(match.group(1), "Total") - def get_vested(self): + def _get_vested(self): return 0 - def get_unvested(self): + def _get_unvested(self): return 0 diff --git a/assets/stock_morgan_stanley.py b/assets/stock_morgan_stanley.py index 0b3039a..870110c 100644 --- a/assets/stock_morgan_stanley.py +++ b/assets/stock_morgan_stanley.py @@ -1,9 +1,9 @@ import json import requests -from .common import StockBrokerBase, convert_usd_to_ils, print_value +from .common import WorkStockBase, convert_usd_to_ils, print_value -class MorganStanleyStockPlanConnect(StockBrokerBase): +class MorganStanleyStockPlanConnect(WorkStockBase): LOGIN_URL = "https://stockplanconnect.morganstanley.com/cesreg/Home/Home.html#/home" LOGIN_POST_URL = "https://stockplanconnect.morganstanley.com/app-bin/cesreg/spc/login/validateLogin" SUMMARY_URL = "https://stockplanconnect.morganstanley.com/app-bin/spc/ba/sps/summary?format=json" @@ -31,11 +31,11 @@ def get_summary_value(self, value_name, print_name): print_value(value_ils, "{} final".format(print_name)) return value_ils - def get_exercisable(self): + def _get_exercisable(self): return self.get_summary_value("totalMktvalue", "Exercisable") - def get_vested(self): + def _get_vested(self): return 0 - def get_unvested(self): + def _get_unvested(self): return self.get_summary_value("totalUnvestedvalue", "Unvested") diff --git a/bank.py b/bank.py index bb5429e..bb8b0ab 100644 --- a/bank.py +++ b/bank.py @@ -3,7 +3,8 @@ from __future__ import print_function from collections import OrderedDict -from assets.common import BankBase, CardBase, StockBrokerBase, all_memoize_caches +from assets import stats +from assets.common import all_memoize_caches from config import get_config_value, get_asset_sections, get_config_options import assets @@ -18,50 +19,24 @@ def main(): for cache in all_memoize_caches: cache.clear() all_values = OrderedDict() + all_stats = stats.StatsDict() - bank_total = 0 - card_total = 0 - card_next = 0 - stock_exercisable = 0 - stock_vested = 0 - stock_unvested = 0 asset_sections = get_asset_sections() for asset_section in asset_sections: print("{}:".format(asset_section)) asset = get_asset(asset_section) - - if isinstance(asset, BankBase): - values = asset.get_values() - bank_total += sum(values.values()) - elif isinstance(asset, CardBase): - credit_value = asset.get_credit() - values = OrderedDict([("Credit", credit_value)]) - card_total += abs(credit_value) - card_next += asset.get_next() - elif isinstance(asset, StockBrokerBase): - exercisable_value = asset.get_exercisable() - values = OrderedDict([("Exercisable", exercisable_value)]) - stock_exercisable += exercisable_value - stock_vested += asset.get_vested() - stock_unvested += asset.get_unvested() - else: - raise Exception("Unknown asset {} of type {}".format(asset, type(asset))) - + values = asset.get_values(all_stats) values_with_prefix = OrderedDict( [("{} - {}".format(asset_section, key), value) for key, value in values.items()]) all_values.update(values_with_prefix) print() - print("Total all banks: {:10,.2f}".format(bank_total)) - print() - print("All cards: {:,.2f} (next: {:,.2f})".format(card_total, card_next)) - print() - if stock_exercisable != 0 or stock_vested != 0 or stock_unvested != 0: - print("All stock brokers: {:,.2f} (vested: {:,.2f}, unvested {:,.2f})" - .format(stock_exercisable, stock_vested, stock_unvested)) + for stat in all_stats.get_all_stats_ordered(): + stat.print_stat() print() - print("Total: {:10,.2f}".format(bank_total - card_total + stock_exercisable)) + + print("Total: {:10,.2f}".format(all_stats.get_total())) print() return all_values From 93b18a238dfc112394e829b4512c1b378047d6fb Mon Sep 17 00:00:00 2001 From: Avihai Berkovitz Date: Fri, 10 Jan 2020 14:29:33 +0200 Subject: [PATCH 2/3] Added type hints everywhere --- assets/bank_beinleumi.py | 22 +++++++++++++++------- assets/bank_discount.py | 15 +++++++++------ assets/bank_leumi.py | 7 ++++++- assets/bank_otsar.py | 7 ++++++- assets/card_cal.py | 18 +++++++++++++----- assets/card_isracard.py | 6 ++++++ assets/card_leumi.py | 7 +++++++ assets/common.py | 26 ++++++++++++++++++++++---- assets/stats.py | 16 ++++++++++++++++ assets/stock_esop.py | 15 ++++++++++++++- assets/stock_fidelity.py | 6 ++++++ assets/stock_morgan_stanley.py | 14 +++++++++++--- bank.py | 7 +++++-- bank_routine.py | 1 + 14 files changed, 137 insertions(+), 30 deletions(-) diff --git a/assets/bank_beinleumi.py b/assets/bank_beinleumi.py index 44a7093..30540f7 100644 --- a/assets/bank_beinleumi.py +++ b/assets/bank_beinleumi.py @@ -1,16 +1,18 @@ from __future__ import print_function + +import os import re -import requests from collections import OrderedDict -from . import stats -from .common import BankBase, format_value, print_value +from typing import List -from selenium.webdriver.support.ui import WebDriverWait +import requests +from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC -from selenium import webdriver -import time -import os +from selenium.webdriver.support.ui import WebDriverWait + +from . import stats +from .common import BankBase, format_value, print_value headers = {"User-Agent": "Mozilla/5.0"} @@ -27,6 +29,7 @@ def _wait_for_id(self, html_id): WebDriverWait(self.selenium, 10).until(indicator) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session os.environ["DISPLAY"] = ":1" self.selenium = webdriver.Firefox() self.selenium.get("https://online.fibi.co.il/") @@ -49,10 +52,12 @@ def _establish_session(self, username, password): return session def _get_accounts(self): + # type: () -> List[str] main_html = self._session.get(self.HOME_URL, headers=headers).text return re.findall('option value="([^"]+)"', main_html) def _switch_account(self, account): + # type: (str) -> None main_html = self._session.get(self.HOME_URL, headers=headers).text base_href = re.search('', main_html).group(1) form_action = re.search( @@ -62,6 +67,7 @@ def _switch_account(self, account): self._session.post(base_href + form_action, data=data, headers=headers) def _get_values_from_main_page(self): + # type: () -> float main_html = self._session.get(self.BALANCE_URL, headers=headers).text match_obj = re.search(self.BALANCE_PATTERN, main_html, re.DOTALL) if match_obj is None: @@ -70,6 +76,7 @@ def _get_values_from_main_page(self): return format_value(OSH) def _get_stock_value(self): + # type: () -> float stock_html = self._session.get(self.STOCK_URL, headers=headers).text match_obj = re.search(self.STOCK_PATTERN, stock_html) if match_obj is None: @@ -78,6 +85,7 @@ def _get_stock_value(self): return format_value(NIA) def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] bank = 0 stock = 0 for account in self._get_accounts(): diff --git a/assets/bank_discount.py b/assets/bank_discount.py index 81ca43b..346ef6d 100644 --- a/assets/bank_discount.py +++ b/assets/bank_discount.py @@ -1,13 +1,14 @@ -import requests from collections import OrderedDict -from . import stats -from .common import BankBase, print_value -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC +import requests from selenium import webdriver +from selenium.webdriver.common.by import By from selenium.webdriver.firefox.options import Options as WebDriverOptions +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from . import stats +from .common import BankBase, print_value # username is in format , @@ -23,6 +24,7 @@ def _wait_for_id(self, html_id): WebDriverWait(self.selenium, 180).until(indicator) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session uid, code = username.split(",") options = WebDriverOptions() options.headless = True @@ -45,6 +47,7 @@ def _establish_session(self, username, password): return session def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] accounts_data = self._session.get(self.ACCOUNTS_JSON_URL).json() account_numbers = [account['FormatAccountID'] for account in accounts_data['UserAccountsData']['UserAccounts']] bank = 0 diff --git a/assets/bank_leumi.py b/assets/bank_leumi.py index fe2a75e..f281cec 100644 --- a/assets/bank_leumi.py +++ b/assets/bank_leumi.py @@ -3,6 +3,7 @@ import json import re from collections import OrderedDict +from typing import Dict import requests @@ -16,6 +17,7 @@ class BankLeumi(BankBase): HOME_URL = "https://hb2.bankleumi.co.il/uniquesig0/ebanking/SO/SPA.aspx#/hpsummary" def __init__(self, asset_section, print_info=True, **asset_options): + # type: (str, bool, ...) -> None super(BankLeumi, self).__init__(asset_section, **asset_options) home_response = self._session.get(self.HOME_URL, headers=HEADERS_USER_AGENT) summary_page = home_response.text @@ -31,7 +33,7 @@ def __init__(self, asset_section, print_info=True, **asset_options): private_data = dict([(key, json.loads(value)) for key, value in private_data.items()]) session_id = private_data['SO_Signon']['SessionID'] - self.__total_values = collections.defaultdict(int) + self.__total_values = collections.defaultdict(float) for account_item in private_data['SHEMESHPREMIUM_AccountsItems_hpsummary']['AccountsItems']: req_obj = { @@ -72,6 +74,7 @@ def __init__(self, asset_section, print_info=True, **asset_options): self.__total_values[account_type_name] = self.__total_values[account_type_name] + account_type_total def _establish_session(self, username, password): + # type: (str, str) -> requests.Session s = requests.Session() s.get(self.LOGIN_URL, headers=HEADERS_USER_AGENT) post_data = {'system': 'test', 'uid': username, 'password': password, 'command': 'login'} @@ -79,6 +82,7 @@ def _establish_session(self, username, password): return s def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] checking = self.__total_values['Checking'] holdings = self.__total_values['Holdings'] deposit = self.__total_values['Deposit'] @@ -87,4 +91,5 @@ def get_values(self, stats_dict): return OrderedDict([("Checking", checking), ("Holdings", holdings), ("Deposit", deposit)]) def get_total_values(self): + # type: () -> Dict[str, float] return self.__total_values diff --git a/assets/bank_otsar.py b/assets/bank_otsar.py index b714856..12182c8 100644 --- a/assets/bank_otsar.py +++ b/assets/bank_otsar.py @@ -1,7 +1,8 @@ import re -import requests from collections import OrderedDict +import requests + from . import stats from .common import BankBase, format_value @@ -12,24 +13,28 @@ class BankOtsar(BankBase): STOCK_URL = "https://online.bankotsar.co.il/wps/myportal/FibiMenu/Online/OnCapitalMarket/OnMyportfolio/AuthSecuritiesPrtfMyPFEquities" def _establish_session(self, username, password): + # type: (str, str) -> requests.Session s = requests.Session() post_data = {"bankId": "OTSARPRTAL", "lang": "HE", "username": username, "password": password} s.post(self.LOGIN_URL, data=post_data) return s def _get_values_from_main_page(self): + # type: () -> float main_page_html = self._session.get(self.HOME_URL).text OSH = re.search("current_balance[^>]+>\s*\S+\s*([^<]+)\s*", main_page_html) OSH = OSH.group(1) return format_value(OSH, 'OSH') def _get_stock_value(self): + # type: () -> float stock_html = self._session.get(self.STOCK_URL).text NIA = re.findall("subtotal_val[^>]+>\s*\S+\s*([^<]+)\s*", stock_html) NIA = NIA[-1] return format_value(NIA, 'NIA') def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] bank = self._get_values_from_main_page() stock = self._get_stock_value() stats_dict[stats.StatType.STAT_BANK].add(bank) diff --git a/assets/card_cal.py b/assets/card_cal.py index 58cd001..67cba2a 100644 --- a/assets/card_cal.py +++ b/assets/card_cal.py @@ -1,12 +1,13 @@ import re -import requests -from .common import CardBase, format_value, print_value -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC +import requests from selenium import webdriver +from selenium.webdriver.common.by import By from selenium.webdriver.firefox.options import Options as WebDriverOptions +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from .common import CardBase, format_value, print_value class CardCal(CardBase): @@ -16,14 +17,17 @@ class CardCal(CardBase): CARD_VALUE_RE = """(.*?)""" def _wait_for_id(self, html_id): + # type: (str) -> None indicator = EC.presence_of_element_located((By.ID, html_id)) WebDriverWait(self.selenium, 10).until(indicator) def _wait_for_name(self, html_name): + # type: (str) -> None indicator = EC.presence_of_element_located((By.NAME, html_name)) WebDriverWait(self.selenium, 10).until(indicator) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session options = WebDriverOptions() options.headless = True self.selenium = webdriver.Firefox(options=options) @@ -48,10 +52,12 @@ def _establish_session(self, username, password): return session def __get_card_value(self, card_data, card_code, print_name=None): + # type: (str, str, str) -> float val = re.search(self.CARD_VALUE_RE % (card_code,), card_data).group(1) return format_value(val, print_name) def __get_balance(self, card_code): + # type: (str) -> float home_data = self._session.get(self.CARD_HOME_URL) card_details_queries = re.findall(r"(\?cardUniqueID=\d+)", home_data.text) card_datas = [self._session.get(self.CARD_DETAIL_URL + card_details_query) @@ -59,10 +65,12 @@ def __get_balance(self, card_code): return sum(self.__get_card_value(card_data.text, card_code) for card_data in card_datas) def _get_credit(self): + # type: () -> float card_total = self.__get_balance("lblTotalRemainingSum") print_value(0 - card_total, "Credit") return 0 - card_total def _get_next(self): + # type: () -> float card_next = self.__get_balance("lblNextDebitSum") return 0 - card_next diff --git a/assets/card_isracard.py b/assets/card_isracard.py index c04a7ac..9b26e95 100644 --- a/assets/card_isracard.py +++ b/assets/card_isracard.py @@ -1,5 +1,7 @@ import json + import requests + from .common import CardBase, format_value @@ -10,6 +12,7 @@ class CardIsracard(CardBase): CARD_DATA_URL = "https://digital.isracard.co.il/services/ProxyRequestHandler.ashx?reqName=DashboardCharges&format=Json&cardIdx=&returnDataStructureLevel=1&cardIndexes=&accountNumber=&actionCode=0&identityId=" def __init__(self, asset_section, user_id=None, card_suffix=None, **asset_options): + # type: (str, str, str, ...) -> None self.__user_id = user_id self.__card_suffix = card_suffix if not self.__user_id or not self.__card_suffix: @@ -17,6 +20,7 @@ def __init__(self, asset_section, user_id=None, card_suffix=None, **asset_option super(CardIsracard, self).__init__(asset_section, user="dummy", **asset_options) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session headers = {"content-type": "application/x-www-form-urlencoded; charset=UTF-8"} s = requests.Session() s.get(self.LOGIN_URL) @@ -46,10 +50,12 @@ def _establish_session(self, username, password): return s def _get_credit(self): + # type: () -> float card_data_raw = self._session.get(self.CARD_DATA_URL).text card_data = json.loads(card_data_raw) upcoming_billing = card_data["DashboardChargesBean"]["inOut"][0]["nextTotalsInOut"][0]["billingSumSekelInOut"] return format_value("-" + upcoming_billing, 'Credit') def _get_next(self): + # type: () -> float return 0 diff --git a/assets/card_leumi.py b/assets/card_leumi.py index 53c2308..33316c9 100644 --- a/assets/card_leumi.py +++ b/assets/card_leumi.py @@ -1,3 +1,5 @@ +import requests + from .bank_leumi import BankLeumi from .common import CardBase, print_value @@ -6,16 +8,21 @@ class CardLeumi(CardBase): TOTAL_RE = r'{\\"AccountType\\":\\"CREDITCARD\\",\\"TotalPerAccountType\\":(.+?)}' def __init__(self, asset_section, **asset_options): + # type: (str, ...) -> None super(CardLeumi, self).__init__(asset_section, **asset_options) self.__bank_instance = BankLeumi(asset_section, print_info=False, **asset_options) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session + # noinspection PyTypeChecker return None def _get_credit(self): + # type: () -> float credit = self.__bank_instance.get_total_values()['Creditcard'] print_value(credit, 'Credit') return credit def _get_next(self): + # type: () -> float return 0 diff --git a/assets/common.py b/assets/common.py index 8c83d18..9cba0ae 100644 --- a/assets/common.py +++ b/assets/common.py @@ -1,8 +1,9 @@ from __future__ import print_function import json -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod, ABC from collections import OrderedDict +from typing import cast import requests @@ -14,10 +15,12 @@ def print_value(val, print_name): + # type: (float, str) -> None print("{}: {:10,.2f}".format(print_name, val)) def format_value(value_text, print_name=None): + # type: (str, str) -> float val = float(value_text.replace(",", "")) if print_name is not None: print_value(val, print_name) @@ -44,6 +47,7 @@ def wrapper(*args): @memoize def get_usd_to_ils_conversion_ratio(): + # type: () -> float api_result = requests.get( "https://free.currencyconverterapi.com/api/v5/convert?q=USD_ILS&compact=ultra&apiKey=207f0d8f1a97997f891a").text api_data = json.loads(api_result) @@ -51,12 +55,14 @@ def get_usd_to_ils_conversion_ratio(): def convert_usd_to_ils(usd_value): + # type: (float) -> float ratio = get_usd_to_ils_conversion_ratio() return usd_value * ratio @memoize def get_stock_value(stock_name): + # type: (str) -> float url = "https://www.alphavantage.co/query?apikey=97DT8FPVN9WQQGIQ&function=TIME_SERIES_DAILY&symbol={}" \ .format(stock_name) api_result = requests.get(url).text @@ -70,11 +76,13 @@ class AssetBase(object): @abstractmethod def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] raise NotImplementedError() class AuthenticatedAssetBase(AssetBase): def __init__(self, asset_section, user=None, password=None, **asset_options): + # type: (str, str, str, ...) -> None self._username = user self._password = password if not self._username or not self._password: @@ -84,47 +92,55 @@ def __init__(self, asset_section, user=None, password=None, **asset_options): @abstractmethod def _establish_session(self, username, password): + # type: (str, str) -> requests.Session raise NotImplementedError() -class BankBase(AuthenticatedAssetBase): +class BankBase(AuthenticatedAssetBase, ABC): pass class CardBase(AuthenticatedAssetBase): @abstractmethod def _get_credit(self): + # type: () -> float raise NotImplementedError() @abstractmethod def _get_next(self): + # type: () -> float raise NotImplementedError() def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] credit_value = self._get_credit() card_next = self._get_next() - stats_dict[stats.StatType.STAT_CARD].add(credit_value, card_next) + cast(stats.StatCard, stats_dict[stats.StatType.STAT_CARD]).add(credit_value, card_next) return OrderedDict([("Credit", credit_value)]) class WorkStockBase(AuthenticatedAssetBase): @abstractmethod def _get_exercisable(self): + # type: () -> float raise NotImplementedError() @abstractmethod def _get_vested(self): + # type: () -> float raise NotImplementedError() @abstractmethod def _get_unvested(self): + # type: () -> float raise NotImplementedError() def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] exercisable = self._get_exercisable() vested = self._get_vested() unvested = self._get_unvested() - stats_dict[stats.StatType.STAT_WORK_STOCK].add(exercisable, vested, unvested) + cast(stats.StatWorkStock, stats_dict[stats.StatType.STAT_WORK_STOCK]).add(exercisable, vested, unvested) return OrderedDict([("Exercisable", exercisable)]) @@ -136,9 +152,11 @@ def __init__(self, asset_section, amount=None, **asset_options): @abstractmethod def _get_value(self): + # type: () -> float raise NotImplementedError() def get_values(self, stats_dict): + # type: (stats.StatsDict) -> OrderedDict[str, float] value = self._get_value() stats_dict[stats.StatType.STAT_NONE].add(value) return OrderedDict([("Value", value)]) diff --git a/assets/stats.py b/assets/stats.py index 636614a..28e0e20 100644 --- a/assets/stats.py +++ b/assets/stats.py @@ -2,6 +2,7 @@ from abc import ABCMeta, abstractmethod from enum import Enum +from typing import List, Type class StatBase(object): @@ -12,18 +13,22 @@ def __init__(self): @abstractmethod def print_stat(self): + # type: () -> None raise NotImplementedError() def add(self, amount): + # type: (float) -> None self.__total += amount def get_total_amount(self): + # type: () -> float return self.__total class StatNone(StatBase): def print_stat(self): + # type: () -> None pass @@ -33,6 +38,7 @@ def __init__(self): super(StatBank, self).__init__() def print_stat(self): + # type: () -> None print("All banks: {:,.2f}".format(self.get_total_amount())) @@ -43,10 +49,12 @@ def __init__(self): self.__next = 0 def add(self, amount, next_amount=0): + # type: (float, float) -> None super(StatCard, self).add(amount) self.__next += next_amount def print_stat(self): + # type: () -> None print("All cards: {:,.2f} (next: {:,.2f})".format(self.get_total_amount(), self.__next)) @@ -56,6 +64,7 @@ def __init__(self): super(StatStockBroker, self).__init__() def print_stat(self): + # type: () -> None print("All stocks: {:,.2f}".format(self.get_total_amount())) @@ -67,11 +76,13 @@ def __init__(self): self.__unvested = 0 def add(self, exercisable, vested=0, unvested=0): + # type: (float, float, float) -> None super(StatWorkStock, self).add(exercisable) self.__vested += vested self.__unvested += unvested def print_stat(self): + # type: () -> None print("All work stocks: {:,.2f} (vested: {:,.2f}, unvested {:,.2f})" .format(self.get_total_amount(), self.__vested, self.__unvested)) @@ -84,22 +95,27 @@ class StatType(Enum): STAT_WORK_STOCK = (4, StatWorkStock) def __init__(self, order, stat_class): + # type: (int, Type[StatBase]) -> None self.order = order self.__stat_class = stat_class def create_stat_class(self): + # type: () -> StatBase return self.__stat_class() class StatsDict(dict): def __getitem__(self, key): + # type: (StatType) -> StatBase if not isinstance(key, StatType): raise Exception("Invalid stats type, must be one of the StatType enum values") return super(StatsDict, self).setdefault(key, key.create_stat_class()) def get_all_stats_ordered(self): + # type: () -> List[StatBase] return [x[1] for x in sorted(self.items(), key=lambda pair: pair[0].order)] def get_total(self): + # type: () -> float return sum([x.get_total_amount() for x in self.values()]) diff --git a/assets/stock_esop.py b/assets/stock_esop.py index 78e2905..be18917 100644 --- a/assets/stock_esop.py +++ b/assets/stock_esop.py @@ -1,7 +1,11 @@ from __future__ import print_function + import json -import requests from datetime import datetime +from typing import Dict, List, Any + +import requests + from .common import WorkStockBase, get_stock_value, convert_usd_to_ils, print_value @@ -11,18 +15,21 @@ class StockEsop(WorkStockBase): PLAN_OBJECT_DATA_LENGTH = 49 def __init__(self, asset_section, gain_tax_percentage=0.28, income_tax_percentage=0.62, **asset_options): + # type: (str, str, str, ...) -> None super(StockEsop, self).__init__(asset_section, **asset_options) self.__gain_tax_percentage = float(gain_tax_percentage) self.__income_tax_percentage = float(income_tax_percentage) self.__plan_data = self.__get_plan_details() def _establish_session(self, username, password): + # type: (str, str) -> requests.Session s = requests.Session() post_data = {"j_username": username, "j_password": password} s.post(self.LOGIN_URL, data=post_data) return s def __get_plan_details(self): + # type: () -> List[Dict[str, float]] # GWT RPC is really bad :( # Here are some docs: https://docs.google.com/document/d/1eG0YocsYYbNAtivkLtcaiEE5IOF5u4LUol8-LL0TIKU/edit @@ -71,6 +78,7 @@ def __get_plan_details(self): return self.__parse_plan_details_object(result_json[3:], result_json[2]) def __parse_plan_details_object(self, object_data, string_table): + # type: (List[Any], List[str]) -> List[Dict[str, float]] assert object_data[0] == 1 assert string_table[object_data[0] - 1] == "cmr.client.main.models.MainContentData/859749487", \ "Unknown response object {}".format(string_table[object_data[0] - 1]) @@ -85,6 +93,7 @@ def __parse_plan_details_object(self, object_data, string_table): return [self.__parse_single_plan_object(x, string_table) for x in array_data] def __parse_single_plan_object(self, object_data, string_table): + # type: (List[Any], List[str]) -> Dict[str, float] assert object_data[0] == 5 assert string_table[object_data[0] - 1] == "cmr.client.main.models.OptionsPlanDetailDataWrapper/457096247", \ "Unknown plan details object {}".format(string_table[object_data[0] - 1]) @@ -119,15 +128,19 @@ def __parse_single_plan_object(self, object_data, string_table): "Unvested": unvested_shares * net_share_value} def __get_total_value(self, value_name): + # type: (str) -> float result = sum([x[value_name] for x in self.__plan_data]) print_value(result, value_name) return result def _get_exercisable(self): + # type: () -> float return self.__get_total_value("Exercisable") def _get_vested(self): + # type: () -> float return self.__get_total_value("Vested") def _get_unvested(self): + # type: () -> float return self.__get_total_value("Unvested") diff --git a/assets/stock_fidelity.py b/assets/stock_fidelity.py index dfbad42..c06d417 100644 --- a/assets/stock_fidelity.py +++ b/assets/stock_fidelity.py @@ -1,6 +1,8 @@ import json import re + import requests + from .common import WorkStockBase, format_value, HEADERS_USER_AGENT @@ -13,6 +15,7 @@ class StockFidelityNetBenefits(WorkStockBase): SUMMARY_URL = "https://netbenefitsww.fidelity.com/mybenefitsww/stockplans/navigation/PlanSummary" def _establish_session(self, username, password): + # type: (str, str) -> requests.Session s = requests.Session() s.get(self.LOGIN_URL, headers=HEADERS_USER_AGENT) @@ -39,12 +42,15 @@ def _establish_session(self, username, password): return s def _get_exercisable(self): + # type: () -> float summary_data_str = self._session.get(self.SUMMARY_URL).text match = re.search(""".+?(.+?)""", summary_data_str) return format_value(match.group(1), "Total") def _get_vested(self): + # type: () -> float return 0 def _get_unvested(self): + # type: () -> float return 0 diff --git a/assets/stock_morgan_stanley.py b/assets/stock_morgan_stanley.py index 870110c..0fd9ceb 100644 --- a/assets/stock_morgan_stanley.py +++ b/assets/stock_morgan_stanley.py @@ -1,5 +1,7 @@ import json + import requests + from .common import WorkStockBase, convert_usd_to_ils, print_value @@ -9,6 +11,7 @@ class MorganStanleyStockPlanConnect(WorkStockBase): SUMMARY_URL = "https://stockplanconnect.morganstanley.com/app-bin/spc/ba/sps/summary?format=json" def __init__(self, asset_section, tax_percentage=0.25, **asset_options): + # type: (str, str, ...) -> None super(MorganStanleyStockPlanConnect, self).__init__(asset_section, **asset_options) self.__tax_percentage = float(tax_percentage) summary_data_str = self._session.get(self.SUMMARY_URL).text @@ -16,6 +19,7 @@ def __init__(self, asset_section, tax_percentage=0.25, **asset_options): self.__summary_data = json.loads(summary_data_str) def _establish_session(self, username, password): + # type: (str, str) -> requests.Session s = requests.Session() s.get(self.LOGIN_URL) post_data = {"username": username, "password": password} @@ -23,7 +27,8 @@ def _establish_session(self, username, password): assert '"success":true' in result, "Result is {}".format(result) return s - def get_summary_value(self, value_name, print_name): + def __get_summary_value(self, value_name, print_name): + # type: (str, str) -> float value_str_raw = self.__summary_data[value_name] value = float(value_str_raw[1:].replace(",", "")) print_value(value, "{} original (USD)".format(print_name)) @@ -32,10 +37,13 @@ def get_summary_value(self, value_name, print_name): return value_ils def _get_exercisable(self): - return self.get_summary_value("totalMktvalue", "Exercisable") + # type: () -> float + return self.__get_summary_value("totalMktvalue", "Exercisable") def _get_vested(self): + # type: () -> float return 0 def _get_unvested(self): - return self.get_summary_value("totalUnvestedvalue", "Unvested") + # type: () -> float + return self.__get_summary_value("totalUnvestedvalue", "Unvested") diff --git a/bank.py b/bank.py index bb8b0ab..1ba8625 100644 --- a/bank.py +++ b/bank.py @@ -2,14 +2,17 @@ # -*- coding: utf-8 -*- from __future__ import print_function + from collections import OrderedDict + +import assets from assets import stats -from assets.common import all_memoize_caches +from assets.common import all_memoize_caches, AssetBase from config import get_config_value, get_asset_sections, get_config_options -import assets def get_asset(asset_section): + # type: (str) -> AssetBase class_name = get_config_value(asset_section, "type") asset_options = get_config_options(asset_section) return getattr(assets, class_name)(asset_section, **asset_options) diff --git a/bank_routine.py b/bank_routine.py index 53bae9b..6fc1714 100644 --- a/bank_routine.py +++ b/bank_routine.py @@ -27,6 +27,7 @@ def wait_until_time(hour, minute): sys.stdout.write("Time remaining: ") sys.stdout.write(time.strftime('%H:%M', time.gmtime(remaining))) sys.stdout.flush() + # noinspection PyTypeChecker time.sleep(min(60, remaining)) sys.stdout.write("\r") sys.stdout.flush() From a784b24f68a7f0ca8e96993390102f826575f6ef Mon Sep 17 00:00:00 2001 From: Avihai Berkovitz Date: Sat, 11 Jan 2020 19:43:35 +0200 Subject: [PATCH 3/3] Returning stats values from get_values() --- assets/bank_beinleumi.py | 13 ++-- assets/bank_discount.py | 13 ++-- assets/bank_leumi.py | 13 ++-- assets/bank_otsar.py | 13 ++-- assets/common.py | 42 ++++++++----- assets/stats.py | 124 +++++++++++++++++++++++++-------------- bank.py | 7 ++- 7 files changed, 138 insertions(+), 87 deletions(-) diff --git a/assets/bank_beinleumi.py b/assets/bank_beinleumi.py index 30540f7..522aa8d 100644 --- a/assets/bank_beinleumi.py +++ b/assets/bank_beinleumi.py @@ -12,7 +12,7 @@ from selenium.webdriver.support.ui import WebDriverWait from . import stats -from .common import BankBase, format_value, print_value +from .common import BankBase, format_value, print_value, AssetValues headers = {"User-Agent": "Mozilla/5.0"} @@ -84,8 +84,8 @@ def _get_stock_value(self): NIA = match_obj.group(1) return format_value(NIA) - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues bank = 0 stock = 0 for account in self._get_accounts(): @@ -94,6 +94,7 @@ def get_values(self, stats_dict): stock += self._get_stock_value() print_value(bank, "OSH") print_value(stock, "NIA") - stats_dict[stats.StatType.STAT_BANK].add(bank) - stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) - return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) + return AssetValues( + OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]), + stats.StatsMapping([stats.StatBank(bank), stats.StatStockBroker(stock)]) + ) diff --git a/assets/bank_discount.py b/assets/bank_discount.py index 346ef6d..3ab8068 100644 --- a/assets/bank_discount.py +++ b/assets/bank_discount.py @@ -8,7 +8,7 @@ from selenium.webdriver.support.ui import WebDriverWait from . import stats -from .common import BankBase, print_value +from .common import BankBase, print_value, AssetValues # username is in format , @@ -46,8 +46,8 @@ def _establish_session(self, username, password): return session - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues accounts_data = self._session.get(self.ACCOUNTS_JSON_URL).json() account_numbers = [account['FormatAccountID'] for account in accounts_data['UserAccountsData']['UserAccounts']] bank = 0 @@ -59,6 +59,7 @@ def get_values(self, stats_dict): 'CurrentSecuritiesPortfolio']['PortfolioValue'] print_value(bank, "OSH") print_value(stock, "NIA") - stats_dict[stats.StatType.STAT_BANK].add(bank) - stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) - return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) + return AssetValues( + OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]), + stats.StatsMapping([stats.StatBank(bank), stats.StatStockBroker(stock)]) + ) diff --git a/assets/bank_leumi.py b/assets/bank_leumi.py index f281cec..1fdb07f 100644 --- a/assets/bank_leumi.py +++ b/assets/bank_leumi.py @@ -8,7 +8,7 @@ import requests from . import stats -from .common import BankBase, HEADERS_USER_AGENT, print_value +from .common import BankBase, HEADERS_USER_AGENT, print_value, AssetValues class BankLeumi(BankBase): @@ -81,14 +81,15 @@ def _establish_session(self, username, password): s.post(self.LOGIN_POST_URL, data=post_data, headers=HEADERS_USER_AGENT) return s - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues checking = self.__total_values['Checking'] holdings = self.__total_values['Holdings'] deposit = self.__total_values['Deposit'] - stats_dict[stats.StatType.STAT_BANK].add(checking + deposit) - stats_dict[stats.StatType.STAT_STOCK_BROKER].add(holdings) - return OrderedDict([("Checking", checking), ("Holdings", holdings), ("Deposit", deposit)]) + return AssetValues( + OrderedDict([("Checking", checking), ("Holdings", holdings), ("Deposit", deposit)]), + stats.StatsMapping([stats.StatBank(checking + deposit), stats.StatStockBroker(holdings)]) + ) def get_total_values(self): # type: () -> Dict[str, float] diff --git a/assets/bank_otsar.py b/assets/bank_otsar.py index 12182c8..d94f54f 100644 --- a/assets/bank_otsar.py +++ b/assets/bank_otsar.py @@ -4,7 +4,7 @@ import requests from . import stats -from .common import BankBase, format_value +from .common import BankBase, format_value, AssetValues class BankOtsar(BankBase): @@ -33,10 +33,11 @@ def _get_stock_value(self): NIA = NIA[-1] return format_value(NIA, 'NIA') - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues bank = self._get_values_from_main_page() stock = self._get_stock_value() - stats_dict[stats.StatType.STAT_BANK].add(bank) - stats_dict[stats.StatType.STAT_STOCK_BROKER].add(stock) - return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) + return AssetValues( + OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]), + stats.StatsMapping([stats.StatBank(bank), stats.StatStockBroker(stock)]) + ) diff --git a/assets/common.py b/assets/common.py index 9cba0ae..a48bb93 100644 --- a/assets/common.py +++ b/assets/common.py @@ -1,9 +1,9 @@ from __future__ import print_function import json +import typing from abc import ABCMeta, abstractmethod, ABC from collections import OrderedDict -from typing import cast import requests @@ -71,12 +71,18 @@ def get_stock_value(stock_name): return float(daily_stats[max(daily_stats.keys())]["1. open"]) +AssetValues = typing.NamedTuple( + 'AssetValues', + [('values', typing.OrderedDict[str, float]), ('stats', stats.StatsMapping)] +) + + class AssetBase(object): __metaclass__ = ABCMeta @abstractmethod - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues raise NotImplementedError() @@ -111,12 +117,14 @@ def _get_next(self): # type: () -> float raise NotImplementedError() - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues credit_value = self._get_credit() card_next = self._get_next() - cast(stats.StatCard, stats_dict[stats.StatType.STAT_CARD]).add(credit_value, card_next) - return OrderedDict([("Credit", credit_value)]) + return AssetValues( + OrderedDict([("Credit", credit_value)]), + stats.StatsMapping([stats.StatCard(credit_value, card_next)]) + ) class WorkStockBase(AuthenticatedAssetBase): @@ -135,13 +143,15 @@ def _get_unvested(self): # type: () -> float raise NotImplementedError() - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues exercisable = self._get_exercisable() vested = self._get_vested() unvested = self._get_unvested() - cast(stats.StatWorkStock, stats_dict[stats.StatType.STAT_WORK_STOCK]).add(exercisable, vested, unvested) - return OrderedDict([("Exercisable", exercisable)]) + return AssetValues( + OrderedDict([("Exercisable", exercisable)]), + stats.StatsMapping([stats.StatWorkStock(exercisable, vested, unvested)]) + ) class CommodityBase(AssetBase): @@ -155,8 +165,10 @@ def _get_value(self): # type: () -> float raise NotImplementedError() - def get_values(self, stats_dict): - # type: (stats.StatsDict) -> OrderedDict[str, float] + def get_values(self): + # type: () -> AssetValues value = self._get_value() - stats_dict[stats.StatType.STAT_NONE].add(value) - return OrderedDict([("Value", value)]) + return AssetValues( + OrderedDict([("Value", value)]), + stats.StatsMapping([stats.StatNone(value)]) + ) diff --git a/assets/stats.py b/assets/stats.py index 28e0e20..7370376 100644 --- a/assets/stats.py +++ b/assets/stats.py @@ -2,31 +2,58 @@ from abc import ABCMeta, abstractmethod from enum import Enum -from typing import List, Type +from typing import List, Iterable + + +class StatType(Enum): + STAT_NONE = 0 + STAT_BANK = 1 + STAT_CARD = 2 + STAT_STOCK_BROKER = 3 + STAT_WORK_STOCK = 4 + + def __init__(self, order): + # type: (int) -> None + self.order = order class StatBase(object): __metaclass__ = ABCMeta - def __init__(self): - self.__total = 0 + def __init__(self, stat_type, total=0): + # type: (StatType, float) -> None + self._total = total + self.__stat_type = stat_type + + def add(self, amount): + # type: (float) -> None + self._total += amount + + def merge(self, other): + # type: (StatBase) -> None + assert isinstance(other, StatBase) + self.add(other.get_total_amount()) @abstractmethod def print_stat(self): # type: () -> None raise NotImplementedError() - def add(self, amount): - # type: (float) -> None - self.__total += amount - def get_total_amount(self): # type: () -> float - return self.__total + return self._total + + def get_stat_type(self): + # type: () -> StatType + return self.__stat_type class StatNone(StatBase): + def __init__(self, total=0): + # type: (float) -> None + super(StatNone, self).__init__(StatType.STAT_NONE, total) + def print_stat(self): # type: () -> None pass @@ -34,8 +61,9 @@ def print_stat(self): class StatBank(StatBase): - def __init__(self): - super(StatBank, self).__init__() + def __init__(self, total=0): + # type: (float) -> None + super(StatBank, self).__init__(StatType.STAT_BANK, total) def print_stat(self): # type: () -> None @@ -44,24 +72,31 @@ def print_stat(self): class StatCard(StatBase): - def __init__(self): - super(StatCard, self).__init__() - self.__next = 0 + def __init__(self, amount=0, next_amount=0): + # type: (float, float) -> None + super(StatCard, self).__init__(StatType.STAT_CARD, amount) + self.__next_amount = next_amount def add(self, amount, next_amount=0): # type: (float, float) -> None super(StatCard, self).add(amount) - self.__next += next_amount + self.__next_amount += next_amount + + def merge(self, other): + # type: (StatCard) -> None + assert isinstance(other, StatCard) + self.add(other._total, other.__next_amount) def print_stat(self): # type: () -> None - print("All cards: {:,.2f} (next: {:,.2f})".format(self.get_total_amount(), self.__next)) + print("All cards: {:,.2f} (next: {:,.2f})".format(self.get_total_amount(), self.__next_amount)) class StatStockBroker(StatBase): - def __init__(self): - super(StatStockBroker, self).__init__() + def __init__(self, total=0): + # type: (float) -> None + super(StatStockBroker, self).__init__(StatType.STAT_STOCK_BROKER, total) def print_stat(self): # type: () -> None @@ -70,10 +105,11 @@ def print_stat(self): class StatWorkStock(StatBase): - def __init__(self): - super(StatWorkStock, self).__init__() - self.__vested = 0 - self.__unvested = 0 + def __init__(self, total=0, vested=0, unvested=0): + # type: (float, float, float) -> None + super(StatWorkStock, self).__init__(StatType.STAT_WORK_STOCK, total) + self.__vested = vested + self.__unvested = unvested def add(self, exercisable, vested=0, unvested=0): # type: (float, float, float) -> None @@ -81,41 +117,39 @@ def add(self, exercisable, vested=0, unvested=0): self.__vested += vested self.__unvested += unvested + def merge(self, other): + # type: (StatWorkStock) -> None + assert isinstance(other, StatWorkStock) + self.add(other._total, other.__vested, other.__unvested) + def print_stat(self): # type: () -> None print("All work stocks: {:,.2f} (vested: {:,.2f}, unvested {:,.2f})" .format(self.get_total_amount(), self.__vested, self.__unvested)) -class StatType(Enum): - STAT_NONE = (0, StatNone) - STAT_BANK = (1, StatBank) - STAT_CARD = (2, StatCard) - STAT_STOCK_BROKER = (3, StatStockBroker) - STAT_WORK_STOCK = (4, StatWorkStock) - - def __init__(self, order, stat_class): - # type: (int, Type[StatBase]) -> None - self.order = order - self.__stat_class = stat_class - - def create_stat_class(self): - # type: () -> StatBase - return self.__stat_class() - +class StatsMapping(object): -class StatsDict(dict): + def __init__(self, stats=None): + # type: (Iterable[StatBase]) -> None + self.__mapping = dict() + if stats: + for x in stats: + self.__mapping[x.get_stat_type()] = x - def __getitem__(self, key): - # type: (StatType) -> StatBase - if not isinstance(key, StatType): - raise Exception("Invalid stats type, must be one of the StatType enum values") - return super(StatsDict, self).setdefault(key, key.create_stat_class()) + def merge(self, other): + # type: (StatsMapping) -> None + for k, v in other.__mapping.items(): + cur_value = self.__mapping.get(k) + if cur_value: + cur_value.merge(v) + else: + self.__mapping[k] = v def get_all_stats_ordered(self): # type: () -> List[StatBase] - return [x[1] for x in sorted(self.items(), key=lambda pair: pair[0].order)] + return [x[1] for x in sorted(self.__mapping.items(), key=lambda pair: pair[0].order)] def get_total(self): # type: () -> float - return sum([x.get_total_amount() for x in self.values()]) + return sum([x.get_total_amount() for x in self.__mapping.values()]) diff --git a/bank.py b/bank.py index 1ba8625..0217b1b 100644 --- a/bank.py +++ b/bank.py @@ -22,16 +22,17 @@ def main(): for cache in all_memoize_caches: cache.clear() all_values = OrderedDict() - all_stats = stats.StatsDict() + all_stats = stats.StatsMapping() asset_sections = get_asset_sections() for asset_section in asset_sections: print("{}:".format(asset_section)) asset = get_asset(asset_section) - values = asset.get_values(all_stats) + values = asset.get_values() values_with_prefix = OrderedDict( - [("{} - {}".format(asset_section, key), value) for key, value in values.items()]) + [("{} - {}".format(asset_section, key), value) for key, value in values.values.items()]) all_values.update(values_with_prefix) + all_stats.merge(values.stats) print()