From fddd1d498434d556b9bece313114035e1445256a Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 14:15:35 -0700 Subject: [PATCH 1/6] use backoff to refresh tokens --- cschwabpy/SchwabAsyncClient.py | 5 +-- cschwabpy/SchwabClient.py | 3 +- poetry.lock | 72 ++++++++++++++++++++++------------ pyproject.toml | 2 + requirements.txt | 2 + 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/cschwabpy/SchwabAsyncClient.py b/cschwabpy/SchwabAsyncClient.py index 7b2495a..dd8bf45 100644 --- a/cschwabpy/SchwabAsyncClient.py +++ b/cschwabpy/SchwabAsyncClient.py @@ -28,7 +28,7 @@ SCHWAB_AUTH_PATH, SCHWAB_TOKEN_PATH, ) - +import backoff import httpx import re import base64 @@ -64,15 +64,14 @@ def __init__( def token_url(self) -> str: return f"{SCHWAB_API_BASE_URL}/{SCHWAB_TOKEN_PATH}" + @backoff.on_exception(backoff.expo, Exception, max_tries=3, max_time=10) async def _ensure_valid_access_token(self, force_refresh: bool = False) -> bool: if self.__tokens is None: raise Exception( "Tokens are not available. Please use get_tokens_manually() to get tokens first." ) - if self.__tokens.is_access_token_valid and not force_refresh: return True - client = httpx.AsyncClient() if self.__client is None else self.__client try: key_sec_encoded = self.__encode_app_key_secret() diff --git a/cschwabpy/SchwabClient.py b/cschwabpy/SchwabClient.py index b3ba881..4c59a46 100644 --- a/cschwabpy/SchwabClient.py +++ b/cschwabpy/SchwabClient.py @@ -18,7 +18,7 @@ InstrumentProjection, ) import cschwabpy.util as util - +import backoff from datetime import datetime, timedelta from typing import Optional, List, Mapping from cschwabpy.costants import ( @@ -66,6 +66,7 @@ def __init__( def token_url(self) -> str: return f"{SCHWAB_API_BASE_URL}/{SCHWAB_TOKEN_PATH}" + @backoff.on_exception(backoff.expo, Exception, max_tries=3, max_time=10) def _ensure_valid_access_token(self, force_refresh: bool = False) -> bool: if self.__tokens is None: raise Exception( diff --git a/poetry.lock b/poetry.lock index e148a0a..4d08642 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,17 @@ # This file is automatically @generated by Poetry and should not be changed by hand. +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -55,6 +67,18 @@ types-python-dateutil = ">=2.8.10" doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (>=1.0.0,<2.0.0)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (>=3.0.0,<4.0.0)"] +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + [[package]] name = "certifi" version = "2024.6.2" @@ -171,14 +195,14 @@ trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.25.2" description = "The next generation HTTP client." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] [package.dependencies] @@ -558,14 +582,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "8.2.1" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -573,49 +597,49 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.7" +version = "0.21.2" description = "Pytest support for asyncio" category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, - {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, ] [package.dependencies] -pytest = ">=7.0.0,<9" +pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-httpx" -version = "0.30.0" +version = "0.26.0" description = "Send responses to httpx." category = "main" optional = false python-versions = ">=3.9" files = [ - {file = "pytest-httpx-0.30.0.tar.gz", hash = "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a"}, - {file = "pytest_httpx-0.30.0-py3-none-any.whl", hash = "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c"}, + {file = "pytest_httpx-0.26.0-py3-none-any.whl", hash = "sha256:ca372b94c569c0aca2f06240f6f78cc223dfbc3ab97b5700d4e14c9a73eab17a"}, + {file = "pytest_httpx-0.26.0.tar.gz", hash = "sha256:b489c5a7bb847551943eaee601bc35053b35dc4f5961c944305120f14a1d770a"}, ] [package.dependencies] -httpx = ">=0.27.0,<0.28.0" -pytest = ">=7,<9" +httpx = ">=0.25.0,<0.26.0" +pytest = ">=7.0.0,<8.0.0" [package.extras] -testing = ["pytest-asyncio (>=0.23.0,<0.24.0)", "pytest-cov (>=4.0.0,<5.0.0)"] +testing = ["pytest-asyncio (>=0.21.0,<0.22.0)", "pytest-cov (>=4.0.0,<5.0.0)"] [[package]] name = "python-dateutil" @@ -812,5 +836,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.10.13" -content-hash = "9deb63b41b939feaddb1177ef43ae94fe165499919bffd5facd5c7da7ed09bc2" +python-versions = "^3.9" +content-hash = "cff2db69c2f9f031b6c739a8a01294ecdde541433612b59bfda4125ea360f26d" diff --git a/pyproject.toml b/pyproject.toml index 9c16dbd..b20a949 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ numpy = "^1.26.4" pytz = "^2024.1" arrow = "^1.3.0" pytest-httpx = "^0.26.0" +aiofiles = "^24.1.0" +backoff = "^2.2.1" [build-system] diff --git a/requirements.txt b/requirements.txt index f5bef22..7dcee74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +aiofiles +backoff httpx pytest pytest-httpx From 9c83d372caaf20c2db2a490fb1bb23b08e112369 Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 14:40:34 -0700 Subject: [PATCH 2/6] adding async local token store --- .gitignore | 2 +- cschwabpy/SchwabAsyncClient.py | 17 ++++++------- cschwabpy/SchwabClient.py | 11 +++----- cschwabpy/models/token.py | 46 ++++++++++++++++++++++++++++++++++ tests/test_models.py | 23 +++++++++-------- 5 files changed, 69 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 763e46c..88880c6 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,4 @@ cython_debug/ notebooks *.log data-logs/ -*tokens.json +*tokens*.json diff --git a/cschwabpy/SchwabAsyncClient.py b/cschwabpy/SchwabAsyncClient.py index dd8bf45..d38588a 100644 --- a/cschwabpy/SchwabAsyncClient.py +++ b/cschwabpy/SchwabAsyncClient.py @@ -1,4 +1,4 @@ -from cschwabpy.models.token import Tokens, ITokenStore, LocalTokenStore +from cschwabpy.models.token import Tokens, IAsyncTokenStore, AsyncLocalTokenStore from cschwabpy.models import ( OptionChainQueryFilter, OptionContractType, @@ -42,7 +42,7 @@ def __init__( self, app_client_id: str, app_secret: str, - token_store: ITokenStore = LocalTokenStore(), + token_store: IAsyncTokenStore = AsyncLocalTokenStore(), tokens: Optional[Tokens] = None, http_client: Optional[httpx.AsyncClient] = None, ) -> None: @@ -51,14 +51,7 @@ def __init__( self.__token_store = token_store self.__client = http_client self.__keep_client_alive = http_client is not None - if ( - tokens is not None - and tokens.is_access_token_valid - and tokens.is_refresh_token_valid - ): - token_store.save_tokens(tokens) - - self.__tokens = token_store.get_tokens() + self.__tokens = tokens @property def token_url(self) -> str: @@ -66,6 +59,9 @@ def token_url(self) -> str: @backoff.on_exception(backoff.expo, Exception, max_tries=3, max_time=10) async def _ensure_valid_access_token(self, force_refresh: bool = False) -> bool: + if self.__tokens is None: + self.__tokens = await self.__token_store.get_tokens() + if self.__tokens is None: raise Exception( "Tokens are not available. Please use get_tokens_manually() to get tokens first." @@ -384,6 +380,7 @@ async def download_option_chain_async( url=target_url, params={}, headers=self.__auth_header() ) json_res = response.json() + print("json_res: ", json_res) return OptionChain(**json_res) finally: if not self.__keep_client_alive: diff --git a/cschwabpy/SchwabClient.py b/cschwabpy/SchwabClient.py index 4c59a46..902b778 100644 --- a/cschwabpy/SchwabClient.py +++ b/cschwabpy/SchwabClient.py @@ -53,14 +53,7 @@ def __init__( self.__token_store = token_store self.__client = http_client self.__keep_client_alive = http_client is not None - if ( - tokens is not None - and tokens.is_access_token_valid - and tokens.is_refresh_token_valid - ): - token_store.save_tokens(tokens) - - self.__tokens = token_store.get_tokens() + self.__tokens = tokens @property def token_url(self) -> str: @@ -68,6 +61,8 @@ def token_url(self) -> str: @backoff.on_exception(backoff.expo, Exception, max_tries=3, max_time=10) def _ensure_valid_access_token(self, force_refresh: bool = False) -> bool: + if self.__tokens is None: + self.__tokens = self.__token_store.get_tokens() if self.__tokens is None: raise Exception( "Tokens are not available. Please use get_tokens_manually() to get tokens first." diff --git a/cschwabpy/models/token.py b/cschwabpy/models/token.py index 8ae1c1c..3a8462b 100644 --- a/cschwabpy/models/token.py +++ b/cschwabpy/models/token.py @@ -4,6 +4,7 @@ import os import json import time +import aiofiles as af from pathlib import Path REFRESH_TOKEN_VALIDITY_SECONDS = 7 * 24 * 60 * 60 # 7 days @@ -77,3 +78,48 @@ def get_tokens(self) -> Optional[Tokens]: def save_tokens(self, tokens: Tokens) -> None: with open(self.token_file_path, "w") as token_file: token_file.write(json.dumps(tokens.to_json(), indent=4)) + + +class IAsyncTokenStore(Protocol): + @property + def token_output_path(self) -> str: + """Path for outputting tokens.""" + return "" + + async def get_tokens(self) -> Optional[Tokens]: + pass + + async def save_tokens(self, tokens: Tokens) -> None: + pass + + +class AsyncLocalTokenStore(IAsyncTokenStore): + def __init__( + self, json_file_name: str = "tokens.json", file_path: Optional[str] = None + ): + self.file_name = json_file_name + self.token_file_path = file_path + if file_path is None: + self.token_file_path = Path(Path(__file__).parent, json_file_name) + else: + self.token_file_path = Path(file_path) + + if not os.path.exists(self.token_file_path.parent): + os.makedirs(self.token_file_path.parent) + + @property + def token_output_path(self) -> str: + return str(self.token_file_path) + + async def get_tokens(self) -> Optional[Tokens]: + try: + async with af.open(self.token_file_path, mode="r") as token_file: + token_json_str = await token_file.read() + tokens_json = json.loads(token_json_str) + return Tokens(**tokens_json) + except: + return None + + async def save_tokens(self, tokens: Tokens) -> None: + async with af.open(self.token_file_path, mode="w") as token_file: + await token_file.write(json.dumps(tokens.to_json(), indent=4)) diff --git a/tests/test_models.py b/tests/test_models.py index c03777e..6c3e263 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -38,7 +38,7 @@ AccountType, SecuritiesAccount, ) -from cschwabpy.models.token import Tokens, LocalTokenStore +from cschwabpy.models.token import Tokens, LocalTokenStore, AsyncLocalTokenStore from cschwabpy.SchwabAsyncClient import SchwabAsyncClient from cschwabpy.SchwabClient import SchwabClient @@ -46,6 +46,7 @@ mock_file_name = "mock_schwab_api_resp.json" token_store = LocalTokenStore(json_file_name="test_tokens.json") +async_token_store = AsyncLocalTokenStore(json_file_name="test_tokens_async.json") def mock_account() -> AccountNumberWithHashID: @@ -97,7 +98,7 @@ async def test_market_hours(httpx_mock: HTTPXMock) -> None: cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -182,7 +183,7 @@ async def test_get_order(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -271,7 +272,7 @@ async def test_place_order(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -308,7 +309,7 @@ async def test_cancel_order(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -348,7 +349,7 @@ async def test_get_order_by_id(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -390,7 +391,7 @@ async def test_get_single_account(httpx_mock: HTTPXMock): cschwab_client2 = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -435,7 +436,7 @@ async def test_get_securities_account(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -487,7 +488,7 @@ async def test_download_option_chain(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -552,7 +553,7 @@ async def test_get_option_expirations(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) @@ -602,7 +603,7 @@ async def test_get_account_numbers(httpx_mock: HTTPXMock): cschwab_client = SchwabAsyncClient( app_client_id="fake_id", app_secret="fake_secret", - token_store=token_store, + token_store=async_token_store, tokens=mocked_token, http_client=client, ) From 25ffac36e48802b11250ecd06f0101de4f4d8d6e Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 14:45:40 -0700 Subject: [PATCH 3/6] fix unit tests --- tests/test_models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 6c3e263..58b3172 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -203,6 +203,7 @@ async def test_get_order(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) @@ -286,6 +287,7 @@ async def test_place_order(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) @@ -323,6 +325,7 @@ async def test_cancel_order(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) @@ -365,6 +368,7 @@ async def test_get_order_by_id(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) @@ -409,6 +413,7 @@ async def test_get_single_account(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) single_account2 = cschwab_client2.get_single_account( @@ -457,6 +462,7 @@ async def test_get_securities_account(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) securities_accounts2 = cschwab_client2.get_accounts(include_positions=True) @@ -515,6 +521,7 @@ async def test_download_option_chain(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) opt_chain_result2 = cschwab_client2.download_option_chain( @@ -572,6 +579,7 @@ async def test_get_option_expirations(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) opt_expirations_list2 = cschwab_client2.get_option_expirations( @@ -624,6 +632,7 @@ async def test_get_account_numbers(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, + tokens=mocked_token, http_client=client2, ) account_numbers2 = cschwab_client2.get_account_numbers() From 734e9e2205081bbb875b99f51425e58eeadd9155 Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 14:59:42 -0700 Subject: [PATCH 4/6] fix unit tests2 --- tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index 58b3172..7f204c4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -483,6 +483,7 @@ async def test_download_option_chain(httpx_mock: HTTPXMock): if os.path.exists(Path(token_store.token_output_path)): os.remove(token_store.token_output_path) # clean up before test + token_store.save_tokens(tokens=mocked_token) mock_response = { **mock_option_chain_resp["option_chain_resp"], @@ -521,7 +522,6 @@ async def test_download_option_chain(httpx_mock: HTTPXMock): app_client_id="fake_id", app_secret="fake_secret", token_store=token_store, - tokens=mocked_token, http_client=client2, ) opt_chain_result2 = cschwab_client2.download_option_chain( From 2d57113d9b21d33a03aaec5de8beab157f83d25e Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 15:00:20 -0700 Subject: [PATCH 5/6] bump version --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b20a949..62e44ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cschwabpy" -version = "0.1.3" +version = "0.1.3.1" description = "" authors = ["Tony Wang "] readme = "README.md" diff --git a/setup.py b/setup.py index 5d98dc1..1134ffd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="CSchwabPy", - version="0.1.3", + version="0.1.3.1", description="Charles Schwab Stock & Option Trade API Client for Python.", long_description=long_description, long_description_content_type="text/markdown", From f371282c4f40d8a0d66153a896e3a761988edaa6 Mon Sep 17 00:00:00 2001 From: rcholic Date: Sun, 14 Jul 2024 15:05:41 -0700 Subject: [PATCH 6/6] delete tokens manual method from async client --- README.md | 9 ++--- cschwabpy/SchwabAsyncClient.py | 62 +--------------------------------- 2 files changed, 6 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 06a0534..c570c1c 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,13 @@ pip install CSchwabPy ```python # save these lines in a file named like cschwab.py -from cschwabpy.SchwabAsyncClient import SchwabAsyncClient +# NOTE: should use SchwabClient to get tokens manually after version 0.1.3 +from cschwabpy.SchwabClient import SchwabClient app_client_key = "---your-app-client-key-here-" app_secret = "app-secret" -schwab_client = SchwabAsyncClient(app_client_id=app_client_key, app_secret=app_secret) +schwab_client = SchwabClient(app_client_id=app_client_key, app_secret=app_secret) schwab_client.get_tokens_manually() # run in your Terminal, follow the prompt to complete authentication: @@ -47,13 +48,13 @@ schwab_client.get_tokens_manually() #---------------- ticker = '$SPX' # get option expirations: -expiration_list = await schwab_client.get_option_expirations_async(underlying_symbol = ticker) +expiration_list = schwab_client.get_option_expirations(underlying_symbol = ticker) # download SPX option chains from_date = 2024-07-01 to_date = 2024-07-01 -opt_chain_result = await schwab_client.download_option_chain_async(ticker, from_date, to_date) +opt_chain_result = schwab_client.download_option_chain(ticker, from_date, to_date) # get call-put dataframe pairs by expiration opt_df_pairs = opt_chain_result.to_dataframe_pairs_by_expiration() diff --git a/cschwabpy/SchwabAsyncClient.py b/cschwabpy/SchwabAsyncClient.py index d38588a..ff84c91 100644 --- a/cschwabpy/SchwabAsyncClient.py +++ b/cschwabpy/SchwabAsyncClient.py @@ -86,7 +86,7 @@ async def _ensure_valid_access_token(self, force_refresh: bool = False) -> bool: if response.status_code == 200: json_res = response.json() tokens = Tokens(**json_res) - self.__token_store.save_tokens(tokens) + await self.__token_store.save_tokens(tokens) return True else: raise Exception( @@ -385,63 +385,3 @@ async def download_option_chain_async( finally: if not self.__keep_client_alive: await client.aclose() - - def get_tokens_manually( - self, - ) -> None: - """Manual steps to get tokens from Charles Schwab API.""" - from prompt_toolkit import prompt - import urllib.parse as url_parser - - redirect_uri = prompt("Enter your redirect uri> ").strip() - complete_auth_url = f"{SCHWAB_API_BASE_URL}/{SCHWAB_AUTH_PATH}?response_type=code&client_id={self.__client_id}&redirect_uri={redirect_uri}" - print( - f"Copy and open the following URL in browser. Complete Login & Authorization:\n {complete_auth_url}" - ) - auth_code_response_url = prompt( - "Paste the entire authorization response URL here> " - ).strip() - - auth_code = "" - try: - auth_code_pattern = re.compile(r"code=(.+)&?") - d = re.search(auth_code_pattern, auth_code_response_url) - if d: - auth_code = d.group(1) - auth_code = url_parser.unquote(auth_code.split("&")[0]) - else: - raise Exception( - "authorization response url does not contain authorization code" - ) - - if len(auth_code) == 0: - raise Exception("authorization code is empty") - except Exception as ex: - raise Exception( - "Failed to get authorization code. Please try again. Exception: ", ex - ) - - key_sec_encoded = self.__encode_app_key_secret() - with httpx.Client() as client: - response = client.post( - url=self.token_url, - headers={ - "Authorization": f"Basic {key_sec_encoded}", - "Content-Type": "application/x-www-form-urlencoded", - }, - data={ - "grant_type": "authorization_code", - "code": auth_code, - "redirect_uri": redirect_uri, - }, - ) - - if response.status_code == 200: - json_res = response.json() - tokens = Tokens(**json_res) - self.__token_store.save_tokens(tokens) - print( - f"Tokens saved successfully at path: {self.__token_store.token_file_path}" - ) - else: - print("Failed to get tokens. Please try again.")