From 185afbf99c6aff9de56a4dc1a735d91b7558900b Mon Sep 17 00:00:00 2001 From: Robert-Zacchigna Date: Sat, 6 Jan 2024 17:31:26 -0600 Subject: [PATCH 1/4] decompress and lint kwargs (except for order.py), general formatting changes --- .bumpversion.cfg | 2 +- README.md | 2 +- pyetrade/__init__.py | 2 +- pyetrade/accounts.py | 130 +++++++++++++++++++++++++++----------- pyetrade/alerts.py | 33 +++++----- pyetrade/authorization.py | 31 ++++----- pyetrade/market.py | 53 +++++++++------- pyetrade/order.py | 19 ++---- setup.py | 2 +- tests/test_accounts.py | 89 ++++++++++++++------------ tests/test_alerts.py | 40 ++++++------ tests/test_market.py | 1 + tests/test_order.py | 11 +++- tox.ini | 3 + 14 files changed, 244 insertions(+), 174 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 638c35d..8e2afea 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.1 +current_version = 1.4.2 commit = True tag = True diff --git a/README.md b/README.md index 4b5a720..9c6fa4c 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ print(accounts.list_accounts()) ## Documentation [PyEtrade Documentation](https://pyetrade.readthedocs.io/en/latest/) ## Contribute to pyetrade -* [ETrade API Docs](https://developer.etrade.com/ctnt/dev-portal/getArticleByCategory?category=Documentation) +* [ETrade API Docs](https://apisb.etrade.com/docs/api/account/api-account-v1.html) * Fork pyetrade * Development Setup: ``` diff --git a/pyetrade/__init__.py b/pyetrade/__init__.py index 00567fb..d63702f 100644 --- a/pyetrade/__init__.py +++ b/pyetrade/__init__.py @@ -1,5 +1,5 @@ """Init for pyetrade module """ -__version__ = "1.4.1" +__version__ = "1.4.2" from . import authorization # noqa: F401 from .authorization import ETradeOAuth, ETradeAccessManager # noqa: F401 diff --git a/pyetrade/accounts.py b/pyetrade/accounts.py index 613c226..48f8892 100644 --- a/pyetrade/accounts.py +++ b/pyetrade/accounts.py @@ -1,7 +1,6 @@ """Accounts - ETrade Accounts API Calls TODO: - * Fix init doc string * Check request response for error """ @@ -29,16 +28,15 @@ class ETradeAccounts(object): :param dev: Defines Sandbox (True) or Live (False) ETrade, defaults to True :type dev: bool, optional :EtradeRef: https://apisb.etrade.com/docs/api/account/api-account-v1.html - """ def __init__( self, - client_key, - client_secret, - resource_owner_key, - resource_owner_secret, - dev=True, + client_key: str, + client_secret: str, + resource_owner_key: str, + resource_owner_secret: str, + dev: bool = True, ): """__init_() """ @@ -80,13 +78,15 @@ def list_accounts(self, resp_format="xml") -> dict: return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() - def get_account_balance( - self, account_id_key: str, account_type=None, real_time=True, resp_format="xml" - ) -> dict: - """:description: Retrieves account balanace for an account + def get_account_balance(self, account_id_key: str, account_type: str = None, real_time: bool = True, resp_format="xml") -> dict: # noqa: E501 + """:description: Retrieves account balance for an account - :param account_id_key: AccountIDkey retrived from :class:`list_accounts` + :param account_id_key: AccountIDkey retrieved from :class:`list_accounts` :type account_id_key: str, required + :param account_type: The registered account type, defaults to None + :type account_type: str, optional + :param real_time: Use real time balance or not, defaults to True + :type real_time: bool, optional :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional :return: Balance of account with key ``account_id_key`` @@ -99,7 +99,9 @@ def get_account_balance( account_id_key, ".json" if resp_format == "json" else "", ) + payload = {"realTimeNAV": real_time, "instType": "BROKERAGE"} + if account_type: payload["accountType"] = account_type @@ -111,16 +113,40 @@ def get_account_balance( return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() def get_account_portfolio( - self, account_id_key: str, resp_format="xml", **kwargs + self, + account_id_key: str, + count: int = 50, + sort_by: str = None, + sort_order: str = "DESC", + page_number: int = None, + market_session: str = "REGULAR", + totals_required: bool = False, + lots_required: bool = False, + view: str = "QUICK", + resp_format="xml" ) -> dict: """:description: Retrieves account portfolio for an account - :param account_id_key: AccountIDkey retrived from :class:`list_accounts` + :param account_id_key: AccountIDkey retrieved from :class:`list_accounts` :type account_id_key: str, required + :param count: The number of positions to return in the response, defaults to 50 + :type count: int, optional + :param sort_by: Sorting done based on the column specified in the query parameter. + :type sort_by: str, optional + :param sort_order: Sort orders (ASC or DESC), defaults to DESC + :type sort_order: str, optional + :param page_number: The specific page that in the list that is to be returned. Each page has a default count of 50 positions. # noqa: E501 + :type page_number: int, optional + :param market_session: The market session (Regular or Extended), defaults to REGULAR + :type market_session: str, optional + :param totals_required: It gives the total values of the portfolio, defaults to False + :type totals_required: bool, optional + :param lots_required: It gives position lots for positions, defaults to False + :type lots_required: bool, optional + :param view: The view query: PERFORMANCE, FUNDAMENTAL, OPTIONSWATCH, QUICK, COMPLETE. Defaults to QUICK. + :type view: str, optional :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional - :param kwargs: Parameters for api - :type kwargs: ``**kwargs``, optional :return: Account portfolio of account with key ``account_id_key`` :rtype: xml or json based on ``resp_format`` :EtradeRef: https://apisb.etrade.com/docs/api/account/api-portfolio-v1.html @@ -133,24 +159,50 @@ def get_account_portfolio( ".json" if resp_format == "json" else "", ) + payload = { + "count": count, + "sortBy": sort_by, + "sortOrder": sort_order, + "pageNumber": page_number, + "marketSession": market_session, + "totalsRequired": totals_required, + "lotsRequired": lots_required, + "view": view + } + LOGGER.debug(api_url) - req = self.session.get(api_url, params=kwargs) + req = self.session.get(api_url, params=payload) req.raise_for_status() LOGGER.debug(req.text) return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() def list_transactions( - self, account_id_key: str, resp_format="xml", **kwargs + self, + account_id_key: str, + start_date: str = None, + end_date: str = None, + sort_order: str = "DESC", + marker=None, + count: int = 50, + resp_format="xml" ) -> dict: """:description: Retrieves transactions for an account - :param account_id_key: AccountIDKey retrived from :class:`list_accounts` + :param account_id_key: AccountIDKey retrieved from :class:`list_accounts` :type account_id_key: str, required + :param start_date: The earliest date to include in the date range, formatted as MMDDYYYY (history is available for two years), default is None # noqa: E501 + :type start_date: str, optional + :param end_date: The latest date to include in the date range, formatted as MMDDYYYY, default is None + :type end_date: `str, optional + :param sort_order: The sort order request (ASC or DESC), default is DESC + :type sort_order: str, optional + :param marker: Specifies the desired starting point of the set of items to return (used for paging), default is None # noqa: E501 + :type marker: ??, optional + :param count: Number of transactions to return in the response, default is 50 + :type count: int, optional :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional - :param kwargs: Parameters for api - :type kwargs: ``**kwargs``, optional :return: Transactions list for account with key ``account_id_key`` :rtype: xml or json based on ``resp_format`` :EtradeRef: https://apisb.etrade.com/docs/api/account/api-transaction-v1.html @@ -162,34 +214,42 @@ def list_transactions( ".json" if resp_format == "json" else "", ) - # Build Payload - payload = kwargs - LOGGER.debug("payload: %s", payload) + payload = { + "startDate": start_date, + "endDate": end_date, + "sortOrder": sort_order, + "marker": marker, + "count": count + } LOGGER.debug(api_url) req = self.session.get(api_url, params=payload) req.raise_for_status() LOGGER.debug(req.text) - return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() + # Depending on when transactions are completed and start/end date + # restrictions, it's possible for the response to return nothing: "" + if req.text == "": + return {} + elif resp_format.lower() == "xml": + return xmltodict.parse(req.text) + else: + return req.json() - def list_transaction_details( - self, account_id_key: str, transaction_id: int, resp_format="xml", **kwargs - ) -> dict: + def list_transaction_details(self, account_id_key: str, transaction_id: int, resp_format="xml", **kwargs) -> dict: """:description: Retrieves transaction details for an account - :param account_id_key: AccountIDKey retrived from :class:`list_accounts` + :param account_id_key: AccountIDKey retrieved from :class:`list_accounts` :type account_id_key: str, required - :param resp_format: Desired Response format, defaults to xml - :type resp_format: str, optional :param transaction_id: Numeric transaction ID obtained from :class:`list_transactions` :type transaction_id: int, required + :param resp_format: Desired Response format, defaults to xml + :type resp_format: str, optional :param kwargs: Parameters for api :type kwargs: ``**kwargs``, optional :return: Transaction Details for ``transaction_id`` for account key ``account_id_key`` :rtype: xml or json based on ``resp_format`` :EtradeRef: https://apisb.etrade.com/docs/api/account/api-transaction-v1.html - """ # Set Env @@ -200,12 +260,8 @@ def list_transaction_details( transaction_id, ) - # Build Payload - payload = kwargs - LOGGER.debug("payload: %s", payload) - LOGGER.debug(api_url) - req = self.session.get(api_url, params=payload) + req = self.session.get(api_url, params=kwargs) req.raise_for_status() LOGGER.debug(req.text) diff --git a/pyetrade/alerts.py b/pyetrade/alerts.py index 3353f3b..1eb11b0 100644 --- a/pyetrade/alerts.py +++ b/pyetrade/alerts.py @@ -1,9 +1,4 @@ """Alerts - ETrade Alerts API - - TODO: - * list_alerts - add args - * list_alert_details - add arg - """ import logging @@ -34,11 +29,11 @@ class ETradeAlerts(object): def __init__( self, - client_key, - client_secret, - resource_owner_key, - resource_owner_secret, - dev=True, + client_key: str, + client_secret: str, + resource_owner_key: str, + resource_owner_secret: str, + dev: bool = True, ): self.client_key = client_key self.client_secret = client_secret @@ -54,9 +49,13 @@ def __init__( signature_type="AUTH_HEADER", ) - def list_alerts(self, resp_format="xml") -> dict: + def list_alerts(self, count: int = 25 <= 300, sort_order: str = "DESC", resp_format="xml") -> dict: """:description: Lists alerts in Etrade + :param count: The alert count, defaults to 25 (max 300) + :type count: int, optional + :param sort_order: Sorting is done based on the createDate (ASC or DESC), defaults to DESC + :type sort_order: str, optional :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional :return: List of alerts @@ -64,20 +63,22 @@ def list_alerts(self, resp_format="xml") -> dict: :EtradeRef: https://apisb.etrade.com/docs/api/user/api-alert-v1.html """ - api_url = "%s%s" % (self.base_url, ".json" if resp_format == "json" else "",) + api_url = "%s%s" % (self.base_url, ".json" if resp_format == "json" else "") LOGGER.debug(api_url) - req = self.session.get(api_url) + req = self.session.get(api_url, params={"count": count, "direction": sort_order}) req.raise_for_status() LOGGER.debug(req.text) return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() - def list_alert_details(self, alert_id, resp_format="xml") -> dict: + def list_alert_details(self, alert_id: int, html_tags: bool = False, resp_format="xml") -> dict: """:description: Provides details for an alert :param alert_id: Alert ID obtained from :class:`list_alerts` :type alert_id: int, required + :param html_tags: The HTML tags on the alert, defaults to false. If set to true, it returns the alert details msgText with html tags. # noqa: E501 + :type html_tags: bool, optional :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional :return: List of alert details @@ -93,13 +94,13 @@ def list_alert_details(self, alert_id, resp_format="xml") -> dict: ) LOGGER.debug(api_url) - req = self.session.get(api_url) + req = self.session.get(api_url, params={"htmlTags": html_tags}) req.raise_for_status() LOGGER.debug(req.text) return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() - def delete_alert(self, alert_id, resp_format="xml"): + def delete_alert(self, alert_id: int, resp_format="xml") -> dict: """:description: Deletes specified alert :param alert_id: Alert ID obtained from :class:`list_alerts` diff --git a/pyetrade/authorization.py b/pyetrade/authorization.py index f4c3f41..bdc600b 100644 --- a/pyetrade/authorization.py +++ b/pyetrade/authorization.py @@ -1,10 +1,9 @@ """Authorization - ETrade Authorization API Calls TODO: - * Lint this messy code * Catch events - """ +""" import logging from requests_oauthlib import OAuth1Session @@ -16,18 +15,17 @@ class ETradeOAuth(object): """:description: Performs authorization for OAuth 1.0a - :param client_key: Client key provided by Etrade - :type client_key: str, required - :param client_secret: Client secret provided by Etrade - :type client_secret: str, required + :param consumer_key: Client key provided by Etrade + :type consumer_key: str, required + :param consumer_secret: Client secret provided by Etrade + :type consumer_secret: str, required :param callback_url: Callback URL passed to OAuth mod, defaults to "oob" :type callback_url: str, optional :EtradeRef: https://apisb.etrade.com/docs/api/authorization/request_token.html """ - def __init__(self, consumer_key, consumer_secret, callback_url="oob"): - + def __init__(self, consumer_key: str, consumer_secret: str, callback_url: str = "oob"): self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.base_url_prod = r"https://api.etrade.com" @@ -39,7 +37,7 @@ def __init__(self, consumer_key, consumer_secret, callback_url="oob"): self.access_token = None self.resource_owner_key = None - def get_request_token(self): + def get_request_token(self) -> str: """:description: Obtains the token URL from Etrade. :param None: Takes no parameters @@ -69,12 +67,11 @@ def get_request_token(self): self.consumer_key, akey["oauth_token"], ) - self.verifier_url = formated_auth_url LOGGER.debug(formated_auth_url) return formated_auth_url - def get_access_token(self, verifier): + def get_access_token(self, verifier: str) -> str: """:description: Obtains access token. Requires token URL from :class:`get_request_token` :param verifier: OAuth Verification Code from Etrade @@ -109,17 +106,13 @@ class ETradeAccessManager(object): """ - def __init__( - self, client_key, client_secret, resource_owner_key, resource_owner_secret - ): + def __init__(self, client_key: str, client_secret: str, resource_owner_key: str, resource_owner_secret: str): self.client_key = client_key self.client_secret = client_secret self.resource_owner_key = resource_owner_key self.resource_owner_secret = resource_owner_secret self.renew_access_token_url = r"https://api.etrade.com/oauth/renew_access_token" - self.revoke_access_token_url = ( - r"https://api.etrade.com/oauth/revoke_access_token" - ) + self.revoke_access_token_url = r"https://api.etrade.com/oauth/revoke_access_token" self.session = OAuth1Session( self.client_key, self.client_secret, @@ -128,7 +121,7 @@ def __init__( signature_type="AUTH_HEADER", ) - def renew_access_token(self): + def renew_access_token(self) -> bool: """:description: Renews access tokens obtained from :class:`ETradeOAuth` :param None: Takes no parameters @@ -143,7 +136,7 @@ def renew_access_token(self): return True - def revoke_access_token(self): + def revoke_access_token(self) -> bool: """:description: Revokes access tokens obtained from :class:`ETradeOAuth` :param None: Takes no parameters diff --git a/pyetrade/market.py b/pyetrade/market.py index bbbe1a9..7b401ae 100644 --- a/pyetrade/market.py +++ b/pyetrade/market.py @@ -7,6 +7,7 @@ import logging import xmltodict +from datetime import datetime from requests_oauthlib import OAuth1Session LOGGER = logging.getLogger(__name__) @@ -32,11 +33,11 @@ class ETradeMarket(object): def __init__( self, - client_key, - client_secret, - resource_owner_key, - resource_owner_secret, - dev=True, + client_key: str, + client_secret: str, + resource_owner_key: str, + resource_owner_secret: str, + dev: bool = True, ): self.client_key = client_key self.client_secret = client_secret @@ -61,7 +62,7 @@ def __str__(self): return "\n".join(ret) def look_up_product(self, search_str: str, resp_format="xml") -> dict: - """:description: Performs a look up product + """:description: Performs a look-up product :param search_str: Full or partial name of the company. :type search_str: str, required @@ -89,10 +90,10 @@ def look_up_product(self, search_str: str, resp_format="xml") -> dict: def get_quote( self, - symbols, - detail_flag=None, - require_earnings_date=None, - skip_mini_options_check=None, + symbols: list, + detail_flag: str = None, + require_earnings_date: str = None, + skip_mini_options_check: str = None, resp_format="xml", ) -> dict: """:description: Get quote data on symbols provided in the list args. @@ -100,7 +101,7 @@ def get_quote( :param symbols: Symbols in list args format. Limit 25. :type symbols: list[], required :param detail_flag: Market fields returned from a quote request, defaults to None - :type detail_flag: enum, optional + :type detail_flag: str, optional :param require_earnings_date: Provides Earnings date if True, defaults to None :type require_earnings_date: str, optional :param skip_mini_options_check: Skips mini options check if True, defaults to None @@ -138,9 +139,11 @@ def get_quote( "mf_detail", None, ) + assert require_earnings_date in (True, False, None) assert skip_mini_options_check in (True, False, None) assert isinstance(symbols, list or tuple) + if len(symbols) > 25: LOGGER.warning( "get_quote asked for %d requests; only first 25 returned" % len(symbols) @@ -155,6 +158,7 @@ def get_quote( args.append("skipMiniOptionsCheck=%s" % str(skip_mini_options_check)) api_url = "%s%s%s" % (self.base_url, "quote/", ",".join(symbols[:25])) + if resp_format.lower() == "json": api_url += ".json" if len(args): @@ -170,13 +174,13 @@ def get_quote( def get_option_chains( self, underlier: str, - expiry_date, - skip_adjusted=None, - chain_type=None, - strike_price_near=None, - no_of_strikes=None, - option_category=None, - price_type=None, + expiry_date: datetime.date, + skip_adjusted: str = None, + chain_type: str = None, + strike_price_near: int = None, + no_of_strikes: int = None, + option_category: str = None, + price_type: str = None, resp_format="xml", ) -> dict: """:description: Returns the option chain information for the @@ -217,10 +221,11 @@ def get_option_chains( :price_type values: * atnm * all - :sampleURL: https://api.etrade.com/v1/market/optionchains?expiryDay=03&expiryMonth=04&expiryYear=2011&chainType=PUT&skipAdjusted=true&symbol=GOOGL + :sampleURL: https://api.etrade.com/v1/market/optionchains?expiryDay=03&expiryMonth=04&expiryYear=2011&chainType=PUT&skipAdjusted=true&symbol=GOOGL # noqa: E501 :EtradeRef: https://apisb.etrade.com/docs/api/market/api-market-v1.html """ + assert chain_type in ("put", "call", "callput", None) assert option_category in ("standard", "all", "mini", None) assert price_type in ("atmn", "all", None) @@ -258,11 +263,11 @@ def get_option_chains( return xmltodict.parse(req.text) if resp_format.lower() == "xml" else req.json() - def get_option_expire_date(self, underlier: str, resp_format="xml") -> dict: + def get_option_expire_date(self, symbol: str, resp_format="xml") -> dict: """:description: Returns a list of dates suitable for structuring an option table display - :param underlier: Market Symbol - :type underlier: str, required + :param symbol: Market Symbol + :type symbol: str, required :param resp_format: Desired Response format, defaults to xml :type resp_format: str, optional :return: Returns expiry of options for symbol @@ -274,13 +279,15 @@ def get_option_expire_date(self, underlier: str, resp_format="xml") -> dict: assert isinstance(resp_format, str) assert resp_format in ["xml", "json"] + api_url = "%s%s" % ( self.base_url, "optionexpiredate" if resp_format.lower() == "xml" else "optionexpiredate.json", ) - payload = {"symbol": underlier, "expiryType": "ALL"} + + payload = {"symbol": symbol, "expiryType": "ALL"} LOGGER.debug(api_url) req = self.session.get(api_url, params=payload) diff --git a/pyetrade/order.py b/pyetrade/order.py index d14c36c..6506aba 100644 --- a/pyetrade/order.py +++ b/pyetrade/order.py @@ -62,7 +62,7 @@ def get_request_result(req: OAuth1Session.request, empty_json: dict, resp_format req_output = xmltodict.parse(req.text) if 'Error' in req_output.keys(): - raise Exception(f'Etrade API Error - Code: {req_output["Error"]["code"]}, Msg: {req_output["Error"]["message"]}') + raise Exception(f'Etrade API Error - Code: {req_output["Error"]["code"]}, Msg: {req_output["Error"]["message"]}') # noqa: E501 else: return req_output @@ -99,7 +99,7 @@ def __str__(self) -> str: return "Missing required parameters" -class ETradeOrder: +class ETradeOrder(object): """:description: Object to perform Orders :param client_key: Client key provided by Etrade @@ -165,17 +165,12 @@ def list_orders(self, account_id_key: str, resp_format: str = "json", **kwargs) if resp_format == "json": api_url += ".json" - # Build Params - params = kwargs - LOGGER.debug("query string params: %s", params) - LOGGER.debug(api_url) - req = self.session.get(api_url, params=params, timeout=self.timeout) + req = self.session.get(api_url, params=kwargs, timeout=self.timeout) return get_request_result(req, {}, resp_format) - def find_option_orders(self, account_id_key: str, symbol: str, call_put: str, - expiry_date: str, strike_price: float) -> list: + def find_option_orders(self, account_id_key: str, symbol: str, call_put: str, expiry_date: str, strike_price: float) -> list: # noqa: E501 """:description: Lists option orders for a specific account ID Key :param account_id_key: AccountIDKey from :class:`pyetrade.accounts.ETradeAccounts.list_accounts` @@ -214,8 +209,8 @@ def check_order(**kwargs): """:description: Check that required params for preview or place order are there and correct (Used internally) - """ + mandatory = [ "accountIdKey", "symbol", @@ -339,7 +334,7 @@ def perform_request(self, method, api_url: str, payload: Union[dict, str], resp_ def preview_equity_order(self, **kwargs) -> dict: """API is used to submit an order request for preview before placing it - :param accountIdKey: AccountIDkey retrived from :class:`list_accounts` + :param accountIdKey: AccountIDkey retrieved from :class:`list_accounts` :type accountIdKey: str, required :param symbol: Market symbol for the security being bought or sold :type symbol: str, required @@ -562,7 +557,7 @@ def place_changed_equity_order(self, **kwargs) -> dict: def cancel_order(self, account_id_key: str, order_num: int, resp_format: str = "xml") -> dict: """:description: Cancels a specific order for a given account - :param account_id_key: AccountIDkey retrived from + :param account_id_key: AccountIDkey retrieved from :class:`pyetrade.accounts.ETradeAccounts.list_accounts` :type account_id_key: str, required :param order_num: Numeric id for this order listed in :class:`list_orders` diff --git a/setup.py b/setup.py index 33bd1fd..03d8b5c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from codecs import open from os import path -__version__ = "1.4.1" +__version__ = "1.4.2" here = path.abspath(path.dirname(__file__)) # Get the long description from the README file diff --git a/tests/test_accounts.py b/tests/test_accounts.py index a8ca589..5fe8716 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -63,34 +63,34 @@ def test_get_account_balance(self, MockOAuthSession): MockOAuthSession().get().text = r" returns " account = accounts.ETradeAccounts("abc123", "xyz123", "abctoken", "xyzsecret") # Test Dev XML - result = account.get_account_balance("12345", resp_format="xml") + result = account.get_account_balance("12345abcd", resp_format="xml") self.assertTrue(isinstance(result, dict)) # Test Dev JSON - result = account.get_account_balance("12345", resp_format="json") + result = account.get_account_balance("12345abcd", resp_format="json") self.assertTrue(isinstance(result, dict)) # Test API URL MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/accounts/12345/balance.json", + "https://apisb.etrade.com/v1/accounts/12345abcd/balance.json", params={"instType": "BROKERAGE", "realTimeNAV": True}, ) account = accounts.ETradeAccounts( "abc123", "xyz123", "abctoken", "xyzsecret", dev=False ) # Test Prod JSON - result = account.get_account_balance("12345", resp_format="json") + result = account.get_account_balance("12345abcd", resp_format="json") self.assertTrue(isinstance(result, dict)) # Test API URL MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/accounts/12345/balance.json", + "https://api.etrade.com/v1/accounts/12345abcd/balance.json", params={"instType": "BROKERAGE", "realTimeNAV": True}, ) # xml prod - result = account.get_account_balance("12345", resp_format="xml") + result = account.get_account_balance("12345abcd", resp_format="xml") self.assertTrue(isinstance(result, dict)) # Test API URL MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/accounts/12345/balance", + "https://api.etrade.com/v1/accounts/12345abcd/balance", params={"instType": "BROKERAGE", "realTimeNAV": True}, ) @@ -107,29 +107,44 @@ def test_get_account_portfolio(self, MockOAuthSession): # Set Mock returns MockOAuthSession().get().json.return_value = {"account": "abc123"} MockOAuthSession().get().text = r" returns " + account = accounts.ETradeAccounts("abc123", "xyz123", "abctoken", "xyzsecret") + default_params = { + 'count': 50, + 'sortBy': None, + 'sortOrder': 'DESC', + 'pageNumber': None, + 'marketSession': 'REGULAR', + 'totalsRequired': False, + 'lotsRequired': False, + 'view': 'QUICK' + } + # Test Dev - result = account.get_account_portfolio("12345") + result = account.get_account_portfolio("12345abcd") self.assertTrue(isinstance(result, dict)) + # Test API URL MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/accounts/12345/portfolio", params={} + "https://apisb.etrade.com/v1/accounts/12345abcd/portfolio", params=default_params ) - result = account.get_account_portfolio("12345", resp_format="json") + result = account.get_account_portfolio("12345abcd", resp_format="json") self.assertTrue(isinstance(result, dict)) + # Test Prod account = accounts.ETradeAccounts( "abc123", "xyz123", "abctoken", "xyzsecret", dev=False ) - result = account.get_account_portfolio("12345") + result = account.get_account_portfolio("12345abcd") self.assertTrue(isinstance(result, dict)) + # Test API URL MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/accounts/12345/portfolio", params={} + "https://api.etrade.com/v1/accounts/12345abcd/portfolio", params=default_params ) self.assertTrue(MockOAuthSession().get().json.called) self.assertTrue(MockOAuthSession().get.called) - result = account.get_account_portfolio("12345", resp_format="xml") + result = account.get_account_portfolio("12345abcd", resp_format="xml") self.assertTrue(isinstance(result, dict)) # Mock out OAuth1Session @@ -142,51 +157,45 @@ def test_list_transactions(self, MockOAuthSession): # Set Mock returns MockOAuthSession().get().json.return_value = "{'transaction': 'abc123'}" MockOAuthSession().get().text = r" returns " + account = accounts.ETradeAccounts("abc123", "xyz123", "abctoken", "xyzsecret") + default_params = {'startDate': None, 'endDate': None, 'sortOrder': 'DESC', 'marker': None, 'count': 50} + # Test Dev JSON self.assertEqual( - account.list_transactions(12345, resp_format="json"), - "{'transaction': 'abc123'}", + account.list_transactions('12345abcd', resp_format="json"), "{'transaction': 'abc123'}" + ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://apisb.etrade.com/v1/accounts/12345/transactions.json"), params={}, + "https://apisb.etrade.com/v1/accounts/12345abcd/transactions.json", params=default_params, ) - # Test Dev XML self.assertEqual( - dict(account.list_transactions(12345, resp_format="xml")), - {"xml": "returns"}, + dict(account.list_transactions('12345abcd', resp_format="xml")), {"xml": "returns"}, ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://apisb.etrade.com/v1/accounts/12345/transactions"), params={}, + "https://apisb.etrade.com/v1/accounts/12345abcd/transactions", params=default_params, ) account = accounts.ETradeAccounts( "abc123", "xyz123", "abctoken", "xyzsecret", dev=False ) # Test Prod JSON self.assertEqual( - account.list_transactions(12345, resp_format="json"), - "{'transaction': 'abc123'}", + account.list_transactions('12345abcd', resp_format="json"), "{'transaction': 'abc123'}", ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://api.etrade.com/v1/accounts/12345/transactions.json"), params={}, + "https://api.etrade.com/v1/accounts/12345abcd/transactions.json", params=default_params, ) # Test Prod XML self.assertEqual( - dict(account.list_transactions(12345, resp_format="xml")), - {"xml": "returns"}, + dict(account.list_transactions('12345abcd', resp_format="xml")), {"xml": "returns"}, ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://api.etrade.com/v1/accounts/12345/transactions"), params={} - ) - # Test optional_args - self.assertEqual( - account.list_transactions(12345, group="WITHDRAWALS", resp_format="json"), - "{'transaction': 'abc123'}", + "https://api.etrade.com/v1/accounts/12345abcd/transactions", params=default_params ) self.assertTrue(MockOAuthSession().get().json.called) self.assertTrue(MockOAuthSession().get.called) @@ -206,42 +215,40 @@ def test_list_transaction_details(self, MockOAuthSession): ) # Test Dev JSON self.assertEqual( - account.list_transaction_details(12345, 67890, resp_format="json"), + account.list_transaction_details('12345abcd', 67890, resp_format="json"), "{'transaction': 'abc123'}", ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://apisb.etrade.com/v1/accounts" "/12345/transactions.json/67890"), - params={}, + "https://apisb.etrade.com/v1/accounts" "/12345abcd/transactions.json/67890", params={}, ) # Test Dev XML self.assertEqual( - dict(account.list_transaction_details(12345, 67890, resp_format="xml")), + dict(account.list_transaction_details('12345abcd', 67890, resp_format="xml")), {"xml": "returns"}, ) MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/accounts/12345/transactions/67890", params={}, + "https://apisb.etrade.com/v1/accounts/12345abcd/transactions/67890", params={}, ) account = accounts.ETradeAccounts( "abc123", "xyz123", "abctoken", "xyzsecret", dev=False ) # Test Prod JSON self.assertEqual( - account.list_transaction_details(12345, 67890, resp_format="json"), + account.list_transaction_details('12345abcd', 67890, resp_format="json"), "{'transaction': 'abc123'}", ) # Test API URL MockOAuthSession().get.assert_called_with( - ("https://api.etrade.com/v1/accounts/12345/transactions.json/67890"), - params={}, + "https://api.etrade.com/v1/accounts/12345abcd/transactions.json/67890", params={}, ) # Test Prod XML self.assertEqual( - dict(account.list_transaction_details(12345, 67890, resp_format="xml")), + dict(account.list_transaction_details('12345abcd', 67890, resp_format="xml")), {"xml": "returns"}, ) MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/accounts/12345/transactions/67890", params={}, + "https://api.etrade.com/v1/accounts/12345abcd/transactions/67890", params={}, ) self.assertTrue(MockOAuthSession().get().json.called) diff --git a/tests/test_alerts.py b/tests/test_alerts.py index 5e7899f..6523638 100644 --- a/tests/test_alerts.py +++ b/tests/test_alerts.py @@ -11,27 +11,29 @@ def test_list_alerts(mocker): # Set Mock returns MockOAuthSession().get().json.return_value = "{'alert': 'abc123'}" MockOAuthSession().get().text = r" returns " + alert = alerts.ETradeAlerts("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) + default_params = {'count': True, 'direction': 'DESC'} + # Test Dev JSON assert alert.list_alerts(resp_format="json") == "{'alert': 'abc123'}" # Test API URL - MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/user/alerts.json" - ) + MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts.json", params=default_params) + # Test Dev XML assert dict(alert.list_alerts(resp_format="xml")) == {"xml": "returns"} - MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts") + MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts", params=default_params) alert = alerts.ETradeAlerts("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + # Test Prod JSON assert alert.list_alerts(resp_format="json") == "{'alert': 'abc123'}" # Test API URL - MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/user/alerts.json" - ) + MockOAuthSession().get.assert_called_with("https://api.etrade.com/v1/user/alerts.json", params=default_params) # test Prod XML assert alert.list_alerts(resp_format="xml") == {"xml": "returns"} - MockOAuthSession().get.assert_called_with("https://api.etrade.com/v1/user/alerts") + + MockOAuthSession().get.assert_called_with("https://api.etrade.com/v1/user/alerts", params=default_params) assert MockOAuthSession().get().json.called assert MockOAuthSession().get.called @@ -46,31 +48,29 @@ def test_list_alert_details(mocker): # Set Mock returns MockOAuthSession().get().json.return_value = "{'alert': 'abc123'}" MockOAuthSession().get().text = r" returns " + alert = alerts.ETradeAlerts("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) + default_params = {'htmlTags': False} + # Test Dev JSON assert alert.list_alert_details(1234, resp_format="json") == "{'alert': 'abc123'}" # Test API URL - MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/user/alerts.json/1234" - ) + MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts.json/1234", params=default_params) + # Test Dev XML assert dict(alert.list_alert_details(1234, resp_format="xml")) == {"xml": "returns"} - MockOAuthSession().get.assert_called_with( - "https://apisb.etrade.com/v1/user/alerts/1234" - ) + MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts/1234", params=default_params) assert dict(alert.list_alert_details(1234, resp_format="xml")) == {"xml": "returns"} alert = alerts.ETradeAlerts("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) # Test Prod JSON assert alert.list_alert_details(1234, resp_format="json") == "{'alert': 'abc123'}" + # Test API URL - MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/user/alerts.json/1234" - ) + MockOAuthSession().get.assert_called_with("https://api.etrade.com/v1/user/alerts.json/1234", params=default_params) assert dict(alert.list_alert_details(1234, resp_format="xml")) == {"xml": "returns"} - MockOAuthSession().get.assert_called_with( - "https://api.etrade.com/v1/user/alerts/1234" - ) + + MockOAuthSession().get.assert_called_with("https://api.etrade.com/v1/user/alerts/1234", params=default_params) assert MockOAuthSession().get().json.called assert MockOAuthSession().get.called diff --git a/tests/test_market.py b/tests/test_market.py index 1405844..cba8b01 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -7,6 +7,7 @@ from unittest.mock import patch from pyetrade import market + # Mock out OAuth1Session @patch("pyetrade.market.OAuth1Session") def test_look_up_product(MockOAuthSession): diff --git a/tests/test_order.py b/tests/test_order.py index 16ff277..5eccc9d 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -53,9 +53,16 @@ def test_place_equity_order(self, MockOAuthSession): param: MockOAuthSession type: mock.MagicMock description: MagicMock of OAuth1Session""" + # Set Mock returns - MockOAuthSession().post().text = r"321" - orders = order.ETradeOrder("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + MockOAuthSession().post().text = r"321" # noqa: E501 + orders = order.ETradeOrder( + "abc123", + "xyz123", + "abctoken", + "xyzsecret", + dev=False + ) result = orders.place_equity_order( accountIdKey="12345", diff --git a/tox.ini b/tox.ini index 89be69c..0177e70 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,9 @@ [tox] envlist = py3 +[flake8] +max-line-length = 120 + [testenv] deps = -rrequirements_dev.txt From 16f7a78cc3669997d18e01f0a66b8969174dee12 Mon Sep 17 00:00:00 2001 From: Robert-Zacchigna Date: Sat, 6 Jan 2024 17:39:17 -0600 Subject: [PATCH 2/4] minor fixes --- pyetrade/authorization.py | 2 +- tests/test_order.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pyetrade/authorization.py b/pyetrade/authorization.py index bdc600b..3321982 100644 --- a/pyetrade/authorization.py +++ b/pyetrade/authorization.py @@ -71,7 +71,7 @@ def get_request_token(self) -> str: return formated_auth_url - def get_access_token(self, verifier: str) -> str: + def get_access_token(self, verifier: str) -> dict: """:description: Obtains access token. Requires token URL from :class:`get_request_token` :param verifier: OAuth Verification Code from Etrade diff --git a/tests/test_order.py b/tests/test_order.py index 5eccc9d..86e64bf 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -56,13 +56,7 @@ def test_place_equity_order(self, MockOAuthSession): # Set Mock returns MockOAuthSession().post().text = r"321" # noqa: E501 - orders = order.ETradeOrder( - "abc123", - "xyz123", - "abctoken", - "xyzsecret", - dev=False - ) + orders = order.ETradeOrder("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) # noqa: E501 result = orders.place_equity_order( accountIdKey="12345", From 5fea548093cf850c76a46cd0efcaedc41eed4aea Mon Sep 17 00:00:00 2001 From: Robert-Zacchigna Date: Sun, 7 Jan 2024 01:41:43 -0600 Subject: [PATCH 3/4] increase code coverage --- pyetrade/market.py | 21 ++- tests/test_accounts.py | 12 ++ tests/test_alerts.py | 2 +- tests/test_market.py | 357 ++++++++++++++++++++++------------------- 4 files changed, 219 insertions(+), 173 deletions(-) diff --git a/pyetrade/market.py b/pyetrade/market.py index 7b401ae..3c4882d 100644 --- a/pyetrade/market.py +++ b/pyetrade/market.py @@ -130,6 +130,9 @@ def get_quote( """ + if detail_flag is not None: + detail_flag = detail_flag.lower() + assert detail_flag in ( "fundamental", "intraday", @@ -145,9 +148,7 @@ def get_quote( assert isinstance(symbols, list or tuple) if len(symbols) > 25: - LOGGER.warning( - "get_quote asked for %d requests; only first 25 returned" % len(symbols) - ) + LOGGER.warning("get_quote asked for %d requests; only first 25 returned" % len(symbols)) args = list() if detail_flag is not None: @@ -226,9 +227,18 @@ def get_option_chains( """ + if chain_type is not None: + chain_type = chain_type.lower() assert chain_type in ("put", "call", "callput", None) + + if option_category is not None: + option_category = option_category.lower() assert option_category in ("standard", "all", "mini", None) + + if price_type is not None: + price_type = price_type.lower() assert price_type in ("atmn", "all", None) + assert skip_adjusted in (True, False, None) assert isinstance(resp_format, str) @@ -250,10 +260,9 @@ def get_option_chains( args.append("skipAdjusted=%s" % str(skip_adjusted)) if no_of_strikes is not None: args.append("noOfStrikes=%d" % no_of_strikes) + api_url = "%s%s%s" % ( - self.base_url, - "optionchains?" if resp_format.lower() == "xml" else "optionchains.json?", - "&".join(args), + self.base_url, "optionchains?" if resp_format.lower() == "xml" else "optionchains.json?", "&".join(args), ) req = self.session.get(api_url) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 5fe8716..0a1cfd1 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -97,6 +97,18 @@ def test_get_account_balance(self, MockOAuthSession): self.assertTrue(MockOAuthSession().get().json.called) self.assertTrue(MockOAuthSession().get.called) + # Test API URL + result = account.get_account_balance("12345abcd", account_type="TRUST", resp_format="json") + self.assertTrue(isinstance(result, dict)) + + MockOAuthSession().get.assert_called_with( + "https://api.etrade.com/v1/accounts/12345abcd/balance.json", + params={"realTimeNAV": True, "instType": "BROKERAGE", "accountType": "TRUST"}, + ) + + self.assertTrue(MockOAuthSession().get().json.called) + self.assertTrue(MockOAuthSession().get.called) + # Mock out OAuth1Session @patch("pyetrade.accounts.OAuth1Session") def test_get_account_portfolio(self, MockOAuthSession): diff --git a/tests/test_alerts.py b/tests/test_alerts.py index 6523638..8eb75c7 100644 --- a/tests/test_alerts.py +++ b/tests/test_alerts.py @@ -55,7 +55,7 @@ def test_list_alert_details(mocker): # Test Dev JSON assert alert.list_alert_details(1234, resp_format="json") == "{'alert': 'abc123'}" # Test API URL - MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts.json/1234", params=default_params) + MockOAuthSession().get.assert_called_with("https://apisb.etrade.com/v1/user/alerts.json/1234", params=default_params) # noqa: E501 # Test Dev XML assert dict(alert.list_alert_details(1234, resp_format="xml")) == {"xml": "returns"} diff --git a/tests/test_market.py b/tests/test_market.py index cba8b01..106a66a 100644 --- a/tests/test_market.py +++ b/tests/test_market.py @@ -4,174 +4,199 @@ * pyetrade.market fixture """ import datetime as dt +import unittest from unittest.mock import patch from pyetrade import market # Mock out OAuth1Session -@patch("pyetrade.market.OAuth1Session") -def test_look_up_product(MockOAuthSession): - """test_look_up_product(MockOAuthSession) - param: MockOAuthSession - type: mock.MagicMock - description: MagicMock of OAuth1Session - - 3 tests based on resp_format = (None,'xml') - test exception raised when resp_format is something - different from two choices - """ - - response = {"symbol": "MMM", "description": "3M CO COM", "type": "EQUITY"} - XML_response = r""" - - MMM - 3M CO COM - EQUITY - """ - # Set Mock returns for resp_format=xml - MockOAuthSession().get().text = XML_response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - # Test Get Quote returning python dict - resp = mark.look_up_product("mmm") - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # Set Mock returns for resp_format=json - MockOAuthSession().get().json.return_value = response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - # Test Get Quote returning python dict - resp = mark.look_up_product("mmm", resp_format="json") - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - -# Mock out OAuth1Session -@patch("pyetrade.market.OAuth1Session") -def test_get_quote(MockOAuthSession): - """test_get_quote(MockOAuthSession) - param: MockOAuthSession - type: mock.MagicMock - description: MagicMock of OAuth1Session - """ - - response = { - "securityType": "EQ", - "symbol": "MMM", - "dateTimeUTC": 1546545180, - "adjustedFlag": "false", - "annualDividend": 0.0, - "averageVolume": 3078683.0, - } - - XML_response = r""" - - - 14:53:00 EST 01-03-2019 - 1546545180 - - false - 0.0 - 3078683 - - EQ - MMM - - """ - # Set Mock returns for resp_format=None - MockOAuthSession().get().text = XML_response - MockOAuthSession().get().json.return_value = response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - resp = mark.get_quote(["MMM"]) - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - resp = mark.get_quote(["MMM"], resp_format="json") - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # test the assertion failure of detail_flag, requireEarningsDate, - # skipMiniOptionsCheck - - -# Mock out OAuth1Session -@patch("pyetrade.market.OAuth1Session") -def test_get_option_chains(MockOAuthSession): - """test_get_optionexpiredate(MockOAuthSession) - param: MockOAuthSession - type: mock.MagicMock - description: MagicMock of OAuth1Session - """ - - response = {"timeStamp": 1546546266, "bid": 41.55, "OptionGreeks": {"iv": 0.6716}} - XML_response = r""" - - 154654626641.55 - 0.435700 - - """ - - # Set Mock returns for resp_format=xml - MockOAuthSession().get().text = XML_response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - resp = mark.get_option_chains( - "AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml" - ) - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # Set Mock returns for resp_format=xml and dev=True - MockOAuthSession().get().text = XML_response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) - resp = mark.get_option_chains( - "AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml" - ) - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # Set Mock returns for resp_format=json - MockOAuthSession().get().json.return_value = response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - resp = mark.get_option_chains( - "AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="json" - ) - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # Set Mock returns for resp_format=xml - MockOAuthSession().get().json.return_value = response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) - resp = mark.get_option_chains( - "AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml" - ) - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - # test the assertion failure of chainType, optionCategory, - # priceType, skipAdjusted - - -@patch("pyetrade.market.OAuth1Session") -def test_get_option_expire_date(MockOAuthSession): - """test_get_optionexpiredate(MockOAuthSession) - param: MockOAuthSession - type: mock.MagicMock - description: MagicMock of OAuth1Session - """ - - # response = [dt.date(2019, 1, 18), dt.date(2019, 1, 25)] - XML_response = ( - r'' - r"" - ) - # Set Mock returns for resp_format=None - MockOAuthSession().get().text = XML_response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) - resp = mark.get_option_expire_date("AAPL", resp_format="xml") - assert isinstance(resp, dict) - assert MockOAuthSession().get.called - - MockOAuthSession().get().text = XML_response - mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) - resp = mark.get_option_expire_date("AAPL", resp_format="xml") - assert isinstance(resp, dict) - assert MockOAuthSession().get.called +class TestETradeMarket(unittest.TestCase): + """TestEtradeAccounts Unit Test""" + + @patch("pyetrade.market.OAuth1Session") + def test_look_up_product(self, MockOAuthSession): + """test_look_up_product(MockOAuthSession) + param: MockOAuthSession + type: mock.MagicMock + description: MagicMock of OAuth1Session + + 3 tests based on resp_format = (None,'xml') + test exception raised when resp_format is something + different from two choices + """ + + response = {"symbol": "MMM", "description": "3M CO COM", "type": "EQUITY"} + XML_response = r""" + + MMM + 3M CO COM + EQUITY + """ + # Set Mock returns for resp_format=xml + MockOAuthSession().get().text = XML_response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + # Test Get Quote returning python dict + resp = mark.look_up_product("mmm") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Set Mock returns for resp_format=json + MockOAuthSession().get().json.return_value = response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + # Test Get Quote returning python dict + resp = mark.look_up_product("mmm", resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + @patch("pyetrade.market.OAuth1Session") + def test_get_quote(self, MockOAuthSession): + """test_get_quote(MockOAuthSession) + param: MockOAuthSession + type: mock.MagicMock + description: MagicMock of OAuth1Session + """ + + response = { + "securityType": "EQ", + "symbol": "MMM", + "dateTimeUTC": 1546545180, + "adjustedFlag": "false", + "annualDividend": 0.0, + "averageVolume": 3078683.0, + } + + XML_response = r""" + + + 14:53:00 EST 01-03-2019 + 1546545180 + + false + 0.0 + 3078683 + + EQ + MMM + + """ + # Set Mock returns for resp_format=None + MockOAuthSession().get().text = XML_response + MockOAuthSession().get().json.return_value = response + + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + + # Test XML return + resp = mark.get_quote(["MMM"]) + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Test JSON return + resp = mark.get_quote(["MMM"], resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Test list of symbols greater than 25 + resp = mark.get_quote(["MMM"]*26, resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Test list of symbols greater than 25 + resp = mark.get_quote(["MMM"] * 26, resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Test detail_flag, requireEarningsDate, skipMiniOptionsCheck + resp = mark.get_quote(["MMM"], detail_flag="ALL", require_earnings_date=True, + skip_mini_options_check=True, resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + MockOAuthSession().get.assert_called_with( + "https://api.etrade.com/v1/market/quote/MMM.json?detailflag=ALL&requireEarningsDate=true&skipMiniOptionsCheck=True" # noqa: E501 + ) + self.assertTrue(MockOAuthSession().get().json.called) + self.assertTrue(MockOAuthSession().get.called) + + # test the assertion failure of detail_flag, requireEarningsDate, + # skipMiniOptionsCheck + + @patch("pyetrade.market.OAuth1Session") + def test_get_option_chains(self, MockOAuthSession): + """test_get_optionexpiredate(MockOAuthSession) + param: MockOAuthSession + type: mock.MagicMock + description: MagicMock of OAuth1Session + """ + + response = {"timeStamp": 1546546266, "bid": 41.55, "OptionGreeks": {"iv": 0.6716}} + XML_response = r""" + + 154654626641.55 + 0.435700 + + """ + + # Set Mock returns for resp_format=xml + MockOAuthSession().get().text = XML_response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + resp = mark.get_option_chains("AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Set Mock returns for resp_format=xml and dev=True + MockOAuthSession().get().text = XML_response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) + resp = mark.get_option_chains("AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Set Mock returns for resp_format=xml + MockOAuthSession().get().json.return_value = response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) + resp = mark.get_option_chains("AAPL", expiry_date=dt.date(2019, 2, 15), resp_format="xml") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + # Set Mock returns for resp_format=json + MockOAuthSession().get().json.return_value = response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + resp = mark.get_option_chains("AAPL", expiry_date=dt.date(2019, 2, 15), + strike_price_near=100, chain_type="CALL", option_category="ALL", + price_type="ALL", skip_adjusted=False, no_of_strikes=5, resp_format="json") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + MockOAuthSession().get.assert_called_with( + "https://api.etrade.com/v1/market/optionchains.json?symbol=AAPL&expiryDay=15&expiryMonth=02&expiryYear=2019&strikePriceNear=100.00&chainType=CALL&optionCategory=ALL&priceType=ALL&skipAdjusted=False&noOfStrikes=5" # noqa: E501 + ) + self.assertTrue(MockOAuthSession().get().json.called) + self.assertTrue(MockOAuthSession().get.called) + + # test the assertion failure of chainType, optionCategory, + # priceType, skipAdjusted + + @patch("pyetrade.market.OAuth1Session") + def test_get_option_expire_date(self, MockOAuthSession): + """test_get_optionexpiredate(MockOAuthSession) + param: MockOAuthSession + type: mock.MagicMock + description: MagicMock of OAuth1Session + """ + + # response = [dt.date(2019, 1, 18), dt.date(2019, 1, 25)] + XML_response = ( + r'' + r"" + ) + # Set Mock returns for resp_format=None + MockOAuthSession().get().text = XML_response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=False) + resp = mark.get_option_expire_date("AAPL", resp_format="xml") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called + + MockOAuthSession().get().text = XML_response + mark = market.ETradeMarket("abc123", "xyz123", "abctoken", "xyzsecret", dev=True) + resp = mark.get_option_expire_date("AAPL", resp_format="xml") + assert isinstance(resp, dict) + assert MockOAuthSession().get.called From 21050b43173f68e1a1f6b7d9139b97c5c29c7cfa Mon Sep 17 00:00:00 2001 From: Robert-Zacchigna Date: Mon, 18 Mar 2024 23:42:29 -0500 Subject: [PATCH 4/4] revert version bump --- .bumpversion.cfg | 2 +- pyetrade/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8e2afea..638c35d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.2 +current_version = 1.4.1 commit = True tag = True diff --git a/pyetrade/__init__.py b/pyetrade/__init__.py index d63702f..00567fb 100644 --- a/pyetrade/__init__.py +++ b/pyetrade/__init__.py @@ -1,5 +1,5 @@ """Init for pyetrade module """ -__version__ = "1.4.2" +__version__ = "1.4.1" from . import authorization # noqa: F401 from .authorization import ETradeOAuth, ETradeAccessManager # noqa: F401 diff --git a/setup.py b/setup.py index 03d8b5c..33bd1fd 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from codecs import open from os import path -__version__ = "1.4.2" +__version__ = "1.4.1" here = path.abspath(path.dirname(__file__)) # Get the long description from the README file