PK!}[[tdameritrade_cli/__init__.py__version__ = '0.1.0' from tdameritrade_cli import cli from tdameritrade_cli import tools PK!K tdameritrade_cli/cli.pyfrom typing import List import click from tdameritrade_cli.tools.client_factory import ClientFactory from tdameritrade_cli.tools.positions import get_positions from tdameritrade_cli.tools.ods_writer import ODSWriter @click.group(help='CLI to use the TDA API. Before calling a subcommand, you must provide sufficient authentication ' 'information. This can be done with the -a and -o options, or else by writing a separate json file ' 'pointed to by -j. The file should contain acct_number, oauth_user_id, and redirect_uri keys with an ' 'optional token_path. See the docs for TDClient for more information.') @click.pass_context @click.option('--json-config', '-j', type=click.Path(exists=True), help='Path to a json object with configuration options. Supercedes other options.') @click.option('--acct-number', '-a', type=int, help='Account number of TDA account to link through.') @click.option('--oauth-user-id', '-o', help='OAuth user ID for the TDA app you are using to access the API.') @click.option('--redirect-uri', '-r', default='http://127.0.0.1:8080', help='Redirect URI for the TDA app you are using to access the API.') def driver(ctx, json_config, acct_number, oauth_user_id, redirect_uri): def _client_factory(): factory = ClientFactory(json_config, acct_number, oauth_user_id, redirect_uri) if json_config is not None: client = factory.from_json() else: client = factory.from_config() return client ctx.obj = { 'CLIENT_FACTORY': _client_factory } @driver.command(name='list-positions', help='Print the positions data for the specified account.') @click.pass_context def list_positions_cli(ctx): client = ctx.obj['CLIENT_FACTORY']() liq_value, acct_positions = get_positions(client) click.echo(prettify_output(['Identifier', 'Asset Type', 'Market Value'])) for pos in acct_positions: formatted_pos = prettify_output(pos) click.echo(formatted_pos) click.echo(f'Liquidation value: {liq_value}') @driver.command(name='write-positions', help='Write the current positions data to an ods file at SHEET_PATH.') @click.pass_context @click.argument('sheet-path', type=click.Path()) def write_positions_cli(ctx, sheet_path): client = ctx.obj['CLIENT_FACTORY']() liq_value, acct_positions = get_positions(client) ods_writer = ODSWriter(sheet_path) ods_writer.write_positions(liq_value, acct_positions) def prettify_output(output: List[str]) -> str: formatted = f'' for item in output: formatted += f'{item:<20} ' return formatted if __name__ == '__main__': driver() PK!L@"tdameritrade_cli/tools/__init__.pyfrom tdameritrade_cli.tools import ods_writer from tdameritrade_cli.tools import positions from tdameritrade_cli.tools import client_factory PK!>>x x (tdameritrade_cli/tools/client_factory.pyimport json from json.decoder import JSONDecodeError import click from tdameritrade_client.client import TDClient class ClientFactory(object): def __init__(self, json_config: str = None, acct_number: int = None, oauth_user_id: str = None, redirect_uri: str = None): """ A factory of TDClient objects. Must pass either a valid json_config or an (acct_number, oauth_user_id) pair. If not supplied, redirect_uri defaults to http://127.0.0.1:8080. Args: json_config: A json object with acct_number, oauth_user_id, and redirect_uri keys. May also have token_path. acct_number: A TDAmeritrade account number. oauth_user_id: The TDDeveloper app OAuth user ID. redirect_uri: The TDDeveloper app redirect URI. Raises: click.Abort: If no valid configuration is passed. """ if json_config is None and [acct_number, oauth_user_id] == [None, None]: click.echo('Must either pass a json_config or, at minimum, ' \ 'an acct_number and an oauth_user_id. See tda-cli --help.') raise click.Abort() self.json = json_config self.acct_number = acct_number self.oauth_user_id = oauth_user_id self.redirect_uri = redirect_uri def from_json(self): """ Create a TDClient from a json_config Returns: An authenticated TDClient. Raises: IsADirectoryError: Bad path to json_config JSONDecodeError: Bad path to json_config """ try: with open(self.json) as f: config = json.load(f) except IsADirectoryError: raise IsADirectoryError('Path to json_config does not lead to a file!') except JSONDecodeError: raise ValueError('Path to json_config does not lead to a valid json file!') client = TDClient(**config) client.run_auth() return client def from_config(self): """ Create a TDClient from passed parameters Returns: An authenticated TDClient. """ client = TDClient(acct_number=self.acct_number, oauth_user_id=self.oauth_user_id, redirect_uri=self.redirect_uri) client.run_auth() return client PK!Žʎ$tdameritrade_cli/tools/ods_writer.pyimport functools import os from collections import OrderedDict from datetime import date from typing import Callable, List import click from pyexcel_ods import get_data, save_data from tdameritrade_cli import __version__ def check_version(func: Callable) -> Callable: """Context manager that checks the written spreadsheet has a supported version.""" @functools.wraps(func) def wrapper_check_version(*args, **kwargs): try: data = args[0].sheet_data version = data['Cover Sheet'][0][4] maj_min = version.split('.')[:2] assert maj_min == __version__.split('.')[:2], f'Passed document version ({version}) at ' \ f'{args[0].sheet_path} is incompatible with current version of the CLI ({__version__}).' except FileNotFoundError: pass except KeyError: raise KeyError('Document at sheetpath is incompatible with current version of the CLI.') except IndexError: raise IndexError('Document at sheetpath is incompatible with current version of the CLI.') value = func(*args, **kwargs) return value return wrapper_check_version class ODSWriter(object): def __init__(self, sheet_path: str): """ Writes financial data to ods files Args: sheet_path (str): Path to the ods document to either create or append. """ self.sheet_path = sheet_path self._sheet_exists = os.path.isfile(sheet_path) def write_positions(self, liq_value: str, positions: List[List[str]]): """ Write positions data to an ods document. Args: liq_value: The current liquidation value of a TDA portfolio. positions: List[List[str]] where each List[str] is [position_id, position_type, position_value]. """ if not self._sheet_exists: self._write_to_sheet(self._sheet_template) current_data = self._update_cover_sheet(liq_value, positions) new_ids = [pos[0] for pos in positions] existing_ids = [sheet for sheet in current_data][1:] for i, new_id in enumerate(new_ids): if new_id in existing_ids: current_data[new_id] = self.update_position(positions[i], current_data[new_id]) else: current_data.update(self.new_position(positions[i])) click.echo(f'Writing positions information to {self.sheet_path}') self._write_to_sheet(current_data) @check_version def _write_to_sheet(self, data: OrderedDict): save_data(self.sheet_path, data) def _update_cover_sheet(self, liq_value: str, positions: List[List[str]]): """ Update the ods cover sheet. Args: liq_value: The liquidation value of a TD portfolio positions: A list of position lists where each sublist is [position_id, position_type, position_value] Returns: current_book (OrderedDict): The entire ods document. """ current_book = self.sheet_data cover_sheet = current_book['Cover Sheet'] # Update current month with current portfolio value current_month = date.today().strftime('%B') for i, month in enumerate(cover_sheet[1]): if current_month == month: # Write all months after the current as having the present value current_liquidation = [liq_value] * (13-i) cover_sheet[2][i:] = current_liquidation # Update the current positions new_cover_sheet = cover_sheet[:5] for pos in positions: new_cover_sheet.append(pos) current_book['Cover Sheet'] = new_cover_sheet return current_book @property def sheet_data(self): """ The data currently in the sheet located at self.sheet_path. Returns: OrderedDict of data. """ return get_data(self.sheet_path) @property def _sheet_template(self): months = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] sheet_template = OrderedDict() sheet_template.update({'Cover Sheet': [['Investments', f'{date.today().strftime("%Y")}', '', 'Version:', f'{__version__}'], months, ['Liq Value:', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [], ['Current Positions:'], ['ID', 'Type', 'Market Value']] }) return sheet_template @staticmethod def update_position(position: List[str], pos_sheet: List[List]) -> List[List]: """ Update an existing position in an ods document Args: position: List of the form [position_id, position_type, position_value] to update. pos_sheet: The sheet in the ods document describing the existing position. Returns: Updated pos_sheet """ entered_dates = [row[0] for row in pos_sheet][2:] today = date.today().strftime('%x') if today in entered_dates: # Don't update more than once per day return pos_sheet pos_sheet.append([date.today().strftime('%x'), position[2]]) return pos_sheet @staticmethod def new_position(position: List[str]) -> OrderedDict: """ Create a sheet with a new position. Args: position: List of the form [position_id, position_type, position_value] to create. Returns: template (OrderedDict): The new sheet with the first data point given by position included. """ template = OrderedDict() template.update({ position[0]: [ [position[0], ' ', 'type:', position[1]], [' '], [date.today().strftime('%x'), position[2]] ] }) return template PK!=#tdameritrade_cli/tools/positions.pyfrom typing import List, Tuple from tdameritrade_client.client import TDClient def get_positions(client: TDClient) -> Tuple[str, List[List[str]]]: """ Retrieve all positions for the authenticated account. Args: client: An authorized TDClient object. Returns: liq_value (str): The current liquidation value of the account. extracted_data (List[List[str]]): A list of positions where each position is [position_id, position_type, position_value]. """ acct_info = client.get_positions() positions = acct_info['securitiesAccount']['positions'] liq_value = acct_info['securitiesAccount']['currentBalances']['liquidationValue'] extracted_data = [ [pos['instrument']['symbol'], pos['instrument']['assetType'], pos['marketValue']] for pos in positions] return liq_value, extracted_data PK!H571tdameritrade_cli-0.1.0.dist-info/entry_points.txtN+I/N.,()*IIMɴҹE%E)@= J),K-PK!*.22(tdameritrade_cli-0.1.0.dist-info/LICENSECopyright (c) 2018 The Python Packaging Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU&tdameritrade_cli-0.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HH)tdameritrade_cli-0.1.0.dist-info/METADATATQo6~篸r-41j/-P Y:K)R%);zoQ=$C'N(`;r^Y<S)Pǩఠ$J1#y*Gb5.1\LP,Ǐ&qxkkJ,Y_Sqr23.?-aևpB5*2dnk>ʑOf/Z+bY2OXqvKE ]::@5dlmC2%;'eG𙳥ú 7hʖ]S{xʇ Q \~%GËhttNl#n$3ا9#ysN50&0(YtMU)w㦰;#ħ>5! Fh?'UҴTA*HH4X}tEI\z`~[~q ҁţ$~Bjܐǖ>á~o&6hѺLo ,*5 Т2'[ml XQXly~QAPXF)?[Ub#!&YZ.ȉ] r4"U5&7}>\`Xg`ԖCL;%!^Yno[,@b6^-ܽe||I9>BL7)V/;Nbm`h"}f< Z4V˅0Be( KpB DWc NՁ_ݭԝ14r#r_4#oIIPIՌOݪsFܾW-c& l~ ?)Т4O}sOc (U:Ԣ0<ߚS:!%ITmFllI @v9Zy?\mG{n'(43a'䐵ʴzO3K<487ZpiW Yc z+x?ͧy+-7sEhIC^D4˃W;Q~J\<=)_|WoD:+60i<֋"ah3C 3%G]XS9"q1Qzb-s 5)7PK!}[[tdameritrade_cli/__init__.pyPK!K tdameritrade_cli/cli.pyPK!L@" tdameritrade_cli/tools/__init__.pyPK!>>x x (X tdameritrade_cli/tools/client_factory.pyPK!Žʎ$tdameritrade_cli/tools/ods_writer.pyPK!=#.tdameritrade_cli/tools/positions.pyPK!H5712tdameritrade_cli-0.1.0.dist-info/entry_points.txtPK!*.22(@3tdameritrade_cli-0.1.0.dist-info/LICENSEPK!HڽTU&7tdameritrade_cli-0.1.0.dist-info/WHEELPK!HH)P8tdameritrade_cli-0.1.0.dist-info/METADATAPK!H*Z,';tdameritrade_cli-0.1.0.dist-info/RECORDPK =