12 |
13 |
14 |
--------------------------------------------------------------------------------
/pyTD/auth/manager.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import logging
25 | import requests
26 | import webbrowser
27 |
28 | from pyTD.auth.server import TDAuthServer
29 | from pyTD.auth.tokens import RefreshToken, AccessToken
30 | from pyTD.utils import to_timestamp
31 | from pyTD.utils.exceptions import AuthorizationError
32 |
33 | logger = logging.getLogger(__name__)
34 |
35 |
36 | class TDAuthManager(object):
37 | """
38 | Authorization manager for TD Ameritrade OAuth 2.0 authorization and
39 | authentication.
40 |
41 | Attributes
42 | ----------
43 | auth_server: TDAuthServer or None
44 | An authentication server instance which can be started and stopped for
45 | handling authentication redirects.
46 | """
47 | def __init__(self, token_cache, consumer_key, callback_url):
48 | """
49 | Initialize the class
50 |
51 | Parameters
52 | ----------
53 | token_cache: MemCache or DiskCache
54 | A cache for storing the refresh and access tokens
55 | consumer_key: str
56 | Client OAuth ID
57 | callback_url: str
58 | Client Redirect URI
59 | """
60 | self.cache = token_cache
61 | self.consumer_key = consumer_key
62 | self.callback_url = callback_url
63 | self.auth_server = None
64 |
65 | @property
66 | def access_token(self):
67 | return self.cache.access_token
68 |
69 | @property
70 | def refresh_token(self):
71 | return self.cache.refresh_token
72 |
73 | def auth_via_browser(self):
74 | """
75 | Handles authentication and authorization.
76 |
77 | Raises
78 | ------
79 | AuthorizationError
80 | If the authentication or authorization could not be completed
81 | """
82 | self._start_auth_server()
83 | self._open_browser(self.callback_url)
84 | logger.debug("Waiting for authorization code...")
85 | tokens = self.auth_server._wait_for_tokens()
86 | self._stop_auth_server()
87 |
88 | try:
89 | refresh_token = tokens["refresh_token"]
90 | refresh_expiry = tokens["refresh_token_expires_in"]
91 | access_token = tokens["access_token"]
92 | access_expiry = tokens["expires_in"]
93 | access_time = tokens["access_time"]
94 | except KeyError:
95 | logger.error("Authorization could not be completed.")
96 | raise AuthorizationError("Authorization could not be completed.")
97 | r = RefreshToken(token=refresh_token, access_time=access_time,
98 | expires_in=refresh_expiry)
99 | a = AccessToken(token=access_token, access_time=access_time,
100 | expires_in=access_expiry)
101 | logger.debug("Refresh and Access tokens received.")
102 | return (r, a,)
103 |
104 | def _open_browser(self, url):
105 | logger.info("Opening browser to %s" % url)
106 | webbrowser.open(url, new=2)
107 | return True
108 |
109 | def refresh_access_token(self):
110 | """
111 | Attempts to refresh access token if current is not valid.
112 |
113 | Updates the cache if new token is received.
114 |
115 | Raises
116 | ------
117 | AuthorizationError
118 | If the access token is not successfully refreshed
119 | """
120 | if self.cache.refresh_token.valid is False:
121 | raise AuthorizationError("Refresh token is not valid.")
122 | logger.debug("Attempting to refresh access token...")
123 | headers = {'Content-Type': 'application/x-www-form-urlencoded'}
124 | data = {'grant_type': 'refresh_token',
125 | 'refresh_token': self.cache.refresh_token.token,
126 | 'client_id': self.consumer_key}
127 | try:
128 | authReply = requests.post('https://api.tdameritrade.com/v1/oauth2/'
129 | 'token', headers=headers, data=data)
130 | now = to_timestamp(datetime.datetime.now())
131 | if authReply.status_code == 400:
132 | raise AuthorizationError("Could not refresh access token.")
133 | authReply.raise_for_status()
134 | json_data = authReply.json()
135 | token = json_data["access_token"]
136 | expires_in = json_data["expires_in"]
137 | except (KeyError, ValueError):
138 | logger.error("Error retrieving access token.")
139 | raise AuthorizationError("Error retrieving access token.")
140 | access_token = AccessToken(token=token, access_time=now,
141 | expires_in=expires_in)
142 | logger.debug("Successfully refreshed access token.")
143 | self.cache.access_token = access_token
144 |
145 | def _start_auth_server(self):
146 | logger.info("Starting authorization server")
147 |
148 | # Return if server is already running
149 | if self.auth_server is not None:
150 | return
151 | self.auth_server = TDAuthServer(self.consumer_key, self.callback_url)
152 |
153 | def _stop_auth_server(self):
154 | logger.info("Shutting down authorization server")
155 | if self.auth_server is None:
156 | return
157 | self.auth_server = None
158 |
--------------------------------------------------------------------------------
/pyTD/auth/server.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import codecs
24 | import datetime
25 | import json
26 | import logging
27 | import os
28 | import requests
29 | import ssl
30 |
31 | from pyTD import BASE_AUTH_URL, DEFAULT_SSL_DIR, PACKAGE_DIR
32 | from pyTD.compat import HTTPServer, BaseHTTPRequestHandler
33 | from pyTD.compat import urlparse, urlencode, parse_qs
34 | from pyTD.utils import to_timestamp
35 | from pyTD.utils.exceptions import AuthorizationError
36 |
37 | logger = logging.getLogger(__name__)
38 |
39 |
40 | class Handler(BaseHTTPRequestHandler):
41 |
42 | STATIC_DIR = os.path.join(PACKAGE_DIR, '/auth/_static/')
43 |
44 | @property
45 | def auth_link(self):
46 | params = {
47 | "response_type": "code",
48 | "redirect_uri": self.server.callback_url,
49 | "client_id": self.server.consumer_key,
50 | }
51 | return '%s?%s' % (BASE_AUTH_URL, urlencode(params))
52 |
53 | def do_GET(self):
54 | if self.path.endswith(".css"):
55 | f = open("pyTD/auth/_static/style.css", 'r')
56 | self.send_response(200)
57 | self.send_header('Content-type', 'text/css')
58 | self.end_headers()
59 | self.wfile.write(f.read().encode())
60 | f.close()
61 | return
62 |
63 | self._set_headers()
64 | path, _, query_string = self.path.partition('?')
65 | try:
66 | code = parse_qs(query_string)['code'][0]
67 | except KeyError:
68 | f = codecs.open("pyTD/auth/_static/auth.html", "r", "utf-8")
69 | auth = f.read()
70 | link = auth.format(self.auth_link)
71 | self.wfile.write(link.encode('utf-8'))
72 | f.close()
73 | else:
74 | self.server.auth_code = code
75 | headers = {'Content-Type': 'application/x-www-form-urlencoded'}
76 | data = {'refresh_token': '', 'grant_type': 'authorization_code',
77 | 'access_type': 'offline', 'code': self.server.auth_code,
78 | 'client_id': self.server.consumer_key,
79 | 'redirect_uri': self.server.callback_url}
80 | now = to_timestamp(datetime.datetime.now())
81 | authReply = requests.post('https://api.tdameritrade.com/v1/oauth2/'
82 | 'token', headers=headers, data=data)
83 | try:
84 | json_data = authReply.json()
85 | json_data["access_time"] = now
86 | self.server._store_tokens(json_data)
87 | except ValueError:
88 | msg = json.dumps(json_data)
89 | logger.Error("Tokens could not be obtained")
90 | logger.Error("RESPONSE: %s" % msg)
91 | raise AuthorizationError("Authorization could not be "
92 | "completed")
93 | success = codecs.open("pyTD/auth/_static/success.html", "r",
94 | "utf-8")
95 |
96 | self.wfile.write(success.read().encode())
97 | success.close()
98 |
99 | def _set_headers(self):
100 | self.send_response(200)
101 | self.send_header('Content-Type', 'text/html')
102 | self.end_headers()
103 |
104 |
105 | class TDAuthServer(HTTPServer):
106 | """
107 | HTTP Server to handle authorization
108 | """
109 | def __init__(self, consumer_key, callback_url, retry_count=3):
110 | self.consumer_key = consumer_key
111 | self.callback_url = callback_url
112 | self.parsed_url = urlparse(self.callback_url)
113 | self.retry_count = retry_count
114 | self.auth_code = None
115 | self.tokens = None
116 | self.ssl_key = os.path.join(DEFAULT_SSL_DIR, 'key.pem')
117 | self.ssl_cert = os.path.join(DEFAULT_SSL_DIR, 'cert.pem')
118 | super(TDAuthServer, self).__init__(('localhost', self.port),
119 | Handler)
120 | self.socket = ssl.wrap_socket(self.socket, keyfile=self.ssl_key,
121 | certfile=self.ssl_cert,
122 | server_side=True)
123 |
124 | @property
125 | def hostname(self):
126 | return "%s://%s" % (self.parsed_url.scheme, self.parsed_url.hostname)
127 |
128 | @property
129 | def port(self):
130 | return self.parsed_url.port
131 |
132 | def _store_tokens(self, tokens):
133 | self.tokens = tokens
134 |
135 | def _wait_for_tokens(self):
136 | count = 0
137 | while count <= self.retry_count and self.tokens is None:
138 | self.handle_request()
139 | if self.tokens:
140 | return self.tokens
141 | else:
142 | raise AuthorizationError("The authorization could not be "
143 | "completed.")
144 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8:noqa
24 |
25 | from pyTD.auth.tokens.access_token import AccessToken
26 | from pyTD.auth.tokens.empty_token import EmptyToken
27 | from pyTD.auth.tokens.refresh_token import RefreshToken
28 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/access_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens.base import Token
24 |
25 |
26 | class AccessToken(Token):
27 | """
28 | Access Token object
29 | """
30 | def __repr__(self):
31 | fmt = ("AccessToken(token= %s, access_time = %s, expires_in = %s)")
32 | return fmt % (self.token, self.access_time, self.expires_in)
33 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 |
25 | from pyTD.utils import to_timestamp
26 |
27 |
28 | class Token(object):
29 | """
30 | Token object base class.
31 |
32 | Parameters
33 | ----------
34 | token: str
35 | Token value
36 | access_time: int
37 | expires_in: int
38 |
39 | """
40 | def __init__(self, options=None, **kwargs):
41 |
42 | kwargs.update(options or {})
43 |
44 | self.token = kwargs['token']
45 | self.access_time = kwargs['access_time']
46 | self.expires_in = kwargs['expires_in']
47 |
48 | def __dict__(self):
49 | return {
50 | "token": self.token,
51 | "access_time": self.access_time,
52 | "expires_in": self.expires_in
53 | }
54 |
55 | def __eq__(self, other):
56 | if isinstance(other, Token):
57 | t = self.token == other.token
58 | a = self.access_time == other.access_time
59 | e = self.expires_in == other.expires_in
60 | return t and a and e
61 |
62 | def __ne__(self, other):
63 | if isinstance(other, Token):
64 | t = self.token == other.token
65 | a = self.access_time == other.access_time
66 | e = self.expires_in == other.expires_in
67 | return t and a and e
68 |
69 | def __repr__(self):
70 | fmt = ("Token(token= %s, access_time = %s, expires_in = %s)")
71 | return fmt % (self.token, self.access_time, self.expires_in)
72 |
73 | def __str__(self):
74 | return self.token
75 |
76 | @property
77 | def expiry(self):
78 | return self.access_time + self.expires_in
79 |
80 | @property
81 | def valid(self):
82 | now = to_timestamp(datetime.datetime.now())
83 | return True if self.expiry > now else False
84 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/empty_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 |
24 | class EmptyToken(object):
25 | """
26 | Empty token object. Returns not valid.
27 | """
28 | def __dict__(self):
29 | return {}
30 |
31 | def __repr__(self):
32 | return str(self)
33 |
34 | def __str__(self):
35 | return ("EmptyToken(valid: False)")
36 |
37 | @property
38 | def token(self):
39 | return None
40 |
41 | @property
42 | def valid(self):
43 | return False
44 |
--------------------------------------------------------------------------------
/pyTD/auth/tokens/refresh_token.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens.base import Token
24 |
25 |
26 | class RefreshToken(Token):
27 | """
28 | Refresh Token object
29 | """
30 | def __repr__(self):
31 | fmt = ("RefreshToken(token= %s, access_time = %s, expires_in = %s)")
32 | return fmt % (self.token, self.access_time, self.expires_in)
33 |
--------------------------------------------------------------------------------
/pyTD/cache/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8: noqa
24 |
25 | from pyTD.cache.disk_cache import DiskCache
26 | from pyTD.cache.mem_cache import MemCache
27 |
--------------------------------------------------------------------------------
/pyTD/cache/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth.tokens import EmptyToken
24 |
25 |
26 | def surface_property(api_property_name, docstring=None):
27 | def getter(self):
28 | return self._get(api_property_name) or EmptyToken()
29 |
30 | def setter(self, value):
31 | if isinstance(value, EmptyToken):
32 | self._set(api_property_name, None)
33 | else:
34 | self._set(api_property_name, value)
35 |
36 | return property(getter, setter, doc=docstring)
37 |
38 |
39 | class TokenCache(object):
40 | """
41 | Base class for auth token caches
42 | """
43 | refresh_token = surface_property("refresh_token")
44 | access_token = surface_property("access_token")
45 |
46 | def __init__(self):
47 | self._create()
48 |
49 | def clear(self):
50 | raise NotImplementedError
51 |
52 | def _create(self):
53 | raise NotImplementedError
54 |
55 | def _exists(self):
56 | raise NotImplementedError
57 |
58 | def _get(self):
59 | raise NotImplementedError
60 |
61 | def _set(self):
62 | raise NotImplementedError
63 |
--------------------------------------------------------------------------------
/pyTD/cache/disk_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import json
24 | import logging
25 | import os
26 |
27 | from pyTD.auth.tokens import AccessToken, RefreshToken
28 | from pyTD.cache.base import TokenCache
29 | from pyTD.utils.exceptions import ConfigurationError
30 |
31 | logger = logging.getLogger(__name__)
32 |
33 |
34 | class DiskCache(TokenCache):
35 | """
36 | On-disk token cache for access and refresh tokens
37 |
38 | Attributes
39 | ----------
40 | config_dir: str
41 | Desired directory to store cache
42 | filename: str
43 | Desired cache file name
44 |
45 | Usage
46 | -----
47 |
48 | >>> c = DiskCache()
49 | >>> c.refresh_token = token
50 | >>> c.access_token = token
51 | """
52 | def __init__(self, config_dir, filename):
53 | self.config_dir = os.path.expanduser(config_dir)
54 | if not os.path.isdir(self.config_dir):
55 | raise ConfigurationError("Directory %s not found. Configuration "
56 | "likely incomplete. "
57 | "Try pyTD.configure()" % self.config_dir)
58 | self.filename = filename
59 | self.config_path = os.path.join(self.config_dir, self.filename)
60 | self._create()
61 |
62 | def clear(self):
63 | """
64 | Empties the cache, though does not delete the cache file
65 | """
66 | with open(self.config_path, 'w') as f:
67 | json_data = {
68 | "refresh_token": None,
69 | "access_token": None,
70 | }
71 | f.write(json.dumps(json_data))
72 | f.close()
73 | return
74 |
75 | def _create(self):
76 | if not os.path.exists(self.config_path):
77 | with open(self.config_path, 'w') as f:
78 | json_data = {
79 | "refresh_token": None,
80 | "access_token": None,
81 | }
82 | f.write(json.dumps(json_data))
83 | f.close()
84 | return
85 |
86 | def _exists(self):
87 | """
88 | Utility function to test whether the configuration exists
89 | """
90 | return os.path.isfile(self.config_path)
91 |
92 | def _get(self, value=None):
93 | """
94 | Retrieves configuration information. If not passed a parameter,
95 | returns all configuration as a dictionary
96 |
97 | Parameters
98 | ----------
99 | value: str, optional
100 | Desired configuration value to retrieve
101 | """
102 | if self._exists() is True:
103 | f = open(self.config_path, 'r')
104 | config = json.load(f)
105 | if value is None:
106 | return config
107 | elif value not in config:
108 | raise ValueError("Value %s not found in configuration "
109 | "file." % value)
110 | else:
111 | if value == "refresh_token" and config[value]:
112 | return RefreshToken(config[value])
113 | elif value == "access_token" and config[value]:
114 | return AccessToken(config[value])
115 | else:
116 | return config[value]
117 | else:
118 | raise ConfigurationError("Configuration file not found in "
119 | "%s." % self.config_path)
120 |
121 | def _set(self, attr, payload):
122 | """
123 | Update configuration file given payload
124 |
125 | Parameters
126 | ----------
127 | payload: dict
128 | Dictionary of updated configuration variables
129 | """
130 | with open(self.config_path) as f:
131 | json_data = json.load(f)
132 | f.close()
133 | json_data.update({attr: payload.__dict__()})
134 | with open(self.config_path, "w") as f:
135 | f.write(json.dumps(json_data))
136 | f.close()
137 | return True
138 | raise ConfigurationError("Could not update config file")
139 |
--------------------------------------------------------------------------------
/pyTD/cache/mem_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.cache.base import TokenCache
24 |
25 |
26 | class MemCache(TokenCache):
27 | """
28 | In-memory token cache for access and refresh tokens
29 |
30 | Usage
31 | -----
32 |
33 | >>> c = MemCache()
34 | >>> c.refresh_token = token
35 | >>> c.access_token = token
36 | """
37 | def clear(self):
38 | return self._create()
39 |
40 | def _create(self):
41 | self._refresh_token = None
42 | self._access_token = None
43 |
44 | def _exists(self):
45 | return True
46 |
47 | def _get(self, api_property_name):
48 | return self.__getattribute__("_%s" % api_property_name)
49 |
50 | def _set(self, api_property_name, value):
51 | return self.__setattr__("_%s" % api_property_name, value)
52 |
--------------------------------------------------------------------------------
/pyTD/compat/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import sys
24 | from distutils.version import LooseVersion
25 |
26 | import pandas as pd
27 |
28 | PY3 = sys.version_info >= (3, 0)
29 |
30 | PANDAS_VERSION = LooseVersion(pd.__version__)
31 |
32 | PANDAS_0190 = (PANDAS_VERSION >= LooseVersion('0.19.0'))
33 | PANDAS_0230 = (PANDAS_VERSION >= LooseVersion('0.23.0'))
34 |
35 | if PANDAS_0190:
36 | from pandas.api.types import is_number
37 | else:
38 | from pandas.core.common import is_number # noqa
39 |
40 | if PANDAS_0230:
41 | from pandas.core.dtypes.common import is_list_like
42 | else:
43 | from pandas.core.common import is_list_like # noqa
44 |
45 | if PY3:
46 | from urllib.error import HTTPError
47 | from urllib.parse import urlparse, urlencode, parse_qs
48 | from io import StringIO
49 | from http.server import HTTPServer, BaseHTTPRequestHandler
50 | from mock import MagicMock
51 | else:
52 | from urllib2 import HTTPError # noqa
53 | from urlparse import urlparse, parse_qs # noqa
54 | from urllib import urlencode # noqa
55 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler # noqa
56 | import StringIO # noqa
57 | from mock import MagicMock # noqa
58 |
--------------------------------------------------------------------------------
/pyTD/instruments/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments.base import Instruments
24 |
25 |
26 | def get_instrument(*args, **kwargs):
27 | """
28 | Retrieve instrument from CUSIP ID from the Get Instrument endpoint
29 |
30 | Parameters
31 | ----------
32 | symbol: str
33 | A CUSIP ID or symbol
34 | output_format: str, default "pandas", optional
35 | Desired output format. "pandas" or "json"
36 | """
37 | return Instruments(*args, **kwargs).execute()
38 |
39 |
40 | def get_instruments(*args, **kwargs):
41 | """
42 | Search or retrieve instrument data, including fundamental data
43 |
44 | Parameters
45 | ----------
46 | symbol: str
47 | A CUSIP ID, symbol, regular expression, or snippet (depends on the
48 | value of the "projection" variable)
49 | projection: str, default symbol-search, optional
50 | Type of request (see documentation)
51 | output_format: str, default "pandas", optional
52 | Desired output format. "pandas" or "json"
53 | """
54 | return Instruments(*args, **kwargs).execute()
55 |
--------------------------------------------------------------------------------
/pyTD/instruments/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth import auth_check
24 | from pyTD.resource import Get
25 | from pyTD.utils.exceptions import ResourceNotFound
26 |
27 |
28 | class Instruments(Get):
29 | """
30 | Class for retrieving instruments
31 | """
32 | def __init__(self, symbol, **kwargs):
33 | self.symbol = symbol
34 | self.output_format = kwargs.get("output_format", "pandas")
35 | self.projection = kwargs.get("projection", "symbol-search")
36 | super(Instruments, self).__init__(kwargs.get("api", None))
37 |
38 | @property
39 | def url(self):
40 | return "%s/instruments" % self._BASE_URL
41 |
42 | @property
43 | def params(self):
44 | return {
45 | "symbol": self.symbol,
46 | "projection": self.projection
47 | }
48 |
49 | def _convert_output(self, out):
50 | import pandas as pd
51 | if self.projection == "fundamental":
52 | return pd.DataFrame({self.symbol:
53 | out[self.symbol]["fundamental"]})
54 | return pd.DataFrame(out)
55 |
56 | @auth_check
57 | def execute(self):
58 | data = self.get()
59 | if not data:
60 | raise ResourceNotFound("Instrument data for %s not"
61 | " found." % self.symbol)
62 | if self.output_format == "pandas":
63 | return self._convert_output(data)
64 | else:
65 | return data
66 |
--------------------------------------------------------------------------------
/pyTD/market/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments.base import Instruments
24 | from pyTD.market.hours import MarketHours
25 | from pyTD.market.quotes import Quotes
26 | from pyTD.market.movers import Movers
27 | from pyTD.market.options import Options
28 | from pyTD.market.price_history import PriceHistory
29 |
30 |
31 | def get_fundamentals(*args, **kwargs):
32 | """
33 | Retrieve fundamental data for a diven symbol or CUSIP ID
34 |
35 | Parameters
36 | ----------
37 | symbol: str
38 | A CUSIP ID, symbol, regular expression, or snippet (depends on the
39 | value of the "projection" variable)
40 | output_format: str, default "pandas", optional
41 | Desired output format. "pandas" or "json"
42 | """
43 | kwargs.update({"projection": "fundamental"})
44 | return Instruments(*args, **kwargs).execute()
45 |
46 |
47 | def get_quotes(*args, **kwargs):
48 | """
49 | Function for retrieving quotes from the Get Quotes endpoint.
50 |
51 | Parameters
52 | ----------
53 | symbols : str, array-like object (list, tuple, Series), or DataFrame
54 | Single stock symbol (ticker), array-like object of symbols or
55 | DataFrame with index containing up to 100 stock symbols.
56 | output_format: str, default 'pandas', optional
57 | Desired output format (json or DataFrame)
58 | kwargs: additional request parameters (see _TDBase class)
59 | """
60 | return Quotes(*args, **kwargs).execute()
61 |
62 |
63 | def get_market_hours(*args, **kwargs):
64 | """
65 | Function to retrieve market hours for a given market from the Market
66 | Hours endpoint
67 |
68 | Parameters
69 | ----------
70 | market: str, default EQUITY, optional
71 | The market to retrieve operating hours for
72 | date : string or DateTime object, (defaults to today's date)
73 | Operating date, timestamp. Parses many different kind of date
74 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
75 | output_format: str, default 'pandas', optional
76 | Desired output format (json or DataFrame)
77 | kwargs: additional request parameters (see _TDBase class)
78 | """
79 | return MarketHours(*args, **kwargs).execute()
80 |
81 |
82 | def get_movers(*args, **kwargs):
83 | """
84 | Function for retrieving market moveers from the Movers endpoint
85 |
86 | Parameters
87 | ----------
88 | index: str
89 | The index symbol to get movers from
90 | direction: str, default up, optional
91 | Return up or down movers
92 | change: str, default percent, optional
93 | Return movers by percent change or value change
94 | output_format: str, default 'pandas', optional
95 | Desired output format (json or DataFrame)
96 | kwargs: additional request parameters (see _TDBase class)
97 | """
98 | return Movers(*args, **kwargs).execute()
99 |
100 |
101 | def get_option_chains(*args, **kwargs):
102 | """
103 | Function to retrieve option chains for a given symbol from the Option
104 | Chains endpoint
105 |
106 | Parameters
107 | ----------
108 |
109 | contractType: str, default ALL, optional
110 | Desired contract type (CALL, PUT, ALL)
111 | strikeCount: int, optional
112 | Number of strikes to return above and below the at-the-money price
113 | includeQuotes: bool, default False, optional
114 | Include quotes for options in the option chain
115 | strategy: str, default None, optional
116 | Passing a value returns a strategy chain (SINGLE or ANALYTICAL)
117 | interval: int, optional
118 | Strike interval for spread strategy chains
119 | strike: float, optional
120 | Filter options that only have a certain strike price
121 | range: str, optional
122 | Returns options for a given range (ITM, OTM, etc.)
123 | fromDate: str or datetime.datetime object, optional
124 | Only return options after this date
125 | toDate: str or datetime.datetime object, optional
126 | Only return options before this date
127 | volatility: float, optional
128 | Volatility to use in calculations (for analytical strategy chains)
129 | underlyingPrice: float, optional
130 | Underlying price to use in calculations (for analytical strategy
131 | chains)
132 | interestRate: float, optional
133 | Interest rate to use in calculations (for analytical strategy
134 | chains)
135 | daysToExpiration: int, optional
136 | Days to expiration to use in calulations (for analytical
137 | strategy chains)
138 | expMonth: str, optional
139 | Expiration month (format JAN, FEB, etc.) to use in calculations
140 | (for analytical strategy chains), default ALL
141 | optionType: str, optional
142 | Type of contracts to return (S: standard, NS: nonstandard,
143 | ALL: all contracts)
144 | output_format: str, optional, default 'pandas'
145 | Desired output format
146 | api: pyTD.api.api object, optional
147 | A pyTD api object. If not passed, API requestor defaults to
148 | pyTD.api.default_api
149 | kwargs: additional request parameters (see _TDBase class)
150 | """
151 | return Options(*args, **kwargs).execute()
152 |
153 |
154 | def get_price_history(*args, **kwargs):
155 | """
156 | Function to retrieve price history for a given symbol over a given period
157 |
158 | Parameters
159 | ----------
160 | symbols : string, array-like object (list, tuple, Series), or DataFrame
161 | Desired symbols for retrieval
162 | periodType: str, default DAY, optional
163 | The type of period to show
164 | period: int, optional
165 | The number of periods to show
166 | frequencyType: str, optional
167 | The type of frequency with which a new candle is formed
168 | frequency: int, optional
169 | The number of frequencyType to includ with each candle
170 | startDate : string or DateTime object, optional
171 | Starting date, timestamp. Parses many different kind of date
172 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
173 | endDate : string or DateTime object, optional
174 | Ending date, timestamp. Parses many different kind of date
175 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
176 | extended: str or bool, default 'True'/True, optional
177 | True to return extended hours data, False for regular hours only
178 | output_format: str, default 'pandas', optional
179 | Desired output format (json or DataFrame)
180 | """
181 | return PriceHistory(*args, **kwargs).execute()
182 |
183 |
184 | # def get_history_intraday(symbols, start, end, interval='1m', extended=True,
185 | # output_format='pandas'):
186 | # """
187 | # Function to retrieve intraday price history for a given symbol
188 |
189 | # Parameters
190 | # ----------
191 | # symbols : string, array-like object (list, tuple, Series), or DataFrame
192 | # Desired symbols for retrieval
193 | # startDate : string or DateTime object, optional
194 | # Starting date, timestamp. Parses many different kind of date
195 | # representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
196 | # endDate : string or DateTime object, optional
197 | # Ending date, timestamp. Parses many different kind of date
198 | # representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
199 | # interval: string, default '1m', optional
200 | # Desired interval (1m, 5m, 15m, 30m, 60m)
201 | # needExtendedHoursData: str or bool, default 'True'/True, optional
202 | # True to return extended hours data, False for regular hours only
203 | # output_format: str, default 'pandas', optional
204 | # Desired output format (json or DataFrame)
205 | # """
206 | # result = PriceHistory(symbols, start_date=start, end_date=end,
207 | # extended=extended,
208 | # output_format=output_format).execute()
209 | # if interval == '1m':
210 | # return result
211 | # elif interval == '5m':
212 | # sample = result.index.floor('5T').drop_duplicates()
213 | # return result.reindex(sample, method='ffill')
214 | # elif interval == '15m':
215 | # sample = result.index.floor('15T').drop_duplicates()
216 | # return result.reindex(sample, method='ffill')
217 | # elif interval == '30m':
218 | # sample = result.index.floor('30T').drop_duplicates()
219 | # return result.reindex(sample, method='ffill')
220 | # elif interval == '60m':
221 | # sample = result.index.floor('60T').drop_duplicates()
222 | # return result.reindex(sample, method='ffill')
223 | # else:
224 | # raise ValueError("Interval must be 1m, 5m, 15m, 30m, or 60m.")
225 |
226 |
227 | # def get_history_daily(symbols, start, end, output_format='pandas'):
228 | # return PriceHistory(symbols, start_date=start, end_date=end,
229 | # frequency_type='daily',
230 | # output_format=output_format).execute()
231 |
--------------------------------------------------------------------------------
/pyTD/market/base.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 | from pyTD.auth import auth_check
26 | from pyTD.resource import Get
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 |
31 | class MarketData(Get):
32 | """
33 | Base class for retrieving market-based information. This includes the
34 | following endpoint groups:
35 | - Market Hours
36 | - Movers
37 | - Option Chains
38 | - Price History
39 | - Quotes
40 |
41 | Parameters
42 | ----------
43 | symbols: str or list-like, optional
44 | A symbol or list of symbols
45 | output_format: str, optional, default 'json'
46 | Desired output format (json or Pandas DataFrame)
47 | api: pyTD.api.api object, optional
48 | A pyTD api object. If not passed, API requestor defaults to
49 | pyTD.api.default_api
50 | """
51 | def __init__(self, output_format='pandas', api=None):
52 | self.output_format = output_format
53 | super(MarketData, self).__init__(api)
54 |
55 | @property
56 | def endpoint(self):
57 | return "marketdata"
58 |
59 | @property
60 | def resource(self):
61 | raise NotImplementedError
62 |
63 | @property
64 | def url(self):
65 | return "%s%s/%s" % (self._BASE_URL, self.endpoint, self.resource)
66 |
67 | def _convert_output(self, out):
68 | import pandas as pd
69 | return pd.DataFrame(out)
70 |
71 | @auth_check
72 | def execute(self):
73 | out = self.get()
74 | return self._output_format(out)
75 |
76 | def _output_format(self, out):
77 | if self.output_format == 'json':
78 | return out
79 | elif self.output_format == 'pandas':
80 | return self._convert_output(out)
81 | else:
82 | raise ValueError("Please enter a valid output format.")
83 |
--------------------------------------------------------------------------------
/pyTD/market/hours.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 |
26 | from pyTD.market.base import MarketData
27 | from pyTD.utils import _handle_lists
28 |
29 |
30 | class MarketHours(MarketData):
31 | """
32 | Class for retrieving data from the Get Market Hours endpoint.
33 |
34 | Parameters
35 | ----------
36 | markets : string, default "EQUITY", optional
37 | Desired market for retrieval (EQUITY, OPTION, FUTURE, BOND,
38 | or FOREX)
39 | date : datetime.datetime object, optional
40 | Data to retrieve hours for (defaults to current day)
41 | output_format: str, optional, default 'pandas'
42 | Desired output format (json or Pandas DataFrame).
43 |
44 | .. note:: JSON output formatting only if "FUTURE" is selected.
45 | api: pyTD.api.api object, optional
46 | A pyTD api object. If not passed, API requestor defaults to
47 | pyTD.api.default_api
48 | """
49 | _MARKETS = {"equity": "EQ",
50 | "option": "EQO",
51 | "future": None,
52 | "bond": "BON",
53 | "forex": "forex"}
54 |
55 | def __init__(self, markets="EQUITY", date=None, output_format='pandas',
56 | api=None):
57 | self.date = date or datetime.datetime.now()
58 | err_msg = "Please enter one more most markets (EQUITY, OPTION, etc.)"\
59 | "for retrieval."
60 | self.markets = _handle_lists(markets, err_msg=err_msg)
61 | self.markets = [market.lower() for market in self.markets]
62 | if not set(self.markets).issubset(set(self._MARKETS)):
63 | raise ValueError("Please input valid markets for hours retrieval.")
64 | super(MarketHours, self).__init__(output_format, api)
65 |
66 | @property
67 | def params(self):
68 | return {
69 | "markets": ','.join(self.markets),
70 | "date": self.date.strftime('%Y-%m-%d')
71 | }
72 |
73 | @property
74 | def resource(self):
75 | return 'hours'
76 |
77 | def _convert_output(self, out):
78 | data = {market: out[market][self._MARKETS[market]] for market in
79 | self.markets}
80 | return pd.DataFrame(data)
81 |
--------------------------------------------------------------------------------
/pyTD/market/movers.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.market.base import MarketData
24 | from pyTD.utils import _handle_lists
25 |
26 |
27 | class Movers(MarketData):
28 | """
29 | Class for retrieving data from the Get Market Hours endpoint.
30 |
31 | Parameters
32 | ----------
33 | markets : str
34 | Ticker of market for retrieval
35 | direction : str, default 'up', optional
36 | To return movers with the specified directions of up or down
37 | change: str, default 'percent', optional
38 | To return movers with the specified change types of percent or value
39 | output_format: str, optional, default 'json'
40 | Desired output format (json or Pandas DataFrame)
41 | api: pyTD.api.api object, optional
42 | A pyTD api object. If not passed, API requestor defaults to
43 | pyTD.api.default_api
44 |
45 | WARNING: this endpoint is often not functional outside of trading hours.
46 | """
47 |
48 | def __init__(self, symbols, direction='up', change='percent',
49 | output_format='pandas', api=None):
50 | self.direction = direction
51 | self.change = change
52 | err_msg = "Please input a valid market ticker (ex. $DJI)."
53 | self.symbols = _handle_lists(symbols, mult=False, err_msg=err_msg)
54 | super(Movers, self).__init__(output_format, api)
55 |
56 | def _convert_output(self, out):
57 | import pandas as pd
58 | return pd.DataFrame(out).set_index("symbol")
59 |
60 | @property
61 | def params(self):
62 | return {
63 | 'change': self.change,
64 | 'direction': self.direction
65 | }
66 |
67 | @property
68 | def resource(self):
69 | return "movers"
70 |
71 | @property
72 | def url(self):
73 | return "%s%s/%s/%s" % (self._BASE_URL, self.endpoint, self.symbols,
74 | self.resource)
75 |
--------------------------------------------------------------------------------
/pyTD/market/options.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.market.base import MarketData
24 | from pyTD.utils import _handle_lists
25 | from pyTD.utils.exceptions import ResourceNotFound
26 |
27 |
28 | class Options(MarketData):
29 | """
30 | Class for retrieving data from the Get Option Chain endpoint
31 |
32 | Parameters
33 | ----------
34 | symbol: str
35 | Desired ticker for retrieval
36 | contract_type: str, default "ALL", optional
37 | Type of contracts to return in the chain. Can be CALL,
38 | PUT, or ALL
39 | strike_count: int, optional
40 | The number of strikes to return above and below the
41 | at-the-money price
42 | include_quotes: bool, optional
43 | Include quotes for options in the option chain
44 | strategy: str, default "SINGLE", optional
45 | Passing a value returns a Strategy Chain. Possible values are SINGLE,
46 | ANALYTICAL, COVERED, VERTICAL, CALENDAR, STRANGLE, STRADDLE,
47 | BUTTERFLY, CONDOR, DIAGONAL, COLLAR, ROLL
48 | interval: int, optional
49 | Strike interval for spread strategy chains
50 | strike: float, optional
51 | Strike price to return options only at that strike price
52 | range: str, default "ALL", optional
53 | Returns options for the given range. Possible values are ITM, NTM,
54 | OTM, SAK, SBK, SNK, ALL
55 | from_date : datetime.datetime object, optional
56 | Only return expirations after this date
57 | to_date: datetime.datetime object, optional
58 | Only return expirations before this date
59 | volatility: int or float, optional
60 | Volatility to use in calculations
61 | underlying_price: int or float, optional
62 | Underlying price to use in calculations
63 | interest_rate: int or float, optional
64 | Interest rate to use in calculations
65 | days_to_expiration: int, optional
66 | Days to expiration to use in calculations
67 | exp_month: str, default "ALL", optional
68 | Return only options expiring in the specified month. Month is given in
69 | 3-character format (JAN, FEB, MAR, etc.)
70 | option_type: str, default "ALL", optional
71 | Type of contracts to return (S, NS, ALL)
72 | output_format: str, optional, default 'json'
73 | Desired output format
74 | api: pyTD.api.api object, optional
75 | A pyTD api object. If not passed, API requestor defaults to
76 | pyTD.api.default_api
77 | """
78 |
79 | def __init__(self, symbol, **kwargs):
80 | self.contract_type = kwargs.pop("contract_type", "ALL")
81 | self.strike_count = kwargs.pop("strike_count", "")
82 | self.include_quotes = kwargs.pop("include_quotes", "")
83 | self.strategy = kwargs.pop("strategy", "")
84 | self.interval = kwargs.pop("interval", "")
85 | self.strike = kwargs.pop("strike", "")
86 | self.range = kwargs.pop("range", "")
87 | self.from_date = kwargs.pop("from_date", "")
88 | self.to_date = kwargs.pop("to_date", "")
89 | self.volatility = kwargs.pop("volatility", "")
90 | self.underlying_price = kwargs.pop("underlying_price", "")
91 | self.interest_rate = kwargs.pop("interest_rate", "")
92 | self.days_to_expiration = kwargs.pop("days_to_expiration", "")
93 | self.exp_month = kwargs.pop("exp_month", "")
94 | self.option_type = kwargs.pop("option_type", "")
95 | self.output_format = kwargs.pop("output_format", 'pandas')
96 | self.api = kwargs.pop("api", None)
97 | self.opts = kwargs
98 | self.symbols = _handle_lists(symbol)
99 | super(Options, self).__init__(self.output_format, self.api)
100 |
101 | @property
102 | def params(self):
103 | p = {
104 | "symbol": self.symbols,
105 | "contractType": self.contract_type,
106 | "strikeCount": self.strike_count,
107 | "includeQuotes": self.include_quotes,
108 | "strategy": self.strategy,
109 | "interval": self.interval,
110 | "strike": self.strike,
111 | "range": self.range,
112 | "fromDate": self.from_date,
113 | "toDate": self.to_date,
114 | "volatility": self.volatility,
115 | "underlyingPrice": self.underlying_price,
116 | "interestRate": self.interest_rate,
117 | "daysToExpiration": self.days_to_expiration,
118 | "expMonth": self.exp_month,
119 | "optionType": self.option_type
120 | }
121 | p.update(self.opts)
122 | return p
123 |
124 | @property
125 | def resource(self):
126 | return 'chains'
127 |
128 | def _convert_output(self, out):
129 | import pandas as pd
130 | ret = {}
131 | ret2 = {}
132 | if self.contract_type in ["CALL", "ALL"]:
133 | for date in out['callExpDateMap']:
134 | for strike in out['callExpDateMap'][date]:
135 | ret[date] = (out['callExpDateMap'][date][strike])[0]
136 | if self.contract_type in ["PUT", "ALL"]:
137 | for date in out['putExpDateMap']:
138 | for strike in out['putExpDateMap'][date]:
139 | ret2[date] = (out['putExpDateMap'][date][strike])[0]
140 | return pd.concat([pd.DataFrame(ret).T, pd.DataFrame(ret2).T], axis=1,
141 | keys=["calls", "puts"])
142 |
143 | def get(self):
144 | data = super(Options, self).get()
145 | if data["status"] == "FAILED":
146 | raise ResourceNotFound(message="Option chains for %s not "
147 | "found." % self.symbols)
148 | return data
149 |
--------------------------------------------------------------------------------
/pyTD/market/price_history.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 |
26 | from pyTD.auth import auth_check
27 | from pyTD.market.base import MarketData
28 | from pyTD.utils import _sanitize_dates, to_timestamp, _handle_lists
29 | from pyTD.utils.exceptions import ResourceNotFound
30 |
31 |
32 | class PriceHistory(MarketData):
33 | """
34 | Class for retrieving data from the Get Price History endpoint. Defaults to
35 | a 10-day, 1-minute chart
36 |
37 | Parameters
38 | ----------
39 | symbols : string, array-like object (list, tuple, Series), or DataFrame
40 | Desired symbols for retrieval
41 | period_type: str, default "day", optional
42 | Type of period to show (valid values are day, month, year, or ytd)
43 | period: int, optional
44 | The number of periods to show
45 | frequency_type: str, optional
46 | The type of frequency with which a new candle is formed. (valid values
47 | are minute, daily, weekly, monthly, depending on period type)
48 | frequency: int, default 1, optional
49 | The number of the frequency type to be included in each candle
50 | startDate : string or DateTime object, optional
51 | Starting date, timestamp. Parses many different kind of date. Defaults
52 | to 1/1/2018
53 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980')
54 | endDate : string or DateTime object, optional
55 | Ending date, timestamp. Parses many different kind of date
56 | representations (e.g., 'JAN-01-2015', '1/1/15', 'Jan, 1, 1980').
57 | Defaults to current day
58 | extended: bool, default True, optional
59 | True to return extended hours data, False for regular market hours only
60 | output_format: str, optional, default 'pandas'
61 | Desired output format (json or Pandas DataFrame)
62 | api: pyTD.api.api object, optional
63 | A pyTD api object. If not passed, API requestor defaults to
64 | pyTD.api.default_api
65 | """
66 |
67 | def __init__(self, symbols, **kwargs):
68 | self.period_type = kwargs.pop("period_type", "month")
69 | self.period = kwargs.pop("period", "")
70 | self.frequency_type = kwargs.pop("frequency_type", "daily")
71 | self.frequency = kwargs.pop("frequency", "")
72 | start = kwargs.pop("start_date", datetime.datetime(2018, 1, 1))
73 | end = kwargs.pop("end_date", datetime.datetime.today())
74 | self.need_extended = kwargs.pop("extended", "")
75 | self.output_format = kwargs.pop("output_format", 'pandas')
76 | self.opt = kwargs
77 | api = kwargs.get("api")
78 | self.start, self.end = _sanitize_dates(start, end, set_defaults=False)
79 | if self.start and self.end:
80 | self.start = to_timestamp(self.start) * 1000
81 | self.end = to_timestamp(self.end) * 1000
82 | self.symbols = _handle_lists(symbols)
83 | super(PriceHistory, self).__init__(self.output_format, api)
84 |
85 | @property
86 | def params(self):
87 | p = {
88 | "periodType": self.period_type,
89 | "period": self.period,
90 | "frequencyType": self.frequency_type,
91 | "frequency": self.frequency,
92 | "startDate": self.start,
93 | "endDate": self.end,
94 | "needExtendedHoursData": self.need_extended
95 | }
96 | return p
97 |
98 | @property
99 | def resource(self):
100 | return 'pricehistory'
101 |
102 | @property
103 | def url(self):
104 | return "%s%s/{}/%s" % (self._BASE_URL, self.endpoint, self.resource)
105 |
106 | def _convert_output(self, out):
107 | for sym in self.symbols:
108 | out[sym] = self._convert_output_one(out[sym])
109 | return pd.concat(out.values(), keys=out.keys(), axis=1)
110 |
111 | def _convert_output_one(self, out):
112 | df = pd.DataFrame(out)
113 | df = df.set_index(pd.DatetimeIndex(df["datetime"]/1000*10**9))
114 | df = df.drop("datetime", axis=1)
115 | return df
116 |
117 | @auth_check
118 | def execute(self):
119 | result = {}
120 | for sym in self.symbols:
121 | data = self.get(url=self.url.format(sym))["candles"]
122 | FMT = "Price history for {} could not be retrieved"
123 | if not data:
124 | raise ResourceNotFound(message=FMT.format(sym))
125 | result[sym] = data
126 | if len(self.symbols) == 1:
127 | return self._output_format_one(result)
128 | else:
129 | return self._output_format(result)
130 |
131 | def _output_format_one(self, out):
132 | out = out[self.symbols[0]]
133 | if self.output_format == 'json':
134 | return out
135 | elif self.output_format == 'pandas':
136 | return self._convert_output_one(out)
137 | else:
138 | raise ValueError("Please enter a valid output format.")
139 |
--------------------------------------------------------------------------------
/pyTD/market/quotes.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.auth import auth_check
24 | from pyTD.market.base import MarketData
25 | from pyTD.utils import _handle_lists
26 | from pyTD.utils.exceptions import ResourceNotFound
27 |
28 |
29 | class Quotes(MarketData):
30 | """
31 | Class for retrieving data from the Get Quote and Get Quotes endpoints.
32 |
33 | Parameters
34 | ----------
35 | symbols : string, array-like object (list, tuple, Series), or DataFrame
36 | Desired symbols for retrieval
37 | output_format: str, optional, default 'pandas'
38 | Desired output format (json or Pandas DataFrame)
39 | api: pyTD.api.api object, optional
40 | A pyTD api object. If not passed, API requestor defaults to
41 | pyTD.api.default_api
42 | """
43 | def __init__(self, symbols, output_format='pandas', api=None):
44 | self.symbols = _handle_lists(symbols)
45 | if len(self.symbols) > 100:
46 | raise ValueError("Please input a valid symbol or list of up to "
47 | "100 symbols")
48 | super(Quotes, self).__init__(output_format, api)
49 |
50 | @property
51 | def resource(self):
52 | return "quotes"
53 |
54 | @property
55 | def params(self):
56 | return {
57 | "symbol": ','.join(self.symbols)
58 | }
59 |
60 | @auth_check
61 | def execute(self):
62 | data = self.get()
63 | if not data:
64 | raise ResourceNotFound(data, message="Quote for symbol %s not "
65 | "found." % self.symbols)
66 | else:
67 | return self._output_format(data)
68 |
--------------------------------------------------------------------------------
/pyTD/resource.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 | from pyTD import BASE_URL
26 | from pyTD.api import default_api
27 | from pyTD.utils.exceptions import TDQueryError
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 |
32 | class Resource(object):
33 | """
34 | Base class for all REST services
35 | """
36 | _BASE_URL = BASE_URL
37 |
38 | def __init__(self, api=None):
39 | self.api = api or default_api()
40 |
41 | @property
42 | def url(self):
43 | return self._BASE_URL
44 |
45 | @property
46 | def params(self):
47 | return {}
48 |
49 | @property
50 | def data(self):
51 | return {}
52 |
53 | @property
54 | def headers(self):
55 | return {
56 | "content-type": "application/json"
57 | }
58 |
59 |
60 | class Get(Resource):
61 | """
62 | GET requests
63 | """
64 | def get(self, url=None, params=None):
65 | params = params or self.params
66 | url = url or self.url
67 |
68 | response = self.api.request("GET", url=url, params=params)
69 |
70 | # Convert GET requests to JSON
71 | try:
72 | json_data = response.json()
73 | except ValueError:
74 | raise TDQueryError(message="An error occurred during the query.",
75 | response=response)
76 | if "error" in json_data:
77 | raise TDQueryError(response=response)
78 | return json_data
79 |
--------------------------------------------------------------------------------
/pyTD/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/pyTD/tests/__init__.py
--------------------------------------------------------------------------------
/pyTD/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | # flake8: noqa
24 | import pytest
25 |
26 | # fixture routing
27 | from pyTD.tests.fixtures import sample_oid
28 | from pyTD.tests.fixtures import sample_uri
29 | from pyTD.tests.fixtures import valid_refresh_token, valid_access_token
30 | from pyTD.tests.fixtures import set_env, del_env
31 | from pyTD.tests.fixtures import valid_cache, invalid_cache
32 |
33 | # mock responses routing
34 | from pyTD.tests.fixtures.mock_responses import mock_400
35 |
36 |
37 | def pytest_addoption(parser):
38 | parser.addoption(
39 | "--noweb", action="store_true", default=False, help="Ignore web tests"
40 | )
41 |
42 | def pytest_collection_modifyitems(config, items):
43 | if config.getoption("--noweb"):
44 | skip_web = pytest.mark.skip(reason="--noweb option passed. Skipping "
45 | "webtest.")
46 | for item in items:
47 | if "webtest" in item.keywords:
48 | item.add_marker(skip_web)
49 |
--------------------------------------------------------------------------------
/pyTD/tests/fixtures/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 |
25 | import pytest
26 |
27 | from pyTD.auth.tokens import RefreshToken, AccessToken
28 | from pyTD.cache import MemCache
29 | from pyTD.utils import to_timestamp
30 |
31 |
32 | valid_params = {
33 | "token": "validtoken",
34 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
35 | "expires_in": 1000000,
36 | }
37 |
38 | invalid_params = {
39 | "token": "invalidtoken",
40 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
41 | "expires_in": 10000,
42 | }
43 |
44 |
45 | @pytest.fixture(scope='function')
46 | def valid_cache():
47 | r = RefreshToken(valid_params)
48 | a = AccessToken(valid_params)
49 | c = MemCache()
50 | c.refresh_token = r
51 | c.access_token = a
52 | return c
53 |
54 |
55 | @pytest.fixture(scope='function')
56 | def invalid_cache():
57 | r = RefreshToken(invalid_params)
58 | a = AccessToken(invalid_params)
59 | c = MemCache()
60 | c.refresh_token = r
61 | c.access_token = a
62 | return c
63 |
64 |
65 | @pytest.fixture(scope='session')
66 | def valid_refresh_token():
67 | return RefreshToken(valid_params)
68 |
69 |
70 | @pytest.fixture(scope='session')
71 | def valid_access_token():
72 | return AccessToken(valid_params)
73 |
74 |
75 | @pytest.fixture(scope='session', autouse=True)
76 | def sample_oid():
77 | return "TEST10@AMER.OAUTHAP"
78 |
79 |
80 | @pytest.fixture(scope='session', autouse=True)
81 | def sample_uri():
82 | return "https://127.0.0.1:60000/td-callback"
83 |
84 |
85 | @pytest.fixture(scope="function")
86 | def set_env(monkeypatch, sample_oid, sample_uri):
87 | monkeypatch.setenv("TD_CONSUMER_KEY", sample_oid)
88 | monkeypatch.setenv("TD_CALLBACK_URL", sample_uri)
89 |
90 |
91 | @pytest.fixture(scope="function")
92 | def del_env(monkeypatch):
93 | monkeypatch.delenv("TD_CONSUMER_KEY", raising=False)
94 | monkeypatch.delenv("TD_CALLBACK_URL", raising=False)
95 |
--------------------------------------------------------------------------------
/pyTD/tests/fixtures/mock_responses.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.utils.testing import MockResponse
26 |
27 |
28 | @pytest.fixture(scope='function')
29 | def mock_400():
30 | r = MockResponse('{"error":"Bad token."', 400)
31 | return r
32 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_api_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.api import api
24 | from pyTD.auth.tokens import EmptyToken
25 | from pyTD.cache import MemCache
26 |
27 |
28 | class TestAPICache(object):
29 |
30 | def test_api_init_cache(self, set_env, sample_oid, sample_uri):
31 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
32 | store_tokens=False)
33 |
34 | assert isinstance(a.cache, MemCache)
35 | assert isinstance(a.cache.refresh_token, EmptyToken)
36 | assert isinstance(a.cache.access_token, EmptyToken)
37 |
38 | def test_api_pass_cache(self, set_env, sample_oid, sample_uri,
39 | valid_access_token, valid_refresh_token):
40 | c = MemCache()
41 |
42 | c.refresh_token = valid_refresh_token
43 | c.access_token = valid_access_token
44 |
45 | assert valid_access_token.valid is True
46 |
47 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
48 | store_tokens=False, cache=c)
49 |
50 | assert isinstance(a.cache, MemCache)
51 | assert a.cache.refresh_token == c.refresh_token
52 | assert a.cache.access_token == c.access_token
53 |
54 | assert a.cache.access_token.valid is True
55 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_api_integrate.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.instruments import get_instrument
24 | from pyTD.market import get_quotes
25 | from pyTD.api import api
26 |
27 | from pyTD.utils.testing import MockResponse
28 |
29 |
30 | class TestMarketAPI(object):
31 |
32 | def test_quote_arg_api(self, valid_cache, sample_oid, sample_uri):
33 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
34 | cache=valid_cache)
35 | r = MockResponse('{"symbol":"AAPL","quote":155.34}', 200)
36 |
37 | a.request = lambda s, *a, **k: r
38 |
39 | q = get_quotes("AAPL", api=a, output_format='json')
40 |
41 | assert isinstance(q, dict)
42 | assert q["symbol"] == "AAPL"
43 |
44 | def test_instrument_arg_api(self, valid_cache, sample_oid, sample_uri):
45 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
46 | cache=valid_cache)
47 |
48 | r = MockResponse('{"symbol":"ORCL"}', 200)
49 |
50 | a.request = lambda s, *a, **k: r
51 |
52 | i = get_instrument("68389X105", api=a, output_format='json')
53 |
54 | assert isinstance(i, dict)
55 | assert i["symbol"] == "ORCL"
56 |
--------------------------------------------------------------------------------
/pyTD/tests/integration/test_integrate.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.api import api
26 | from pyTD.market import get_quotes
27 | from pyTD.utils.exceptions import AuthorizationError
28 | from pyTD.utils.testing import MockResponse
29 |
30 |
31 | class TestInvalidToken(object):
32 |
33 | def test_invalid_access_quote(self, valid_cache, sample_oid, sample_uri,
34 | monkeypatch):
35 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
36 | cache=valid_cache)
37 |
38 | r = MockResponse('{"error":"Not Authrorized"}', 401)
39 |
40 | def _mock_handler(self, *args, **kwargs):
41 | return a.handle_response(r)
42 |
43 | a.request = _mock_handler
44 |
45 | with pytest.raises(AuthorizationError):
46 | get_quotes("AAPL", api=a)
47 |
--------------------------------------------------------------------------------
/pyTD/tests/test_helper.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 | import os
25 | import pyTD
26 |
27 |
28 | logging.basicConfig(level=logging.INFO)
29 |
30 | consumer_key = os.getenv("TD_CONSUMER_KEY")
31 | refresh_token = os.getenv("TD_REFRESH_TOKEN")
32 |
33 | if refresh_token is None:
34 | raise EnvironmentError("Must set TD_REFRESH_TOKEN environment variable "
35 | "in order to run tests")
36 |
37 | init_data = {
38 | "token": refresh_token,
39 | "access_time": 10000000,
40 | "expires_in": 99999999999999
41 | }
42 |
43 | refresh_token = pyTD.auth.tokens.RefreshToken(options=init_data)
44 |
45 | cache = pyTD.cache.MemCache()
46 | cache.refresh_token = refresh_token
47 |
48 | pyTD.configure(consumer_key=consumer_key,
49 | callback_url="https://127.0.0.1:65010/td-callback",
50 | cache=cache)
51 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_api.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 | import subprocess
25 |
26 | from pyTD.api import api, default_api, gen_ssl
27 | from pyTD.utils.exceptions import (SSLError, ConfigurationError,
28 | ValidationError, AuthorizationError,
29 | ForbiddenAccess, ResourceNotFound,
30 | ClientError, ServerError)
31 | from pyTD.utils.testing import MockResponse
32 |
33 |
34 | @pytest.fixture(params=[
35 | (400, ValidationError),
36 | (401, AuthorizationError),
37 | (403, ForbiddenAccess),
38 | (404, ResourceNotFound),
39 | (450, ClientError),
40 | (500, ServerError)
41 | ])
42 | def bad_requests(request):
43 | return request.param
44 |
45 |
46 | class TestAPI(object):
47 |
48 | def test_non_global_api(self, sample_oid, sample_uri):
49 |
50 | a = api(consumer_key=sample_oid, callback_url=sample_uri)
51 |
52 | assert a.consumer_key == sample_oid
53 | assert a.callback_url == sample_uri
54 |
55 | assert a.refresh_valid is False
56 | assert a.access_valid is False
57 | assert a.auth_valid is False
58 |
59 | def test_api_passed_dict(self, sample_oid, sample_uri, valid_cache):
60 | params = {
61 | "consumer_key": sample_oid,
62 | "callback_url": sample_uri,
63 | "cache": valid_cache
64 | }
65 |
66 | a = api(params)
67 |
68 | assert a.consumer_key == sample_oid
69 | assert a.callback_url == sample_uri
70 |
71 | assert a.refresh_valid is True
72 | assert a.access_valid is True
73 | assert a.auth_valid is True
74 |
75 |
76 | class TestDefaultAPI(object):
77 |
78 | def test_default_api(self, sample_oid, sample_uri, set_env):
79 |
80 | a = default_api(ignore_globals=True)
81 |
82 | assert a.consumer_key == sample_oid
83 | assert a.callback_url == sample_uri
84 |
85 | assert a.refresh_valid is False
86 | assert a.access_valid is False
87 | assert a.auth_valid is False
88 |
89 | def test_default_api_no_env(self, del_env):
90 |
91 | with pytest.raises(ConfigurationError):
92 | default_api(ignore_globals=True)
93 |
94 |
95 | class sesh(object):
96 |
97 | def __init__(self, response):
98 | self.response = response
99 |
100 | def request(self, *args, **kwargs):
101 | return self.response
102 |
103 |
104 | class TestAPIRequest(object):
105 |
106 | def test_api_request_errors(self, bad_requests):
107 | mockresponse = MockResponse("Error", bad_requests[0])
108 | m_api = default_api(ignore_globals=True)
109 |
110 | m_api.session = sesh(mockresponse)
111 |
112 | with pytest.raises(bad_requests[1]):
113 | m_api.request("GET", "https://none.com")
114 |
115 | def test_bad_oid_request(self):
116 | mockresponse = MockResponse('{"error": "Invalid ApiKey"}', 500)
117 | api = default_api(ignore_globals=True)
118 |
119 | api.session = sesh(mockresponse)
120 |
121 | with pytest.raises(AuthorizationError):
122 | api.request("GET", "https://none.com")
123 |
124 |
125 | class TestGenSSL(object):
126 |
127 | def test_gen_ssl_pass(self, monkeypatch):
128 | monkeypatch.setattr("pyTD.api.subprocess.check_call",
129 | lambda *a, **k: True)
130 | monkeypatch.setattr("pyTD.api.os.chdir", lambda *a, **k: None)
131 | assert gen_ssl(".") is True
132 |
133 | @pytest.mark.skip
134 | def test_gen_ssl_raises(self, monkeypatch):
135 | def mocked_check_call(*args, **kwargs):
136 | raise subprocess.CalledProcessError(1, "openssl")
137 |
138 | monkeypatch.setattr("pyTD.api.subprocess.check_call",
139 | mocked_check_call)
140 | monkeypatch.setattr("pyTD.api.os.chdir", lambda *a, **k: None)
141 |
142 | with pytest.raises(SSLError):
143 | gen_ssl("/path/to/dir")
144 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_auth.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import json
24 | import pytest
25 | from pyTD.compat import MagicMock
26 |
27 | from pyTD.auth import TDAuthManager
28 | from pyTD.cache import MemCache
29 | from pyTD.auth.server import TDAuthServer
30 | from pyTD.auth.tokens import EmptyToken, RefreshToken, AccessToken
31 | from pyTD.utils.exceptions import AuthorizationError
32 | from pyTD.utils.testing import MockResponse
33 |
34 |
35 | @pytest.fixture(scope='function', autouse=True)
36 | def sample_manager(sample_oid, sample_uri, valid_cache):
37 | def dummy(*args, **kwargs):
38 | return None
39 | c = MemCache()
40 | m = TDAuthManager(c, sample_oid, sample_uri)
41 | m._open_browser = dummy
42 | m._start_auth_server = dummy
43 | m._stop_auth_server = dummy
44 | return m
45 |
46 |
47 | @pytest.fixture(scope='function')
48 | def test_auth_response():
49 | return {
50 | "refresh_token": "TESTREFRESHVALUE",
51 | "refresh_token_expires_in": 7776000,
52 | "access_token": "TESTACCESSVALUE",
53 | "expires_in": 1800,
54 | "access_time": 1534976931
55 | }
56 |
57 |
58 | @pytest.fixture(scope='function')
59 | def test_auth_response_bad():
60 | return {
61 | "error": "Bad request"
62 | }
63 |
64 |
65 | class TestAuth(object):
66 |
67 | def test_auth_init(self, sample_manager):
68 |
69 | assert isinstance(sample_manager.refresh_token, EmptyToken)
70 | assert isinstance(sample_manager.access_token, EmptyToken)
71 |
72 | def test_refresh_access_token_no_refresh(self, sample_manager):
73 |
74 | with pytest.raises(AuthorizationError):
75 | sample_manager.refresh_access_token()
76 |
77 | def test_auth_browser_fails(self, sample_manager, test_auth_response_bad):
78 | mock_server = MagicMock(TDAuthServer)
79 | mock_server._wait_for_tokens.return_value = test_auth_response_bad
80 |
81 | sample_manager.auth_server = mock_server
82 |
83 | with pytest.raises(AuthorizationError):
84 | sample_manager.auth_via_browser()
85 |
86 | def test_auth_browser_succeeds(self, sample_oid, sample_uri,
87 | sample_manager, monkeypatch,
88 | test_auth_response,
89 | valid_refresh_token,
90 | valid_access_token):
91 | mock_server = MagicMock(TDAuthServer)
92 | mock_server._wait_for_tokens.return_value = test_auth_response
93 |
94 | sample_manager.auth_server = mock_server
95 |
96 | r1, r2 = sample_manager.auth_via_browser()
97 | assert isinstance(r1, RefreshToken)
98 | assert isinstance(r2, AccessToken)
99 | assert r1.token == "TESTREFRESHVALUE"
100 | assert r2.token == "TESTACCESSVALUE"
101 |
102 | def test_auth_refresh_access_bad_token(self, invalid_cache, monkeypatch,
103 | mock_400, sample_oid, sample_uri):
104 | c = invalid_cache
105 | c.refresh_token.expires_in = 100000000000
106 | monkeypatch.setattr("pyTD.auth.manager.requests.post", lambda *a, **k:
107 | mock_400)
108 |
109 | manager = TDAuthManager(c, sample_oid, sample_uri)
110 | with pytest.raises(AuthorizationError):
111 | manager.refresh_access_token()
112 |
113 | def test_auth_refresh_access(self, test_auth_response, monkeypatch,
114 | sample_oid, sample_uri, valid_cache):
115 |
116 | mocked_response = MockResponse(json.dumps(test_auth_response), 200)
117 | monkeypatch.setattr("pyTD.auth.manager.requests.post", lambda *a, **k:
118 | mocked_response)
119 |
120 | manager = TDAuthManager(valid_cache, sample_oid, sample_uri)
121 | manager.refresh_access_token()
122 | assert manager.access_token.token == "TESTACCESSVALUE"
123 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_cache.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 | from pyTD.cache import MemCache
26 | from pyTD.auth.tokens import EmptyToken
27 |
28 |
29 | @pytest.fixture(scope='function', autouse=True)
30 | def full_consumer_key():
31 | return "TEST@AMER.OAUTHAP"
32 |
33 |
34 | class TestMemCache(object):
35 |
36 | def test_default_values(self):
37 | c = MemCache()
38 |
39 | assert isinstance(c.refresh_token, EmptyToken)
40 | assert isinstance(c.access_token, EmptyToken)
41 |
42 | def test_set_token(self, valid_refresh_token):
43 | c = MemCache()
44 | c.refresh_token = valid_refresh_token
45 |
46 | assert c.refresh_token.token == "validtoken"
47 | assert c.refresh_token.expires_in == 1000000
48 |
49 | def test_clear(self, valid_refresh_token, valid_access_token):
50 | c = MemCache()
51 | c.refresh_token = valid_refresh_token
52 | c.access_token == valid_access_token
53 |
54 | c.clear()
55 | assert isinstance(c.refresh_token, EmptyToken)
56 | assert isinstance(c.access_token, EmptyToken)
57 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_exceptions.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.utils.exceptions import (AuthorizationError,
24 | ConfigurationError,
25 | SSLError)
26 |
27 |
28 | class TestExceptions(object):
29 |
30 | def test_auth_error(self):
31 | error = AuthorizationError("Authorization failed.")
32 | assert str(error) == "Authorization failed."
33 |
34 | def test_config_error(self):
35 | error = ConfigurationError("Configuration failed.")
36 | assert str(error) == "Configuration failed."
37 |
38 | def test_ssl_error(self):
39 | error = SSLError("SSL failed.")
40 | assert str(error) == "SSL failed."
41 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_instruments.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pandas as pd
24 | import pytest
25 |
26 | from pyTD.tests.test_helper import pyTD
27 |
28 | ResourceNotFound = pyTD.utils.exceptions.ResourceNotFound
29 | TDQueryError = pyTD.utils.exceptions.TDQueryError
30 |
31 |
32 | @pytest.mark.webtest
33 | class TestInstrument(object):
34 |
35 | def test_instrument_no_symbol(self):
36 | with pytest.raises(TypeError):
37 | pyTD.instruments.get_instrument()
38 |
39 | def test_instrument_bad_instrument(self):
40 | with pytest.raises(ResourceNotFound):
41 | pyTD.instruments.get_instrument("BADINSTRUMENT")
42 |
43 | def test_instrument_cusip(self):
44 | cusip = "68389X105"
45 | data = pyTD.instruments.get_instrument(cusip,
46 | output_format='json')[cusip]
47 |
48 | assert isinstance(data, dict)
49 |
50 | assert data["symbol"] == "ORCL"
51 | assert data["exchange"] == "NYSE"
52 |
53 | def test_instruments_cusip(self):
54 | cusip = "17275R102"
55 | data = pyTD.instruments.get_instruments(cusip,
56 | output_format='json')[cusip]
57 |
58 | assert isinstance(data, dict)
59 |
60 | assert data["symbol"] == "CSCO"
61 | assert data["exchange"] == "NASDAQ"
62 |
63 | def test_instrument_cusp_pandas(self):
64 | data = pyTD.instruments.get_instrument("68389X105").T
65 |
66 | assert isinstance(data, pd.DataFrame)
67 |
68 | assert len(data) == 1
69 | assert len(data.columns) == 5
70 | assert data.iloc[0]["symbol"] == "ORCL"
71 |
72 | def test_instrument_symbol(self):
73 | data = pyTD.instruments.get_instrument("AAPL").T
74 |
75 | assert isinstance(data, pd.DataFrame)
76 |
77 | assert len(data) == 1
78 | assert len(data.columns) == 5
79 | assert data.iloc[0]["symbol"] == "AAPL"
80 |
81 | def test_instruments_fundamental(self):
82 | data = pyTD.instruments.get_instruments("AAPL",
83 | projection="fundamental").T
84 |
85 | assert isinstance(data, pd.DataFrame)
86 |
87 | assert len(data.columns) == 46
88 | assert data.iloc[0]["symbol"] == "AAPL"
89 |
90 | def test_instruments_regex(self):
91 | data = pyTD.instruments.get_instruments("AAP.*",
92 | projection="symbol-regex").T
93 |
94 | assert isinstance(data, pd.DataFrame)
95 |
96 | assert data.shape == (13, 5)
97 |
98 | def test_instruments_bad_projection(self):
99 | with pytest.raises(TDQueryError):
100 | pyTD.instruments.get_instruments("AAPL",
101 | projection="BADPROJECTION")
102 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_market.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pandas as pd
25 | import pytest
26 |
27 | from pyTD.tests.test_helper import pyTD
28 |
29 | TDQueryError = pyTD.utils.exceptions.TDQueryError
30 | ResourceNotFound = pyTD.utils.exceptions.ResourceNotFound
31 |
32 |
33 | @pytest.fixture(scope='session', autouse=True)
34 | def now():
35 | return datetime.datetime.now()
36 |
37 |
38 | @pytest.mark.webtest
39 | class TestMarketExceptions(object):
40 |
41 | def test_get_quotes_bad_symbol(self):
42 | with pytest.raises(TypeError):
43 | pyTD.market.get_quotes()
44 |
45 | def test_get_movers_bad_index(self):
46 | with pytest.raises(TypeError):
47 | pyTD.market.get_movers()
48 |
49 | with pytest.raises(TDQueryError):
50 | pyTD.market.get_movers("DJI")
51 |
52 |
53 | @pytest.mark.webtest
54 | class TestQuotes(object):
55 |
56 | def test_quotes_json_single(self):
57 | aapl = pyTD.market.get_quotes("AAPL", output_format='json')
58 | assert isinstance(aapl, dict)
59 | assert "AAPL" in aapl
60 |
61 | def test_quotes_json_multiple(self):
62 | aapl = pyTD.market.get_quotes(["AAPL", "TSLA"],
63 | output_format='json')
64 | assert isinstance(aapl, dict)
65 | assert len(aapl) == 2
66 | assert "TSLA" in aapl
67 |
68 | def test_quotes_pandas(self):
69 | df = pyTD.market.get_quotes("AAPL")
70 | assert isinstance(df, pd.DataFrame)
71 | assert "AAPL" in df
72 |
73 | assert len(df) == 41
74 |
75 | def test_quotes_bad_symbol(self):
76 | with pytest.raises(ResourceNotFound):
77 | pyTD.market.get_quotes("BADSYMBOL")
78 |
79 | def test_quotes_bad_params(self):
80 | bad = ["AAPL"] * 1000
81 | with pytest.raises(ValueError):
82 | pyTD.market.get_quotes(bad)
83 |
84 |
85 | @pytest.mark.webtest
86 | class TestMarketMovers(object):
87 |
88 | @pytest.mark.xfail(reason="Movers may return empty outside of "
89 | "trading hours.")
90 | def test_movers_json(self):
91 | data = pyTD.market.get_movers("$DJI", output_format='json')
92 | assert isinstance(data, list)
93 | assert len(data) == 10
94 |
95 | @pytest.mark.xfail(reason="Movers may return empty outside of "
96 | "trading hours.")
97 | def test_movers_pandas(self):
98 | data = pyTD.market.get_movers("$DJI")
99 | assert isinstance(data, pd.DataFrame)
100 | assert len(data) == 10
101 |
102 | def test_movers_bad_index(self):
103 | with pytest.raises(ResourceNotFound):
104 | pyTD.market.get_movers("DJI")
105 |
106 | def test_movers_no_params(self):
107 | with pytest.raises(TypeError):
108 | pyTD.market.get_movers()
109 |
110 |
111 | @pytest.mark.webtest
112 | class TestMarketHours(object):
113 |
114 | def test_hours_bad_market(self):
115 | with pytest.raises(ValueError):
116 | pyTD.market.get_market_hours("BADMARKET")
117 |
118 | with pytest.raises(ValueError):
119 | pyTD.market.get_market_hours(["BADMARKET", "EQUITY"])
120 |
121 | def test_hours_default(self):
122 | data = pyTD.market.get_market_hours()
123 |
124 | assert len(data) == 8
125 | assert data.index[0] == "category"
126 |
127 | def test_hours_json(self):
128 | date = now()
129 | data = pyTD.market.get_market_hours("EQUITY", date,
130 | output_format='json')
131 | assert isinstance(data, dict)
132 |
133 | def test_hours_pandas(self):
134 | date = now()
135 | data = pyTD.market.get_market_hours("EQUITY", date)
136 | assert isinstance(data, pd.DataFrame)
137 | assert data.index[0] == "category"
138 |
139 | def test_hours_batch(self):
140 | data = pyTD.market.get_market_hours(["EQUITY", "OPTION"])
141 |
142 | assert len(data) == 8
143 | assert isinstance(data["equity"], pd.Series)
144 |
145 |
146 | @pytest.mark.webtest
147 | class TestOptionChains(object):
148 |
149 | def test_option_chain_no_symbol(self):
150 | with pytest.raises(TypeError):
151 | pyTD.market.get_option_chains()
152 |
153 | def test_option_chain_bad_symbol(self):
154 | with pytest.raises(ResourceNotFound):
155 | pyTD.market.get_option_chains("BADSYMBOL")
156 |
157 | def test_option_chain(self):
158 | data = pyTD.market.get_option_chains("AAPL", output_format='json')
159 |
160 | assert isinstance(data, dict)
161 | assert len(data) == 13
162 | assert data["status"] == "SUCCESS"
163 |
164 | def test_option_chain_call(self):
165 | data = pyTD.market.get_option_chains("AAPL", contract_type="CALL",
166 | output_format='json')
167 |
168 | assert not data["putExpDateMap"]
169 |
170 | def test_option_chain_put(self):
171 | data = pyTD.market.get_option_chains("AAPL", contract_type="PUT",
172 | output_format='json')
173 |
174 | assert not data["callExpDateMap"]
175 |
176 |
177 | @pytest.mark.webtest
178 | class TestPriceHistory(object):
179 |
180 | def test_price_history_no_symbol(self):
181 | with pytest.raises(TypeError):
182 | pyTD.market.get_price_history()
183 |
184 | def test_price_history_default_dates(self):
185 | data = pyTD.market.get_price_history("AAPL", output_format='json')
186 |
187 | assert isinstance(data, list)
188 |
189 | def test_price_history_bad_symbol(self):
190 | with pytest.raises(ResourceNotFound):
191 | pyTD.market.get_price_history("BADSYMBOL")
192 |
193 | def test_price_history_bad_symbols(self):
194 | with pytest.raises(ResourceNotFound):
195 | pyTD.market.get_price_history(["BADSYMBOL", "BADSYMBOL"],
196 | output_format='pandas')
197 |
198 | def test_price_history_json(self):
199 | data = pyTD.market.get_price_history("AAPL", output_format='json')
200 |
201 | assert isinstance(data, list)
202 | assert data[0]["close"] == 172.26
203 | assert data[0]["volume"] == 25555934
204 | assert len(data) > 100
205 |
206 | def test_batch_history_json(self):
207 | syms = ["AAPL", "TSLA", "MSFT"]
208 | data = pyTD.market.get_price_history(syms, output_format='json')
209 |
210 | assert len(data) == 3
211 | assert set(data) == set(syms)
212 |
213 | def test_price_history_pandas(self):
214 | data = pyTD.market.get_price_history("AAPL")
215 |
216 | assert isinstance(data, pd.DataFrame)
217 |
218 | def test_batch_history_pandas(self):
219 | data = pyTD.market.get_price_history(["AAPL", "TSLA", "MSFT"],
220 | output_format='pandas')
221 |
222 | assert isinstance(data, pd.DataFrame)
223 | assert isinstance(data.columns, pd.MultiIndex)
224 |
225 | assert "AAPL" in data.columns
226 | assert "TSLA" in data.columns
227 | assert "MSFT" in data.columns
228 |
229 | assert data.iloc[0].name.date() == datetime.date(2018, 1, 2)
230 |
231 | @pytest.mark.xfail(reason="Odd behavior on travis: wrong dates returned")
232 | def test_history_dates(self):
233 | start = datetime.date(2018, 1, 24)
234 | end = datetime.date(2018, 2, 12)
235 |
236 | data = pyTD.market.get_price_history("AAPL", start_date=start,
237 | end_date=end,
238 | output_format='pandas')
239 |
240 | assert data.iloc[0].name.date() == start
241 | assert data.iloc[-1].name.date() == datetime.date(2018, 2, 9)
242 |
243 | assert pd.infer_freq(data.index) == "B"
244 |
245 |
246 | @pytest.mark.webtest
247 | class TestFundamentals(object):
248 |
249 | def test_fundamentals(self):
250 | data = pyTD.market.get_fundamentals("AAPL")
251 |
252 | assert isinstance(data, pd.DataFrame)
253 | assert len(data) == 46
254 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_resource.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import pytest
24 |
25 |
26 | from pyTD.api import api
27 | from pyTD.resource import Get
28 | from pyTD.utils.exceptions import TDQueryError
29 | from pyTD.utils.testing import MockResponse
30 |
31 |
32 | @pytest.fixture(params=[
33 | MockResponse("", 200),
34 | MockResponse('{"error":"Not Found."}', 200)], ids=[
35 | "Empty string",
36 | '"Error" in response',
37 | ])
38 | def bad_json(request):
39 | return request.param
40 |
41 |
42 | class TestResource(object):
43 |
44 | def test_get_raises_json(self, bad_json, valid_cache,
45 | sample_oid, sample_uri):
46 | a = api(consumer_key=sample_oid, callback_url=sample_uri,
47 | cache=valid_cache)
48 | a.request = lambda s, *a, **k: bad_json
49 | resource = Get(api=a)
50 |
51 | with pytest.raises(TDQueryError):
52 | resource.get()
53 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_tokens.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pytest
25 |
26 | from pyTD.auth.tokens import RefreshToken, AccessToken, EmptyToken
27 | from pyTD.utils import to_timestamp
28 |
29 |
30 | valid_params = {
31 | "token": "validtoken",
32 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
33 | "expires_in": 1000000,
34 | }
35 |
36 | invalid_params = {
37 | "token": "invalidtoken",
38 | "access_time": to_timestamp(datetime.datetime.now()) - 15000,
39 | "expires_in": 10000,
40 | }
41 |
42 |
43 | @pytest.fixture(params=[
44 | RefreshToken,
45 | AccessToken
46 | ], ids=["Refresh", "Access"], scope='function')
47 | def tokens(request):
48 | return request.param
49 |
50 |
51 | @pytest.fixture(params=[
52 | (valid_params, True),
53 | (invalid_params, False)
54 | ], ids=["valid", "invalid"])
55 | def validity(request):
56 | return request.param
57 |
58 |
59 | class TestTokens(object):
60 |
61 | def test_empty_token(self):
62 | t = EmptyToken()
63 |
64 | assert t.valid is False
65 |
66 | def test_new_token_dict_validity(self, tokens, validity):
67 | t = tokens(validity[0])
68 |
69 | assert t.valid is validity[1]
70 |
71 | assert t.token == validity[0]["token"]
72 | assert t.access_time == validity[0]["access_time"]
73 | assert t.expires_in == validity[0]["expires_in"]
74 |
75 | def test_new_token_params_validity(self, tokens, validity):
76 | t = tokens(**validity[0])
77 |
78 | assert t.valid is validity[1]
79 |
80 | assert t.token == validity[0]["token"]
81 | assert t.access_time == validity[0]["access_time"]
82 | assert t.expires_in == validity[0]["expires_in"]
83 |
84 | def test_token_equality(self, valid_refresh_token):
85 | token1 = RefreshToken(token="token", access_time=1, expires_in=1)
86 | token2 = RefreshToken({"token": "token", "access_time": 1,
87 | "expires_in": 1})
88 | assert token1 == token2
89 |
--------------------------------------------------------------------------------
/pyTD/tests/unit/test_utils.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import datetime
24 | import pytest
25 | import pandas as pd
26 |
27 | from pyTD.utils import _handle_lists, _sanitize_dates
28 |
29 |
30 | @pytest.fixture(params=[
31 | 1,
32 | "test"
33 | ], ids=[
34 | "int",
35 | "string"
36 | ])
37 | def single(request):
38 | return request.param
39 |
40 |
41 | @pytest.fixture(params=[
42 | [1, 2, 3],
43 | (1, 2, 3),
44 | pd.DataFrame([], index=[1, 2, 3]),
45 | pd.Series([1, 2, 3]),
46 | ], ids=[
47 | "list",
48 | "tuple",
49 | "DataFrame",
50 | "Series"
51 | ])
52 | def mult(request):
53 | return request.param
54 |
55 |
56 | class TestUtils(object):
57 |
58 | def test_handle_lists_sing(self, single):
59 | assert _handle_lists(single, mult=False) == single
60 | assert _handle_lists(single) == [single]
61 |
62 | def test_handle_lists_mult(self, mult):
63 | assert _handle_lists(mult) == [1, 2, 3]
64 |
65 | def test_handle_lists_err(self, mult):
66 | with pytest.raises(ValueError):
67 | _handle_lists(mult, mult=False)
68 |
69 | def test_sanitize_dates_years(self):
70 | expected = (datetime.datetime(2017, 1, 1),
71 | datetime.datetime(2018, 1, 1))
72 | assert _sanitize_dates(2017, 2018) == expected
73 |
74 | def test_sanitize_dates_default(self):
75 | exp_start = datetime.datetime(2017, 1, 1, 0, 0)
76 | exp_end = datetime.datetime.today()
77 | start, end = _sanitize_dates(None, None)
78 |
79 | assert start == exp_start
80 | assert end.date() == exp_end.date()
81 |
82 | def test_sanitize_dates(self):
83 | start = datetime.datetime(2017, 3, 4)
84 | end = datetime.datetime(2018, 3, 9)
85 |
86 | assert _sanitize_dates(start, end) == (start, end)
87 |
88 | def test_sanitize_dates_error(self):
89 | start = datetime.datetime(2018, 1, 1)
90 | end = datetime.datetime(2017, 1, 1)
91 |
92 | with pytest.raises(ValueError):
93 | _sanitize_dates(start, end)
94 |
--------------------------------------------------------------------------------
/pyTD/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 | import requests
25 | import datetime as dt
26 | import time
27 |
28 | import pandas as pd
29 | from pandas import to_datetime
30 | import pandas.compat as compat
31 |
32 | from pyTD.compat import is_number
33 |
34 |
35 | logger = logging.getLogger(__name__)
36 |
37 |
38 | def bprint(msg):
39 | return color.BOLD + msg + color.ENDC
40 |
41 |
42 | class color:
43 | HEADER = '\033[95m'
44 | OKBLUE = '\033[94m'
45 | OKGREEN = '\033[92m'
46 | WARNING = '\033[93m'
47 | FAIL = '\033[91m'
48 | ENDC = '\033[0m'
49 | BOLD = '\033[1m'
50 | UNDERLINE = '\033[4m'
51 | GRN = '\x1B[32m'
52 | RED = '\x1B[31m'
53 |
54 |
55 | def gprint(msg):
56 | return(color.GRN + msg + color.ENDC)
57 |
58 |
59 | def _handle_lists(l, mult=True, err_msg=None):
60 | if isinstance(l, (compat.string_types, int)):
61 | return [l] if mult is True else l
62 | elif isinstance(l, pd.DataFrame) and mult is True:
63 | return list(l.index)
64 | elif mult is True:
65 | return list(l)
66 | else:
67 | raise ValueError(err_msg or "Only 1 symbol/market parameter allowed.")
68 |
69 |
70 | def _init_session(session):
71 | if session is None:
72 | session = requests.session()
73 | return session
74 |
75 |
76 | def input_require(msg):
77 | result = ''
78 | while result == '':
79 | result = input(msg)
80 | return result
81 |
82 |
83 | def rprint(msg):
84 | return(color.BOLD + color.RED + msg + color.ENDC)
85 |
86 |
87 | def _sanitize_dates(start, end, set_defaults=True):
88 | """
89 | Return (datetime_start, datetime_end) tuple
90 | if start is None - default is 2017/01/01
91 | if end is None - default is today
92 |
93 | Borrowed from Pandas DataReader
94 | """
95 | if is_number(start):
96 | # regard int as year
97 | start = dt.datetime(start, 1, 1)
98 | start = to_datetime(start)
99 |
100 | if is_number(end):
101 | end = dt.datetime(end, 1, 1)
102 | end = to_datetime(end)
103 |
104 | if set_defaults is True:
105 | if start is None:
106 | start = dt.datetime(2017, 1, 1, 0, 0)
107 | if end is None:
108 | end = dt.datetime.today()
109 | if start and end:
110 | if start > end:
111 | raise ValueError('start must be an earlier date than end')
112 | return start, end
113 |
114 |
115 | def to_timestamp(date):
116 | return int(time.mktime(date.timetuple()))
117 |
118 |
119 | def yn_require(msg):
120 | template = "{} [y/n]: "
121 | result = ''
122 | YES = ["y", "Y", "Yes", "yes"]
123 | NO = ["n", "N", "No", "no"]
124 | while result not in YES and result not in NO:
125 | result = input_require(template.format(msg))
126 | if result in YES:
127 | return True
128 | if result in NO:
129 | return False
130 |
--------------------------------------------------------------------------------
/pyTD/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | import logging
24 |
25 |
26 | logger = logging.getLogger(__name__)
27 |
28 |
29 | class AuthorizationError(Exception):
30 | """
31 | This error is thrown when an error with authorization occurs
32 | """
33 |
34 | def __init__(self, msg):
35 | self.msg = msg
36 |
37 | def __str__(self):
38 | return self.msg
39 |
40 |
41 | class SSLError(AuthorizationError):
42 | pass
43 |
44 |
45 | class TDQueryError(Exception):
46 | def __init__(self, response=None, content=None, message=None):
47 | self.response = response
48 | self.content = content
49 | self.message = message
50 |
51 | def __str__(self):
52 | message = self.message
53 | if hasattr(self.response, 'status_code'):
54 | message += " Response status: %s." % (self.response.status_code)
55 | if hasattr(self.response, 'reason'):
56 | message += " Response message: %s." % (self.response.reason)
57 | if hasattr(self.response, 'text'):
58 | message += " Response text: %s." % (self.response.text)
59 | if hasattr(self.response, 'url'):
60 | message += " Request URL: %s." % (self.response.url)
61 | if self.content is not None:
62 | message += " Error message: %s" % str(self.content)
63 | return message
64 |
65 |
66 | class ClientError(TDQueryError):
67 |
68 | _DEFAULT = "There was a client error with your request."
69 |
70 | def __init__(self, response, content=None, message=None):
71 | pass
72 |
73 |
74 | class ServerError(TDQueryError):
75 | pass
76 |
77 |
78 | class ResourceNotFound(TDQueryError):
79 | pass
80 |
81 |
82 | class ValidationError(TDQueryError):
83 | pass
84 |
85 |
86 | class ForbiddenAccess(TDQueryError):
87 | pass
88 |
89 |
90 | class ConnectionError(TDQueryError):
91 | pass
92 |
93 |
94 | class Redirection(TDQueryError):
95 | pass
96 |
97 |
98 | class ConfigurationError(Exception):
99 | def __init__(self, message):
100 | self.message = message
101 |
102 | def __str__(self):
103 | return self.message
104 |
105 |
106 | class CacheError(Exception):
107 | pass
108 |
--------------------------------------------------------------------------------
/pyTD/utils/testing.py:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | # Copyright (c) 2018 Addison Lynch
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from pyTD.api import default_api
24 | from pyTD.compat import HTTPError
25 | from pyTD.utils.exceptions import ConfigurationError
26 |
27 |
28 | def default_auth_ok():
29 | """
30 | Used for testing. Returns true if a default API object is authorized
31 | """
32 | global __api__
33 | if __api__ is None:
34 | try:
35 | a = default_api()
36 | return a.auth_valid
37 | except ConfigurationError:
38 | return False
39 | else:
40 | if __api__.refresh_valid is True:
41 | return True
42 | else:
43 | return False
44 |
45 |
46 | class MockResponse(object):
47 | """
48 | Class for mocking HTTP response objects
49 | """
50 |
51 | def __init__(self, text, status_code, request_url=None,
52 | request_params=None, request_headers=None):
53 | """
54 | Initialize the class
55 |
56 | Parameters
57 | ----------
58 | text: str
59 | A plaintext string of the response
60 | status_code: int
61 | HTTP response code
62 | url: str, optional
63 | Request URL
64 | request_params: dict, optional
65 | Request Parameters
66 | request_headers: dict, optional
67 | Request headers
68 | """
69 | self.text = text
70 | self.status_code = status_code
71 | self.url = request_url
72 | self.request_params = request_params
73 | self.request_headers = request_headers
74 |
75 | def json(self):
76 | import json
77 | return json.loads(self.text)
78 |
79 | def raise_for_status(self):
80 | # Pulled directly from requests source code
81 | reason = ''
82 | http_error_msg = ''
83 | if 400 <= self.status_code < 500:
84 | http_error_msg = u'%s Client Error: %s for url: %s' % (
85 | self.status_code, reason, self.url)
86 |
87 | elif 500 <= self.status_code < 600:
88 | http_error_msg = u'%s Server Error: %s for url: %s' % (
89 | self.status_code, reason, self.url)
90 |
91 | if http_error_msg:
92 | raise HTTPError(http_error_msg, response=self)
93 |
94 |
95 | MOCK_SSL_CERT = """\
96 | -----BEGIN CERTIFICATE-----
97 | MIIDtTCCAp2gAwIBAgIJAPuEP7NccyjCMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
98 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
99 | aWRnaXRzIFB0eSBMdGQwHhcNMTgwNzAzMDIzODMwWhcNMTkwNzAzMDIzODMwWjBF
100 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
101 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
102 | CgKCAQEA/S/ocvpHNqQvuVtKqZi4JJbWRmw0hG2rS8NwXsn7YBkvPydvc9+CX5ZC
103 | Tdt93Hh2g6t07+EDjFQdWzuD1paKoLsjI3RTGM9OhY25AF13jsgdCORSetKiAuQy
104 | zKWtzLJ7egfjj8ZQdaUKhRONqLYu8IbtcQFuuL+B49xwPIfafMCmy6US/R6maCTH
105 | zeIw8LahV4ECM9NttfIJTkEkN/O8D30rJVZbpMhJHq+Y4rh94oBVW4JJMc+VZlHi
106 | C9d6E9yIiUtcKSsOZkZ3FL0TNEm2dmzI69wufC53B6NynYFVA0yhtvRgOZYdoFX6
107 | cMhk3Ciy7nFav+fdZ4PsJirATjtisQIDAQABo4GnMIGkMB0GA1UdDgQWBBRtfob1
108 | mHz0mr5YHvSYQ728X4Sz7zB1BgNVHSMEbjBsgBRtfob1mHz0mr5YHvSYQ728X4Sz
109 | 76FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
110 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPuEP7NccyjCMAwGA1UdEwQF
111 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAMUq5ZcfIzJF4nk3HqHxyajJJZNUarTU
112 | aizCqDcLSU+SgcrsrVu51s5OGpK+HhwwkY5uq5C1yv0tYc7e0V9e/dpANvUR5RMv
113 | Tme60HfJKioqhzSaNSz87a3TZayYhnREVfA6UqVL6EQ2ArVeqnn+mmrZ/oU5TJ9T
114 | Opwr8Kah78xnC/0iOWOR4IXliakNHdO0qqJIYlbpBxM7znYT6vPbvp/IQC7PA8qP
115 | AMce1keJ5u462aCza6zp95sFhqneDlI9lh9EA31eUfPgvdNPfqQP40DCQGSnvdeU
116 | fPm9pF9V4FSlznPyRJI4AgZOqpt580+GWTtYQBwPCqZSHq66f83Lmz4=
117 | -----END CERTIFICATE-----
118 | """
119 |
120 |
121 | MOCK_SSL_KEY = """\
122 | -----BEGIN RSA PRIVATE KEY-----
123 | MIIEowIBAAKCAQEA/S/ocvpHNqQvuVtKqZi4JJbWRmw0hG2rS8NwXsn7YBkvPydv
124 | c9+CX5ZCTdt93Hh2g6t07+EDjFQdWzuD1paKoLsjI3RTGM9OhY25AF13jsgdCORS
125 | etKiAuQyzKWtzLJ7egfjj8ZQdaUKhRONqLYu8IbtcQFuuL+B49xwPIfafMCmy6US
126 | /R6maCTHzeIw8LahV4ECM9NttfIJTkEkN/O8D30rJVZbpMhJHq+Y4rh94oBVW4JJ
127 | Mc+VZlHiC9d6E9yIiUtcKSsOZkZ3FL0TNEm2dmzI69wufC53B6NynYFVA0yhtvRg
128 | OZYdoFX6cMhk3Ciy7nFav+fdZ4PsJirATjtisQIDAQABAoIBAQChwFKj6gtG+FvY
129 | 8l7fvMaf8ZGRSh2/IQVXkNOgay/idBSAJ2SHxZpYEPnpHbnp+TfV5Nr/SWTn6PEc
130 | UQhoNqL4DrZjNzTDW+XRYvp3Jj90g5oxDRU4jIqeiEWAArTnWnuSOaoDN3I9xqPS
131 | 4uwUhde1KK5XDNA8zXRhK3q04SIPogtgyzYY9D+6TVF/F+34jhFG6TDjnuIP9PwG
132 | l6eY+b7q1zspcqAXFXVJ5xxhkI79zmH0SoVKEz7VAtqdDi3dKfsInexjiLET4ibV
133 | YcBgW0PRA0ZDw10EOjDAOZBzr1jitUuQ3VJI8XaWQaWt33tD7iVEdwDJt88w0YIc
134 | bgtlIIXlAoGBAP63Fl8OWhgtcjVg/+idQg08HM/Tv6Ri/jtpWTnPmQolW8bCqB7M
135 | SIc0DkHKluqTzTkNFD890WgKGTjV5UFXMFREtrRQJuycIfHg0FvGCtpYVjtqqgjn
136 | 0IVHfGVJ5Q3mFeSqMj8cheb5Nk767P66gd2gTLTFgca0Wh1Vf1ykBY3DAoGBAP52
137 | 2PMXrTBKsssXGPmA4/0HVvd1f0JzEH4ithhYwSvkNfwv+EdW8hriNVj5LL4sMC4j
138 | P2hZKC7c39paG4MVvBHQ9AhgrH97VXxFzjIECTx9VINyR3yxT5Nqn6ilmTR1gmty
139 | gdlEztFVloUlGrfHh8cGTGI6J7eYFCnk7NzGrEJ7AoGANu7ZfkqkF47FkMmIp2wy
140 | 8JPESvYJ4LQQzFNeEN+6y7te3bDhfTLleXM6l+nPPmv92I3/jdwRK3TyF5XZyYu6
141 | OpJPLPgUTPcnQvkPNpuxf4GJp2rLnPwRtozCQT38jlDO6+/gwkeugS/CDKqFLjKf
142 | C2Mk59+oq2f9/1GPFDWzlO0CgYAW8XZMLMVTxlhqkVGSJXno9YF03GY2ApPpG44Z
143 | kd8Q6wmnDFgxbnhzzhOLSyQqnWdWsZzk9qz11LpmQJucbRhA7vshyj2jXOZvRwf5
144 | YH3Is3AsTeB+MKqBGyr8FLpEjZfNwkxM37RaEYJ5zMek7FukqT+315B/MDoZMOfe
145 | XBdqAwKBgD1CoyKb7Cgcb8zEHMkVAPP7tljpO1/gzuXRSOp7G4blKK+fF40vSh79
146 | azBtciC6VbBwUPRW4OY9qPqhOMA3DAgeJZBrCrEkQVHWqW2u0FOdJsMDz5TpDQSV
147 | cHy9ZQCz9WDroSC21Z0BFJ8DKPXvFL/XjlCtpfBP7JFoAChm5MeW
148 | -----END RSA PRIVATE KEY-----
149 | """
150 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | markers =
3 | webtest: mark a test as a webtest
4 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | flake8
2 | ipython
3 | matplotlib
4 | mock
5 | pytest
6 | pytest-runner
7 | requests-cache
8 | six
9 | sphinx
10 | sphinx-autobuild
11 | sphinx-rtd-theme
12 | sphinxcontrib-napoleon
13 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pandas
2 | requests
3 |
--------------------------------------------------------------------------------
/scripts/get_refresh_token.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | """
4 | Utility script to return cached refresh token if one exists given current
5 | environment configuration
6 |
7 | Attempts to find configuration file in configuration directory (located in
8 | either the value of TD_CONFIG_DIR or the default ~/.tdm). Raises an exception
9 | if a refresh token cannot be found or is not valid (expired or malformed)
10 | """
11 | import datetime
12 | import json
13 | import os
14 | import time
15 |
16 | CONFIG_DIR = os.getenv("TD_CONFIG_DIR") or os.path.expanduser("~/.tdm")
17 |
18 | CONSUMER_KEY = os.getenv("TD_CONSUMER_KEY")
19 | # Must have consumer key to locate DiskCache file
20 | if CONSUMER_KEY is None:
21 | raise ValueError("Environment variable TD_CONSUMER_KEY must be set")
22 |
23 | CONFIG_PATH = os.path.join(CONFIG_DIR, CONSUMER_KEY)
24 |
25 | try:
26 | with open(CONFIG_PATH, 'r') as f:
27 | json_data = json.load(f)
28 | refresh_token = json_data["refresh_token"]
29 | token = refresh_token["token"]
30 | now = datetime.datetime.now()
31 | now = int(time.mktime(now.timetuple()))
32 | access = int(refresh_token["access_time"])
33 | expires = int(refresh_token["expires_in"])
34 | expiry = access + expires
35 | if expiry > now:
36 | print(token)
37 | else:
38 | raise Exception("Refresh token expired")
39 | except Exception:
40 | raise Exception("Refresh token could not be retrieved")
41 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # Obtain refresh token and store
4 | A=$(python get_refresh_token.py)
5 | export TD_REFRESH_TOKEN=$A
6 |
7 | cd ..
8 |
9 | # flake8 check
10 | echo "flake8 check..."
11 | flake8 pyTD
12 | rc=$?; if [[ $rc != 0 ]]; then
13 | echo "flake8 check failed."
14 | exit $rc;
15 | fi
16 | echo "PASSED"
17 |
18 | # flake8-rst check
19 | echo "flake8-rst docs check..."
20 | flake8-rst --filename="*.rst" .
21 | rc=$?; if [[ $rc != 0 ]]; then
22 | echo "flake8-rst docs check failed."
23 | exit $rc;
24 | fi
25 | echo "PASSED"
26 |
27 | # run all tests
28 | echo "pytest..."
29 | cd pyTD
30 | pytest -x tests
31 | rc=$?;
32 |
33 | if [[ $rc != 0 ]]; then
34 | echo "Pytest failed."
35 | exit $rc
36 | fi
37 | echo "PASSED"
38 |
39 | echo 'All tests passed!'
40 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8-rst]
2 | ignore = E402
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import codecs
3 | import os
4 | import re
5 |
6 | here = os.path.abspath(os.path.dirname(__file__))
7 |
8 |
9 | def find_version(*file_paths):
10 | """Read the version number from a source file.
11 | Why read it, and not import?
12 | see https://groups.google.com/d/topic/pypa-dev/0PkjVpcxTzQ/discussion
13 | """
14 | # Open in Latin-1 so that we avoid encoding errors.
15 | # Use codecs.open for Python 2 compatibility
16 | with codecs.open(os.path.join(here, *file_paths), 'r', 'latin1') as f:
17 | version_file = f.read()
18 |
19 | # The version line must have the form
20 | # __version__ = 'ver'
21 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
22 | version_file, re.M)
23 | if version_match:
24 | return version_match.group(1)
25 | raise RuntimeError("Unable to find version string.")
26 |
27 |
28 | def parse_requirements(filename):
29 |
30 | with open(filename) as f:
31 | required = f.read().splitlines()
32 | return required
33 |
34 |
35 | # Get the long description from the relevant file
36 | with codecs.open('README.rst', encoding='utf-8') as f:
37 | long_description = f.read()
38 |
39 | setup(
40 | name="pyTD",
41 | version=find_version('pyTD', '__init__.py'),
42 | description="Python interface to TD Ameritrade Developer API",
43 | long_description=long_description,
44 |
45 | # The project URL.
46 | url='https://github.com/addisonlynch/pyTD',
47 | download_url='https://github.com/addisonlynch/pyTD/releases',
48 |
49 | # Author details
50 | author='Addison Lynch',
51 | author_email='ahlshop@gmail.com',
52 | test_suite='pytest',
53 |
54 | # Choose your license
55 | license='Apache',
56 |
57 | classifiers=[
58 | # How mature is this project? Common values are
59 | # 3 - Alpha
60 | # 4 - Beta
61 | # 5 - Production/Stable
62 | 'Development Status :: 3 - Alpha',
63 |
64 | # Indicate who your project is intended for
65 | 'Intended Audience :: Developers',
66 | 'Intended Audience :: Financial and Insurance Industry',
67 | 'Topic :: Office/Business :: Financial :: Investment',
68 | 'Topic :: Software Development :: Libraries :: Python Modules',
69 | 'Operating System :: OS Independent',
70 |
71 | # Pick your license as you wish (should match "license" above)
72 | 'License :: OSI Approved :: Apache Software License',
73 |
74 | # Specify the Python versions you support here. In particular, ensure
75 | # that you indicate whether you support Python 2, Python 3 or both.
76 | 'Programming Language :: Python',
77 | 'Programming Language :: Python :: 2.7',
78 | 'Programming Language :: Python :: 3.4',
79 | 'Programming Language :: Python :: 3.5',
80 | 'Programming Language :: Python :: 3.6'
81 | ],
82 |
83 | # What does your project relate to?
84 | keywords='stocks market finance tdameritrade quotes shares currency',
85 |
86 | # You can just specify the packages manually here if your project is
87 | # simple. Or you can use find_packages.
88 | packages=find_packages(exclude=["contrib", "docs", "tests*"]),
89 |
90 | # List run-time dependencies here. These will be installed by pip when your
91 | # project is installed.
92 | install_requires=parse_requirements("requirements.txt"),
93 | setup_requires=['pytest-runner'],
94 | tests_require=parse_requirements("requirements-dev.txt"),
95 | # If there are data files included in your packages that need to be
96 | # installed, specify them here. If using Python 2.6 or less, then these
97 | # have to be included in MANIFEST.in as well.
98 | package_data={
99 | 'pyTD': [],
100 | },
101 |
102 |
103 | )
104 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py26,py27,py34,py35,py36
3 | skip_missing_interpreters = true
4 | skipsdist = true
5 |
6 | [testenv]
7 | deps =
8 | -rrequirements.txt
9 | -rrequirements-dev.txt
10 | commands =
11 | python setup.py pytest
12 | flake8 pyTD
13 | flake8-rst --filename="*.rst" .
14 |
--------------------------------------------------------------------------------