Skip to content

Commit

Permalink
go back to requests to fix problems with httpx (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
Graeme22 authored Jul 30, 2024
1 parent 0128eee commit 90204f5
Show file tree
Hide file tree
Showing 10 changed files with 44 additions and 55 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
project = 'tastytrade'
copyright = '2024, Graeme Holliday'
author = 'Graeme Holliday'
release = '8.1'
release = '8.2'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
httpx==0.27.0
requests==2.32.3
mypy==1.10.0
flake8==7.0.0
isort==5.13.2
types-pytz==2024.1.0.20240417
types-requests==2.32.0.20240712
websockets==12.0
pandas_market_calendars==4.3.3
pydantic==2.7.1
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='tastytrade',
version='8.1',
version='8.2',
description='An unofficial SDK for Tastytrade!',
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
Expand All @@ -16,7 +16,7 @@
url='https://github.com/tastyware/tastytrade',
license='MIT',
install_requires=[
'httpx>=0.27.0',
'requests<3',
'websockets>=11.0.3',
'pydantic>=2.6.3',
'pandas_market_calendars>=4.3.3',
Expand Down
2 changes: 1 addition & 1 deletion tastytrade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

API_URL = 'https://api.tastyworks.com'
CERT_URL = 'https://api.cert.tastyworks.com'
VERSION = '8.1'
VERSION = '8.2'

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
Expand Down
10 changes: 6 additions & 4 deletions tastytrade/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ def get_history(
txns = []
while True:
response = session.client.get(
f'/accounts/{self.account_number}/transactions',
(f'{session.base_url}/accounts/{self.account_number}'
f'/transactions'),
params={
k: v # type: ignore
for k, v in params.items()
Expand Down Expand Up @@ -846,9 +847,9 @@ def get_order_history(
orders = []
while True:
response = session.client.get(
f'/accounts/{self.account_number}/orders',
f'{session.base_url}/accounts/{self.account_number}/orders',
params={
k: v # type: ignore
k: v # type: ignore
for k, v in params.items()
if v is not None
}
Expand Down Expand Up @@ -895,7 +896,8 @@ def get_complex_order_history(
orders = []
while True:
response = session.client.get(
f'/accounts/{self.account_number}/complex-orders',
(f'{session.base_url}/accounts/{self.account_number}'
f'/complex-orders'),
params={k: v for k, v in params.items() if v is not None}
)
validate_response(response)
Expand Down
2 changes: 1 addition & 1 deletion tastytrade/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def get_active_equities(
equities = []
while True:
response = session.client.get(
'/instruments/equities/active',
f'{session.base_url}/instruments/equities/active',
params={k: v for k, v in params.items() if v is not None}
)
validate_response(response)
Expand Down
3 changes: 2 additions & 1 deletion tastytrade/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def symbol_search(
:param symbol: search phrase
"""
symbol = symbol.replace('/', '%2F')
response = session.client.get(f'/symbols/search/{symbol}')
response = session.client.get(f'{session.base_url}/symbols/search/'
f'{symbol}')
if response.status_code // 100 != 2:
# here it doesn't really make sense to throw an exception
return []
Expand Down
68 changes: 25 additions & 43 deletions tastytrade/session.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import Any, Dict, Optional

import httpx
import requests
from fake_useragent import UserAgent # type: ignore
from httpx import Client, Response

from tastytrade import API_URL, CERT_URL
from tastytrade.utils import (TastytradeError, TastytradeJsonDataclass,
Expand Down Expand Up @@ -34,14 +33,6 @@ class Session:
:param dxfeed_tos_compliant:
whether to use the dxfeed TOS-compliant API endpoint for the streamer
"""
client: Client
is_test: bool
remember_token: Optional[str]
session_token: str
streamer_token: str
dxlink_url: str
user: Dict[str, str]

def __init__(
self,
login: str,
Expand All @@ -50,8 +41,7 @@ def __init__(
remember_token: Optional[str] = None,
is_test: bool = False,
two_factor_authentication: Optional[str] = None,
dxfeed_tos_compliant: bool = False,
proxy: Optional[str] = None
dxfeed_tos_compliant: bool = False
):
body = {
'login': login,
Expand All @@ -65,7 +55,7 @@ def __init__(
raise TastytradeError('You must provide a password or remember '
'token to log in.')
# The base url to use for API requests
base_url = CERT_URL if is_test else API_URL
self.base_url = CERT_URL if is_test else API_URL
#: Whether this is a cert or real session
self.is_test = is_test
# The headers to use for API requests
Expand All @@ -74,21 +64,19 @@ def __init__(
'Content-Type': 'application/json',
'User-Agent': UserAgent().random
}
# Set client for requests
self.client = requests.Session()
self.client.headers.update(headers)
if two_factor_authentication is not None:
response = httpx.post(
f'{base_url}/sessions',
response = self.client.post(
f'{self.base_url}/sessions',
json=body,
headers={
**headers,
'X-Tastyworks-OTP': two_factor_authentication
},
proxy=proxy
headers={'X-Tastyworks-OTP': two_factor_authentication}
)
else:
response = httpx.post(
f'{base_url}/sessions',
json=body,
proxy=proxy
response = self.client.post(
f'{self.base_url}/sessions',
json=body
)
validate_response(response) # throws exception if not 200

Expand All @@ -97,47 +85,41 @@ def __init__(
self.user = json['data']['user']
#: The session token used to authenticate requests
self.session_token = json['data']['session-token']
headers['Authorization'] = self.session_token
#: A single-use token which can be used to login without a password
self.remember_token = json['data'].get('remember-token')
# Set clients for sync and async requests
self.client = Client(
base_url=base_url,
headers=headers,
proxy=proxy,
timeout=30 # many requests can take a while
)
self.client.headers.update({'Authorization': self.session_token})
self.validate()

# Pull streamer tokens and urls
url = ('api-quote-tokens'
url = ('/api-quote-tokens'
if dxfeed_tos_compliant or is_test
else 'quote-streamer-tokens')
response = self.client.get(f'/{url}')
validate_response(response)
data = response.json()['data']
else '/quote-streamer-tokens')
data = self.get(url)
#: Auth token for dxfeed websocket
self.streamer_token = data['token']
#: URL for dxfeed websocket
self.dxlink_url = data['dxlink-url']

def get(self, url, **kwargs) -> Dict[str, Any]:
response = self.client.get(url, **kwargs)
response = self.client.get(self.base_url + url, timeout=30, **kwargs)
return self._validate_and_parse(response)

def delete(self, url, **kwargs) -> None:
response = self.client.delete(url, **kwargs)
response = self.client.delete(self.base_url + url, **kwargs)
validate_response(response)

def post(self, url, **kwargs) -> Dict[str, Any]:
response = self.client.post(url, **kwargs)
response = self.client.post(self.base_url + url, **kwargs)
return self._validate_and_parse(response)

def put(self, url, **kwargs) -> Dict[str, Any]:
response = self.client.put(url, **kwargs)
response = self.client.put(self.base_url + url, **kwargs)
return self._validate_and_parse(response)

def _validate_and_parse(self, response: Response) -> Dict[str, Any]:
def _validate_and_parse(
self,
response: requests.Response
) -> Dict[str, Any]:
validate_response(response)
return response.json()['data']

Expand All @@ -147,7 +129,7 @@ def validate(self) -> bool:
:return: True if the session is valid and False otherwise.
"""
response = self.client.post('/sessions/validate')
response = self.client.post(f'{self.base_url}/sessions/validate')
return (response.status_code // 100 == 2)

def destroy(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tastytrade/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import pandas_market_calendars as mcal # type: ignore
import pytz
from httpx import Response
from pydantic import BaseModel
from requests import Response

NYSE = mcal.get_calendar('NYSE')
TZ = pytz.timezone('US/Eastern')
Expand Down
3 changes: 3 additions & 0 deletions tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def test_get_live_orders(session, account):


def test_place_oco_order(session, account):
"""
# account must have a share of F for this to work
symbol = Equity.get_equity(session, 'F')
closing = symbol.build_leg(Decimal(1), OrderAction.SELL_TO_CLOSE)
Expand All @@ -142,6 +143,8 @@ def test_place_oco_order(session, account):
# test get complex order
_ = account.get_complex_order(session, resp2.complex_order.id)
account.delete_complex_order(session, resp2.complex_order.id)
"""
assert True


def test_place_otoco_order(session, account):
Expand Down

0 comments on commit 90204f5

Please sign in to comment.