diff --git a/assets/bank_beinleumi.py b/assets/bank_beinleumi.py index f4d4de8..522aa8d 100644 --- a/assets/bank_beinleumi.py +++ b/assets/bank_beinleumi.py @@ -1,15 +1,18 @@ from __future__ import print_function + +import os import re -import requests from collections import OrderedDict -from .common import BankBase, format_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, AssetValues headers = {"User-Agent": "Mozilla/5.0"} @@ -26,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/") @@ -48,17 +52,22 @@ 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('
', 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) 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: @@ -67,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: @@ -75,12 +85,16 @@ def _get_stock_value(self): return format_value(NIA) def get_values(self): + # type: () -> AssetValues 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)) - return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) + print_value(bank, "OSH") + print_value(stock, "NIA") + 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 1f9dee9..3ab8068 100644 --- a/assets/bank_discount.py +++ b/assets/bank_discount.py @@ -1,12 +1,15 @@ -import re -import requests from collections import OrderedDict -from .common import BankBase, format_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, AssetValues + # username is in format , @@ -21,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 @@ -43,13 +47,19 @@ def _establish_session(self, username, password): return session 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 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)) - return OrderedDict([("Bank", bank), ("Deposit", 0), ("Stock", stock), ("Car", 0)]) + 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") + 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 b9279da..1fdb07f 100644 --- a/assets/bank_leumi.py +++ b/assets/bank_leumi.py @@ -3,8 +3,12 @@ import json import re from collections import OrderedDict +from typing import Dict + import requests -from .common import BankBase, HEADERS_USER_AGENT, print_value + +from . import stats +from .common import BankBase, HEADERS_USER_AGENT, print_value, AssetValues class BankLeumi(BankBase): @@ -13,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 @@ -28,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 = { @@ -69,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'} @@ -76,10 +82,15 @@ def _establish_session(self, username, password): return s def get_values(self): + # type: () -> AssetValues checking = self.__total_values['Checking'] holdings = self.__total_values['Holdings'] deposit = self.__total_values['Deposit'] - 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] return self.__total_values diff --git a/assets/bank_otsar.py b/assets/bank_otsar.py index 29bc0f1..d94f54f 100644 --- a/assets/bank_otsar.py +++ b/assets/bank_otsar.py @@ -1,7 +1,10 @@ import re -import requests from collections import OrderedDict -from .common import BankBase, format_value + +import requests + +from . import stats +from .common import BankBase, format_value, AssetValues class BankOtsar(BankBase): @@ -10,24 +13,31 @@ 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): + # type: () -> AssetValues bank = self._get_values_from_main_page() stock = self._get_stock_value() - 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/card_cal.py b/assets/card_cal.py index 6b5b8ce..67cba2a 100644 --- a/assets/card_cal.py +++ b/assets/card_cal.py @@ -1,13 +1,14 @@ 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 -import time +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): CARD_LOGIN_URL = "https://services.cal-online.co.il/card-holders/Screens/AccountManagement/Login.aspx" @@ -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) @@ -47,21 +51,26 @@ 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): + # 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): + def __get_balance(self, card_code): + # type: (str) -> float 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): + # type: () -> float + 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): + # 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 1fc872f..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) @@ -45,11 +49,13 @@ 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): + # 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): + def _get_next(self): + # type: () -> float return 0 diff --git a/assets/card_leumi.py b/assets/card_leumi.py index 5c213be..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): + def _get_credit(self): + # type: () -> float credit = self.__bank_instance.get_total_values()['Creditcard'] print_value(credit, 'Credit') return credit - def get_next(self): + def _get_next(self): + # type: () -> float return 0 diff --git a/assets/common.py b/assets/common.py index e10ea89..a48bb93 100644 --- a/assets/common.py +++ b/assets/common.py @@ -1,20 +1,26 @@ from __future__ import print_function import json -from abc import ABCMeta, abstractmethod +import typing +from abc import ABCMeta, abstractmethod, ABC +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" } 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) @@ -41,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) @@ -48,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 @@ -62,10 +71,24 @@ 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): + # type: () -> AssetValues + 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: @@ -75,34 +98,77 @@ 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(AssetBase): - @abstractmethod - def get_values(self): - raise NotImplementedError() +class BankBase(AuthenticatedAssetBase, ABC): + pass -class CardBase(AssetBase): +class CardBase(AuthenticatedAssetBase): @abstractmethod - def get_credit(self): + def _get_credit(self): + # type: () -> float raise NotImplementedError() @abstractmethod - def get_next(self): + def _get_next(self): + # type: () -> float raise NotImplementedError() + def get_values(self): + # type: () -> AssetValues + credit_value = self._get_credit() + card_next = self._get_next() + return AssetValues( + OrderedDict([("Credit", credit_value)]), + stats.StatsMapping([stats.StatCard(credit_value, card_next)]) + ) + + +class WorkStockBase(AuthenticatedAssetBase): + @abstractmethod + def _get_exercisable(self): + # type: () -> float + raise NotImplementedError() -class StockBrokerBase(AssetBase): @abstractmethod - def get_exercisable(self): + def _get_vested(self): + # type: () -> float raise NotImplementedError() @abstractmethod - def get_vested(self): + def _get_unvested(self): + # type: () -> float raise NotImplementedError() + def get_values(self): + # type: () -> AssetValues + exercisable = self._get_exercisable() + vested = self._get_vested() + unvested = self._get_unvested() + return AssetValues( + OrderedDict([("Exercisable", exercisable)]), + stats.StatsMapping([stats.StatWorkStock(exercisable, vested, unvested)]) + ) + + +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_unvested(self): + def _get_value(self): + # type: () -> float raise NotImplementedError() + + def get_values(self): + # type: () -> AssetValues + value = self._get_value() + return AssetValues( + OrderedDict([("Value", value)]), + stats.StatsMapping([stats.StatNone(value)]) + ) diff --git a/assets/stats.py b/assets/stats.py new file mode 100644 index 0000000..7370376 --- /dev/null +++ b/assets/stats.py @@ -0,0 +1,155 @@ +from __future__ import print_function + +from abc import ABCMeta, abstractmethod +from enum import Enum +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, 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 get_total_amount(self): + # type: () -> float + 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 + + +class StatBank(StatBase): + + def __init__(self, total=0): + # type: (float) -> None + super(StatBank, self).__init__(StatType.STAT_BANK, total) + + def print_stat(self): + # type: () -> None + print("All banks: {:,.2f}".format(self.get_total_amount())) + + +class StatCard(StatBase): + + 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_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_amount)) + + +class StatStockBroker(StatBase): + + def __init__(self, total=0): + # type: (float) -> None + super(StatStockBroker, self).__init__(StatType.STAT_STOCK_BROKER, total) + + def print_stat(self): + # type: () -> None + print("All stocks: {:,.2f}".format(self.get_total_amount())) + + +class StatWorkStock(StatBase): + + 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 + super(StatWorkStock, self).add(exercisable) + 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 StatsMapping(object): + + 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 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.__mapping.items(), key=lambda pair: pair[0].order)] + + def get_total(self): + # type: () -> float + return sum([x.get_total_amount() for x in self.__mapping.values()]) diff --git a/assets/stock_esop.py b/assets/stock_esop.py index 6324bff..be18917 100644 --- a/assets/stock_esop.py +++ b/assets/stock_esop.py @@ -1,28 +1,35 @@ from __future__ import print_function + import json -import requests from datetime import datetime -from .common import StockBrokerBase, get_stock_value, convert_usd_to_ils, print_value +from typing import Dict, List, Any + +import requests + +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 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): + def _get_exercisable(self): + # type: () -> float return self.__get_total_value("Exercisable") - def get_vested(self): + def _get_vested(self): + # type: () -> float return self.__get_total_value("Vested") - def get_unvested(self): + 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 353d08b..c06d417 100644 --- a/assets/stock_fidelity.py +++ b/assets/stock_fidelity.py @@ -1,10 +1,12 @@ 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" @@ -13,6 +15,7 @@ class StockFidelityNetBenefits(StockBrokerBase): 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) @@ -38,13 +41,16 @@ def _establish_session(self, username, password): return s - def get_exercisable(self): + 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): + def _get_vested(self): + # type: () -> float return 0 - def get_unvested(self): + def _get_unvested(self): + # type: () -> float return 0 diff --git a/assets/stock_morgan_stanley.py b/assets/stock_morgan_stanley.py index 0b3039a..0fd9ceb 100644 --- a/assets/stock_morgan_stanley.py +++ b/assets/stock_morgan_stanley.py @@ -1,14 +1,17 @@ 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" 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)) @@ -31,11 +36,14 @@ def get_summary_value(self, value_name, print_name): print_value(value_ils, "{} final".format(print_name)) return value_ils - def get_exercisable(self): - return self.get_summary_value("totalMktvalue", "Exercisable") + def _get_exercisable(self): + # type: () -> float + return self.__get_summary_value("totalMktvalue", "Exercisable") - def get_vested(self): + def _get_vested(self): + # type: () -> float return 0 - def get_unvested(self): - return self.get_summary_value("totalUnvestedvalue", "Unvested") + def _get_unvested(self): + # type: () -> float + return self.__get_summary_value("totalUnvestedvalue", "Unvested") diff --git a/bank.py b/bank.py index bb5429e..0217b1b 100644 --- a/bank.py +++ b/bank.py @@ -2,13 +2,17 @@ # -*- coding: utf-8 -*- from __future__ import print_function + from collections import OrderedDict -from assets.common import BankBase, CardBase, StockBrokerBase, all_memoize_caches -from config import get_config_value, get_asset_sections, get_config_options + import assets +from assets import stats +from assets.common import all_memoize_caches, AssetBase +from config import get_config_value, get_asset_sections, get_config_options 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) @@ -18,50 +22,25 @@ def main(): for cache in all_memoize_caches: cache.clear() all_values = OrderedDict() + all_stats = stats.StatsMapping() - 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() 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() - 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 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()