├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── img │ │ ├── appinfo.png │ │ ├── authprompt.png │ │ ├── createapp.png │ │ ├── noapps.png │ │ ├── tdallow.png │ │ └── tdlogin.png │ ├── auth.rst │ ├── cache │ └── pyTD.sqlite │ ├── conf.py │ ├── configuration.rst │ ├── exceptions.rst │ ├── faq.rst │ ├── index.rst │ ├── install.rst │ ├── instruments.rst │ ├── market.rst │ ├── quickstart.rst │ ├── testing.rst │ └── tutorials │ └── basics.rst ├── github_deploy_key_addisonlynch_pytd.enc ├── pyTD ├── __init__.py ├── api.py ├── auth │ ├── __init__.py │ ├── _static │ │ ├── auth.html │ │ ├── failed.html │ │ ├── style.css │ │ └── success.html │ ├── manager.py │ ├── server.py │ └── tokens │ │ ├── __init__.py │ │ ├── access_token.py │ │ ├── base.py │ │ ├── empty_token.py │ │ └── refresh_token.py ├── cache │ ├── __init__.py │ ├── base.py │ ├── disk_cache.py │ └── mem_cache.py ├── compat │ └── __init__.py ├── instruments │ ├── __init__.py │ └── base.py ├── market │ ├── __init__.py │ ├── base.py │ ├── hours.py │ ├── movers.py │ ├── options.py │ ├── price_history.py │ └── quotes.py ├── resource.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── fixtures │ │ ├── __init__.py │ │ └── mock_responses.py │ ├── integration │ │ ├── __init__.py │ │ ├── test_api_cache.py │ │ ├── test_api_integrate.py │ │ └── test_integrate.py │ ├── test_helper.py │ └── unit │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_auth.py │ │ ├── test_cache.py │ │ ├── test_exceptions.py │ │ ├── test_instruments.py │ │ ├── test_market.py │ │ ├── test_resource.py │ │ ├── test_tokens.py │ │ └── test_utils.py └── utils │ ├── __init__.py │ ├── exceptions.py │ └── testing.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── get_refresh_token.py └── test.sh ├── setup.cfg ├── setup.py └── tox.ini /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary (include Python version) 2 | 3 | 4 | ### Date/time of issue 5 | 6 | 7 | ### Expected behavior 8 | 9 | 10 | ### Actual behavior 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] closes #xxxx 2 | - [ ] tests added / passed 3 | - [ ] passes `git diff upstream/master -u -- "*.py" | flake8 --diff` 4 | - [ ] added entry to docs/source/whatsnew/vLATEST.txt 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TODO file 2 | TODO 3 | 4 | # tox bin 5 | bin/ 6 | 7 | # macOS attributes file 8 | ._.DS_Store 9 | .DS_Store 10 | 11 | # Sublime SFTP config 12 | sftp-config.json 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "3.5" 9 | - "3.6" 10 | 11 | env: 12 | global: 13 | # Doctr deploy key for addisonlynch/pyTD 14 | - secure: "I6xa7gmoDuKAdForRLD8bax6D1IG9wfWzUUZGUH+zrio7888tx2DMzbEowML3rfL2Kg40F6lkmuhmH/ak4bgOLIL5GbdTIDAQEo9hNTV4WdeDnAX5InQkzzpPuAMXxtfZKvDN3UVCKv5lCFoTtqjpB0OgOPhXMExmEljlPEqtlem1jmtzZYyetW9ZRYZ/AhfzvlVseqs7qUF0r7yJmE+Bh2Rtx8lgYcGXSnwT72PqQvaMbKHpRA5Cb7wp35Yxyq1hRJsMoCGfMWFjNAMyHwKuh/iHJ3g8B/A78aKnMHiZwjuKrjbUOxAdGivkAGtlqhVQ9jLNT1RO4AkLBFz6RkKALsMc01iPq+S2crZ85GIoJp+piF8C+IGEaI5AcpedvST0sAqSYCgTe/uWWO4oL1S645yJblZHR7bSg1FQAxWkYnB4ndnKnFKhQSPvx/6JH2IXcz7+LT3xKRanJqQ/OmZlb1Rm1Iqfzfsa1+hYdXAacxUGZM3P2PZXUUanAnKPjo3ckwd3Kv+gDFmFvlIqyI45jzLesGyAgyBlCsOMQ+th5PXus5cgp4Rn7wfZ1r6krvx99cM0Ihh89qOsJcHN/u1OD/dC2JyMFJHzou1wHhG9vKCMTHJcmWUAru5RDctPw1iIuUkQPIui+3t00wtyM24q1Jsw45J/UCZQtGsV1CmZsg=" 15 | 16 | install: 17 | - pip install -qq flake8 18 | - pip install codecov 19 | - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then 20 | pip install doctr sphinx sphinx_rtd_theme ipython matplotlib; 21 | pip install sphinxcontrib-napoleon; 22 | fi 23 | - python setup.py install 24 | 25 | script: 26 | - pytest --noweb pyTD/tests 27 | - pytest -k webtest 28 | - flake8 --version 29 | - flake8 pyTD 30 | 31 | after_success: 32 | - | 33 | if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then 34 | cd docs 35 | make html && make html 36 | cd .. 37 | doctr deploy devel --build-tags 38 | if [[ -z ${TRAVIS_TAG} ]]; then 39 | echo "Not a tagged build." 40 | else 41 | doctr deploy stable --build-tags 42 | fi 43 | fi 44 | - codecov --token='34052138-4c31-4bf0-96cf-fbc2f1e56e65' 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | include LICENSE.txt 4 | include requirements-dev.txt 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: # clean the repository 2 | find . -name "__pycache__" | xargs rm -rf 3 | find . -name "*pytest_cache" | xargs rm -rf 4 | find . -name "*.pyc" | xargs rm -rf 5 | rm -rf .coverage cover htmlcov logs build dist *.egg-info 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pyTD 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | livehtml: 24 | sphinx-autobuild source build --host 127.0.0.1 -p 8001 -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=pyTD 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/img/appinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/appinfo.png -------------------------------------------------------------------------------- /docs/source/_static/img/authprompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/authprompt.png -------------------------------------------------------------------------------- /docs/source/_static/img/createapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/createapp.png -------------------------------------------------------------------------------- /docs/source/_static/img/noapps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/noapps.png -------------------------------------------------------------------------------- /docs/source/_static/img/tdallow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/tdallow.png -------------------------------------------------------------------------------- /docs/source/_static/img/tdlogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/_static/img/tdlogin.png -------------------------------------------------------------------------------- /docs/source/auth.rst: -------------------------------------------------------------------------------- 1 | .. _auth: 2 | 3 | Authentication 4 | ============== 5 | 6 | TD Ameritrade uses `OAuth 2.0 `__ to authorize and 7 | authenticate requests. 8 | 9 | 10 | .. seealso:: Not familiar with OAuth 2.0? See :ref:`What is OAuth 2.0?` for an overview of OAuth Authentication and Authorization. 11 | 12 | .. _auth.overview: 13 | 14 | Overview 15 | -------- 16 | 17 | 1. Send Consumer Key and Callback URL from your app's details to TD Ameritrade 18 | 2. Open web browser to TD Ameritrade, **login to TD Ameritrade Brokerage Account** 19 | 3. Send authorization code to receive refresh and access tokens 20 | 4. Refresh and access tokens are stored in your ``api`` instance's ``cache`` (either ``DiskCache`` or ``MemCache``) 21 | 22 | .. _auth.script: 23 | 24 | Script Application 25 | ------------------ 26 | 27 | **Script** applications are the simplest type of application to work with 28 | because they don't involve any sort of callback process to obtain an 29 | ``access_token``. 30 | 31 | TD Ameritrade requires that you provide a Callback URL when registering your application -- ``http://localhost:8080`` is a simple one to use. 32 | 33 | .. seealso:: :ref:`What should my Callback URL be?` 34 | 35 | 36 | pyTD provides a simple web server, written in pure Python, to handle 37 | authentication with TD Ameritrade. If used for authentication, this server will run on your localhost (127.0.0.1) and receive your authorization code at your specified Callback URL. 38 | 39 | .. _auth.web: 40 | 41 | Web Application 42 | --------------- 43 | 44 | If you have a **web** application and want to be able to access pyTD 45 | Enter the appropriate Callback URL and configure that endpoint to complete the code flow. 46 | 47 | 48 | .. _auth.installed: 49 | 50 | Installed Application 51 | --------------------- 52 | 53 | 54 | 55 | .. _auth.cache: 56 | 57 | Token Caching 58 | ------------- 59 | 60 | .. warning:: To enable persistent access to authentication tokens across sessions, pyTD stores tokens on-disk by default. **Storing tokens on-disk may pose a security risk to some users.** See :ref:`Is it safe to save my authentications on-disk?` for more information. 61 | 62 | By default, tokens are stored *on-disk* in the :ref:`Configuration 63 | Directory`, though they can also be stored *in-memory*. There are two ways to select a token storage method: 64 | 65 | 1. **Environment Variable** (recommended) - set the ``TD_STORE_TOKENS`` variable: 66 | 67 | .. code-block:: bash 68 | 69 | $ export TD_STORE_TOKENS=False 70 | 71 | 2. Pass ``store_tokens`` keyword argument when creating an ``api`` instance to set token storage temporarily: 72 | 73 | .. code-block:: python 74 | 75 | from pyTD.api import api 76 | 77 | oid = "TEST@AMER.OAUTHAP" 78 | uri = "https://localhost:8080" 79 | 80 | a = api(consumer_key=oid, callback_url=uri, store_tokens=False) 81 | 82 | When ``store_tokens`` is set to ``False``, any stored tokens will be freed from memory when the program exits. 83 | 84 | 85 | Caches 86 | ~~~~~~ 87 | 88 | In-Memory - ``MemCache`` 89 | ^^^^^^^^^^^^^^^^^^^^^^^^ 90 | 91 | The ``MemCache`` class provides in-memory caching for authorization tokens. 92 | 93 | **Important** - the stored tokens will be freed from memory when your program exits. 94 | 95 | .. autoclass:: pyTD.cache.MemCache 96 | 97 | 98 | On-Disk - ``DiskCache`` 99 | ^^^^^^^^^^^^^^^^^^^^^^^ 100 | 101 | To store auth tokens on-disk, the ``DiskCache`` class is provided. When passed an absolute path, ``DiskCache`` creates the necessary directories and instantiates an empty cache file. 102 | 103 | .. autoclass:: pyTD.cache.DiskCache 104 | 105 | 106 | .. todo:: ``SQLCache`` - caching auth tokens in a sqllite database. 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/source/cache/pyTD.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonlynch/pyTD/28099664c8a3b6b7e60f62f5e5c120f01e3530af/docs/source/cache/pyTD.sqlite -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('..')) 4 | sys.path.insert(0, os.path.abspath('../..')) 5 | # -*- coding: utf-8 -*- 6 | # 7 | # Configuration file for the Sphinx documentation builder. 8 | # 9 | # This file does only contain a selection of the most common options. For a 10 | # full list see the documentation: 11 | # http://www.sphinx-doc.org/en/master/config 12 | 13 | # -- Path setup -------------------------------------------------------------- 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- Project information ----------------------------------------------------- 25 | 26 | project = 'pyTD' 27 | copyright = '2018, Addison Lynch' 28 | author = 'Addison Lynch' 29 | 30 | # The short X.Y version 31 | version = '' 32 | # The full version, including alpha/beta/rc tags 33 | release = '' 34 | 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | "sphinx.ext.autodoc", 47 | "sphinx.ext.napoleon", 48 | "sphinx.ext.todo", 49 | "sphinx.ext.autosectionlabel", 50 | 'IPython.sphinxext.ipython_console_highlighting', 51 | 'IPython.sphinxext.ipython_directive', 52 | 'numpydoc' 53 | ] 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This pattern also affects html_static_path and html_extra_path . 77 | exclude_patterns = [] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = 'sphinx' 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = "sphinx_rtd_theme" 89 | html_theme_path = ["_themes", ] 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | # html_theme_options = {} 96 | 97 | # Add any paths that contain custom static files (such as style sheets) here, 98 | # relative to this directory. They are copied after the builtin static files, 99 | # so a file named "default.css" will overwrite the builtin "default.css". 100 | html_static_path = ['_static'] 101 | 102 | # Custom sidebar templates, must be a dictionary that maps document names 103 | # to template names. 104 | # 105 | # The default sidebars (for documents that don't match any pattern) are 106 | # defined by theme itself. Builtin themes are using these templates by 107 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 108 | # 'searchbox.html']``. 109 | # 110 | # html_sidebars = {} 111 | 112 | 113 | # -- Options for HTMLHelp output --------------------------------------------- 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = 'pyTDdoc' 117 | 118 | 119 | # -- Options for LaTeX output ------------------------------------------------ 120 | 121 | latex_elements = { 122 | # The paper size ('letterpaper' or 'a4paper'). 123 | # 124 | # 'papersize': 'letterpaper', 125 | 126 | # The font size ('10pt', '11pt' or '12pt'). 127 | # 128 | # 'pointsize': '10pt', 129 | 130 | # Additional stuff for the LaTeX preamble. 131 | # 132 | # 'preamble': '', 133 | 134 | # Latex figure (float) alignment 135 | # 136 | # 'figure_align': 'htbp', 137 | } 138 | 139 | # Grouping the document tree into LaTeX files. List of tuples 140 | # (source start file, target name, title, 141 | # author, documentclass [howto, manual, or own class]). 142 | latex_documents = [ 143 | (master_doc, 'pyTD.tex', 'pyTD Documentation', 144 | 'Addison Lynch', 'manual'), 145 | ] 146 | 147 | 148 | # -- Options for manual page output ------------------------------------------ 149 | 150 | # One entry per manual page. List of tuples 151 | # (source start file, name, description, authors, manual section). 152 | man_pages = [ 153 | (master_doc, 'pytd', 'pyTD Documentation', 154 | [author], 1) 155 | ] 156 | 157 | 158 | # -- Options for Texinfo output ---------------------------------------------- 159 | 160 | # Grouping the document tree into Texinfo files. List of tuples 161 | # (source start file, target name, title, author, 162 | # dir menu entry, description, category) 163 | texinfo_documents = [ 164 | (master_doc, 'pyTD', 'pyTD Documentation', 165 | author, 'pyTD', 'One line description of project.', 166 | 'Miscellaneous'), 167 | ] 168 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _config: 2 | 3 | 4 | Configuration 5 | ============= 6 | 7 | 8 | - :ref:`config.environment` 9 | - :ref:`config.user_agent` 10 | - :ref:`config.logging` 11 | - :ref:`config.appendix` 12 | 13 | 14 | The following are the available configuration options for pyTD. 15 | 16 | 17 | **Environment** 18 | 19 | :Configuration Directory: 20 | Location in which pyTD's :ref:`log `, :ref:`cached 21 | tokens ` (if using on-disk caching), and :ref:`SSL certificate and 22 | key` are stored 23 | 24 | :SSL Certificate and Key: 25 | If using local web server authentication (script applications), a 26 | self-signed SSL certificate and key are needed. 27 | 28 | **User Agent** - ``api`` 29 | 30 | :Consumer Key \& Callback URL: 31 | TD Ameritrade authorization credentials 32 | 33 | :Token Caching: 34 | Storage of authentication tokens. Can be cached *on-disk* or *in-memory*. 35 | 36 | :Request Parameters: 37 | Specify how requests should be be made. These include ``retry_count``, ``pause``, and ``session``. 38 | 39 | **Logging** 40 | 41 | :Log Level: 42 | Logging level of pyTD. The ``logging`` module handles pyTD's logging. 43 | 44 | .. _config.environment: 45 | 46 | Configuring Environment 47 | ----------------------- 48 | 49 | .. _config.config_dir: 50 | 51 | Configuration Directory 52 | ~~~~~~~~~~~~~~~~~~~~~~~ 53 | 54 | By default, pyTD creates the directory ``.tdm`` in your home directory, which 55 | serves as the default location for on-disk token caching, pyTD's log, and your SSL 56 | certificate and key. 57 | 58 | To specify a custom configuration directory, store such directory's *absolute* 59 | path in the environment variable ``TD_CONFIG_DIR``: 60 | 61 | .. code:: bash 62 | 63 | $ export TD_CONFIG_DIR= 64 | 65 | replacing the bracketed text with your absolute path. 66 | 67 | .. _config.ssl: 68 | 69 | SSL Certificate and Key 70 | ~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | .. seealso:: :ref:`What is a self-signed SSL Certificate?` 73 | 74 | .. _config.ssl_auto: 75 | 76 | Automatic 77 | ^^^^^^^^^ 78 | 79 | To generate the self-signed SSL key and certificate needed for local web server 80 | authentication, use the top-level ``configure`` function: 81 | 82 | .. code:: python 83 | 84 | from pyTD import configure 85 | 86 | configure() 87 | 88 | This function will prompt creation of a self-signed SSL key and certificate, 89 | which will both be placed in your :ref:`Configuration Directory`. 90 | 91 | .. _config.ssl_manual: 92 | 93 | Manual 94 | ^^^^^^ 95 | 96 | The SSL key and certificate can be created manually with the 97 | following OpenSSL command: 98 | 99 | .. code:: bash 100 | 101 | $ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem 102 | 103 | Place the generated key and certificate in the ``ssl`` sub-directory of your 104 | :ref:`config.config_dir`. 105 | 106 | .. _config.environment-variables: 107 | 108 | Setting Environment Variables 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | MacOS/Linux 112 | ^^^^^^^^^^^ 113 | 114 | To set the environment variables for your Consumer Key and Callback URL, use the 115 | following command: 116 | 117 | .. code:: bash 118 | 119 | $ export TD_CONSUMER_KEY='' 120 | $ export TD_CALLBACK_URL='' 121 | 122 | replacing the bracketed text with your information. 123 | 124 | .. _config.all_in_one: 125 | 126 | 127 | The All-in-One Solution: ``pyTD.configure`` 128 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 129 | 130 | pyTD provides the top-level function ``pyTD.configure`` which handles all 131 | configuration necessary to make an authenticated query. 132 | 133 | .. autofunction:: pyTD.configure 134 | 135 | 136 | 137 | .. _config.user_agent: 138 | 139 | Configuring User Agent 140 | ---------------------- 141 | 142 | .. _config.default-api: 143 | 144 | There are three ways to configure your user agent: 145 | 146 | 1. **Let pyTD do it for you**. So long as you have your :ref:`Configuration 147 | Directory` set up, simply run any pyTD function or method and pyTD will create 148 | an ``api`` object for you, defaulting to an on-disk token cache. 149 | 150 | .. code:: python 151 | 152 | from pyTD.market import get_quotes 153 | 154 | get_quotes("AAPL") 155 | 156 | 2. **Create a non-global API object** 157 | 158 | Either pass the required parameters individually: 159 | 160 | .. code:: python 161 | 162 | from pyTD.api import api 163 | from pyTD.market import get_quotes 164 | 165 | consumer_key = "TEST@AMER.OAUTHAP" 166 | callback_url = "https://localhost:8080" 167 | 168 | my_api = api(consumer_key=consumer_key, callback_url=callback_url) 169 | 170 | get_quotes("AAPL", api=my_api) 171 | 172 | Or pass a pre-instantiated dictonary: 173 | 174 | .. code:: python 175 | 176 | from pyTD.api import api 177 | from pyTD.market import get_quotes 178 | 179 | params = { 180 | "consumer_key": "TEST@AMER.OAUTHAP", 181 | "callback_url": "https://localhost:8080" 182 | } 183 | 184 | my_api = api(params) 185 | 186 | get_quotes("AAPL", api=my_api) 187 | 188 | 3. **Use** ``pyTD.configure``: 189 | 190 | .. code:: python 191 | 192 | from pyTD import configure 193 | from pyTD.market import get_quotes 194 | 195 | consumer_key = "TEST@AMER.OAUTHAP" 196 | callback_url = "https://localhost:8080" 197 | 198 | configure(consumer_key=consumer_key, callback_url=callback_url) 199 | 200 | get_quotes("AAPL") 201 | 202 | The ``api`` object 203 | ~~~~~~~~~~~~~~~~~~ 204 | 205 | The ``api`` object serves as the user agent for all requests to the TD 206 | Ameritrade Developer API. The ``api`` object: 207 | 208 | 1. Manages configuration (directory, SSL, Consumer Key, Callback URL) 209 | 2. Connects to the token cache 210 | 3. Verifies, validates, and handles authentication and authorization. 211 | 212 | 213 | .. autoclass:: pyTD.api.api 214 | 215 | 216 | 217 | .. _config.logging: 218 | 219 | Configuring Logging 220 | ------------------- 221 | 222 | pyTD uses Python's `logging 223 | `__ module to log its activity 224 | for both informational and debugging purposes. 225 | 226 | By default, a log is kept in pyTD's :ref:`Configuration Directory` and named 227 | ``pyTD.log``. 228 | 229 | Setting the logging level 230 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 231 | 232 | The console logging level of pyTD can be set in one of three ways: 233 | 234 | 1. **Using** ``pyTD.log_level``: 235 | 236 | .. code:: python 237 | 238 | import pyTD 239 | 240 | pyTD.log_level = "DEBUG" 241 | 242 | 2. **Using** ``logging.setLevel``: 243 | 244 | .. code:: python 245 | 246 | import logging 247 | 248 | logging.getLogger("pyTD").setLevel(logging.DEBUG) 249 | 250 | 3. **Using environment variables** 251 | 252 | The environment variable ``TD_LOG_LEVEL`` will override any log level settings for the console logger. 253 | 254 | .. code:: bash 255 | 256 | export TD_LOG_LEVEL='DEBUG' 257 | 258 | .. _config.appendix: 259 | 260 | Appendix 261 | -------- 262 | 263 | Environment Variables 264 | ~~~~~~~~~~~~~~~~~~~~~ 265 | 266 | For reference, the following environment variables may be set to configure pyTD: 267 | 268 | - ``TD_CONSUMER_KEY`` (required) - TD Ameritrade Developer application OAUTH ID 269 | - ``TD_CALLBACK_URL`` (required) - TD Ameritrade Developer application Callback URL 270 | - ``TD_CONFIG_DIR`` - for specifying a custom pyTD configuration directory (defaults to ~/.tdm) 271 | - ``TD_STORE_TOKENS`` - set to false to disable on-disk authentication token 272 | caching 273 | - ``TD_LOG_LEVEL`` - for specifying a console logging level for pyTD 274 | -------------------------------------------------------------------------------- /docs/source/exceptions.rst: -------------------------------------------------------------------------------- 1 | .. _exceptions: 2 | 3 | Exceptions 4 | ========== 5 | 6 | 7 | 8 | .. autoexception:: pyTD.utils.exceptions.AuthorizationError 9 | 10 | .. autoexception:: pyTD.utils.exceptions.SSLError 11 | 12 | .. autoexception:: pyTD.utils.exceptions.ResourceNotFound 13 | 14 | .. autoexception:: pyTD.utils.exceptions.ConfigurationError 15 | 16 | .. autoexception:: pyTD.utils.exceptions.CacheError 17 | 18 | .. autoexception:: pyTD.utils.exceptions.TDQueryError 19 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | Frequently Asked Questions 4 | ========================== 5 | 6 | .. _faq.oauth_20: 7 | 8 | What is OAuth 2.0? 9 | ------------------ 10 | 11 | From `RFC 6749 `__: 12 | 13 | 14 | 15 | The OAuth 2.0 authorization framework enables a third-party 16 | application to obtain limited access to an HTTP service, either on 17 | behalf of a resource owner by orchestrating an approval interaction 18 | between the resource owner and the HTTP service, or by allowing the 19 | third-party application to obtain access on its own behalf. 20 | 21 | In other words, OAuth 2.0 is the protocol that TD Ameritrade uses to help you 22 | gain access to the Developer API (and your account). There are four roles in 23 | this process: 24 | 25 | 1. **Resource Owner** - an entity capable of granting access to a protected 26 | resource. **In this case, YOU**. 27 | 28 | 2. **Resource Server** - the server hosting the protected resources, capable of 29 | accepting and responding to protected resource requests using access tokens. 30 | **In this case, TD Ameritrade's servers**. 31 | 32 | 3. **Client** - An application making protected resource requests on behalf of 33 | the resource owner and with its authorization. **In this case, pyTD (or an 34 | application which uses it)**. 35 | 36 | 4. **Authorization Server** - The server issung access tokens to the client 37 | after successfully authenticating the resource owner and obtaining 38 | authorization. **In this case, your application or local authorization 39 | server**. 40 | 41 | 42 | 43 | 44 | .. _faq.callback-url: 45 | 46 | What should my Callback URL be? 47 | ------------------------------- 48 | 49 | This raises an important question: **What is a Callback URL?** 50 | 51 | From `RFC 6749 `__: 52 | 53 | The authorization code is obtained by using an **authorization server** 54 | as an intermediary between the client and resource owner. Instead of 55 | requesting authorization directly from the resource owner, **the client 56 | directs the resource owner to an authorization server** (via its 57 | user-agent as defined in [RFC2616]), which in turn directs the 58 | resource owner back to the client with the authorization code.... 59 | 60 | ... **Because the resource owner only authenticates with the authorization 61 | server, the resource owner's credentials are never shared with the client**. 62 | 63 | As explained in :ref:`What is OAuth 2.0?`, the resource owner is you - as you 64 | have the access to your TD Ameritrade brokerage account. In order to prevent 65 | your account credentials from being revealed, authentication is completed with 66 | an **authorization server**. 67 | 68 | Default ``pyTD`` behavior is to start this server locally, running on your 69 | localhost (typically ``127.0.0.1``). If this is the case, your Callback URL 70 | should be https://localhost:8080. 71 | 72 | 73 | .. _faq.ssl-basics: 74 | 75 | What is a self-signed SSL certificate? 76 | -------------------------------------- 77 | 78 | An SSL certificate certifies the identity of an entity such as your local pyTD authentication server. **Self-signed SSL certificates** are signed by the same entity which they are certifying the identity of. 79 | 80 | This may cause problems for some browsers, which will display messages such as "Your connection is not private" and "This site's security certificate is not trusted!". This is due to the fact that your application is not a trusted certificate authority. 81 | 82 | .. _faq.create-ssl-cert-key: 83 | 84 | How do I create a self-signed SSL certificate and key? 85 | ------------------------------------------------------ 86 | 87 | There are a number of different options for generating a self-signed SSL 88 | certificate and key. 89 | 90 | The easiest way: OpenSSL 91 | ~~~~~~~~~~~~~~~~~~~~~~~~ 92 | 93 | `OpenSSL `__ is an SSL/TLS toolkit which is useful 94 | for generating SSL certificates. Once installed (see system-specific 95 | installation instructions below), run the following command to generate key 96 | and certificate files ``key.pem`` and ``cert.pem``: 97 | 98 | .. code-block:: bash 99 | 100 | openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem 101 | 102 | 103 | Installing OpenSSL 104 | ^^^^^^^^^^^^^^^^^^ 105 | 106 | **macOS** 107 | 108 | Install using `Homebrew `__: 109 | 110 | .. code-block:: bash 111 | 112 | brew update 113 | brew install openssl 114 | 115 | **Linux** 116 | 117 | OpenSSL is packaged with most Linux distributions 118 | 119 | 120 | **Windows** 121 | 122 | OpenSSL for Windows can be downloaded `here `__. 123 | 124 | 125 | .. _faq.dev_account: 126 | 127 | How do I get my Consumer Key and Callback URL? 128 | ---------------------------------------------- 129 | 130 | A TD Ameritrade Developer account and application are required in order to access the Developer API. **This is a separate account from TD Ameritrade Brokerage Accounts**. A TD Ameritrade Brokerage Account is **not required** to obtain a TD Ameritrade Developer Account. 131 | 132 | To register for a TD Ameritrade Developer account, visit https://developer.tdameritrade.com/ and click "Register" in the top-right corner of the screen. 133 | 134 | Creating an App 135 | ~~~~~~~~~~~~~~~ 136 | 137 | To create a new TD Ameritrade Developer Application, navigate to the "My Apps" page of your TD Ameritrade Developer Account: 138 | 139 | .. figure:: _static/img/noapps.png 140 | 141 | My Apps Page 142 | 143 | From here, click the "Add a new App" button: 144 | 145 | .. figure:: _static/img/createapp.png 146 | 147 | Creating an app 148 | 149 | You will be prompted to enter the following fields: 150 | 151 | 1) **App Name** - desired application name 152 | 2) **Callback URL** - also known as Callback URL, this is the callback address to complete authorization 153 | 3) **OAuth User ID** - a unique ID that will be used to create your full OAauth ID 154 | 4) **App Description** - a description of your application 155 | 156 | After completing the form, your application will be created. By clicking on the application in the "My Apps" page, you can display information about it: 157 | 158 | .. figure:: _static/img/appinfo.png 159 | 160 | App Info 161 | 162 | The **Consumer Key** field is your **Consumer Key**. Your **Callback URL** is the **Callback URL** which you entered at the app's creation. 163 | 164 | .. note:: To change the Callback URL (Callback URL) of your application, you must delete the application and create a new one. This is a caveat of the TD Ameritrade registration process. 165 | 166 | 167 | .. _faq.script: 168 | 169 | What is a Script Application? 170 | ----------------------------- 171 | 172 | A script application is simply an application that is run as a script from your 173 | local environment. This may be a stand-alone script that is run which uses pyTD 174 | or command-line invocation of pyTD, such as running: 175 | 176 | .. code-block:: python 177 | 178 | >>> from pyTD.market import get_quotes 179 | >>> get_quotes("AAPL") 180 | 181 | in an interactive Python shell. 182 | 183 | 184 | .. _faq.cusip: 185 | 186 | What is a CUSIP ID? 187 | ------------------- 188 | 189 | A CUSIP is a nine-character alphanumeric code that identifies a North American 190 | financial security. 191 | 192 | Simply put, CUSIPs are unique identifiers for a number of financial instruments 193 | including common stocks, bonds, and other equities. 194 | 195 | .. _faq.cusip-examples: 196 | 197 | Examples 198 | ~~~~~~~~ 199 | 200 | - Apple Inc.: 03783100 201 | - Cisco Systems: 17275R102 202 | - Google Inc.: 38259P508 203 | - Microsoft Corporation: 594918104 204 | - Oracle Corporation: 58389X105 205 | 206 | .. _faq.token_storage: 207 | 208 | Is it safe to save my authentications on-disk? 209 | ---------------------------------------------- 210 | 211 | .. note:: TODO 212 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | pyTD: Python SDK for TD Ameritrade 2 | ================================== 3 | 4 | This documentation is organized as follows: 5 | 6 | - :ref:`getting_started` 7 | - :ref:`endpoints` 8 | - :ref:`tutorials` 9 | - :ref:`package_info` 10 | 11 | .. _getting_started: 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Getting Started 15 | 16 | quickstart.rst 17 | install.rst 18 | configuration.rst 19 | auth.rst 20 | 21 | .. _endpoints: 22 | .. toctree:: 23 | :maxdepth: 2 24 | :caption: Endpoints 25 | 26 | market.rst 27 | instruments.rst 28 | 29 | .. _tutorials: 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: Tutorials 33 | :glob: 34 | 35 | tutorials/* 36 | 37 | .. _package_info: 38 | .. toctree:: 39 | :maxdepth: 2 40 | :caption: Package Info 41 | 42 | exceptions.rst 43 | testing.rst 44 | faq.rst 45 | 46 | 47 | Indices and tables 48 | ================== 49 | 50 | * :ref:`genindex` 51 | * :ref:`modindex` 52 | * :ref:`search` 53 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | 4 | Installing pyTD 5 | =============== 6 | 7 | pyTD supports Python 2.7, 3.4, 3.5, 3.6, and 3.7. The recommended 8 | installation method is via ``pip``: 9 | 10 | .. code-block:: bash 11 | 12 | $ pip install pyTD 13 | 14 | 15 | Below are the options for installing pyTD. 16 | 17 | .. warning:: After pyTD is installed, it must be configured. See the pyTD :ref:`Quick Start Guide` for more information. 18 | 19 | .. _install.dependencies: 20 | 21 | Dependencies 22 | ------------ 23 | 24 | - requests 25 | - pandas 26 | 27 | For testing requirements, see `testing `__. 28 | 29 | Installation 30 | ------------ 31 | 32 | The recommended installation method is ``pip``. For more information about 33 | installing Python and pip, see "The Hitchhiker's Guide to Python" `Installation 34 | Guides `__. 35 | 36 | Stable Release 37 | ~~~~~~~~~~~~~~ 38 | 39 | .. code:: bash 40 | 41 | $ pip install pyTD 42 | 43 | 44 | Development Version 45 | ~~~~~~~~~~~~~~~~~~~ 46 | 47 | 48 | .. code:: bash 49 | 50 | $ pip install git+https://github.com/addisonlynch/pyTD.git 51 | 52 | or 53 | 54 | .. code:: bash 55 | 56 | $ git clone https://github.com/addisonlynch/pyTD.git 57 | $ cd pyTD 58 | $ pip install . 59 | 60 | Older Versions 61 | ~~~~~~~~~~~~~~ 62 | 63 | .. code:: bash 64 | 65 | $ pip install pyTD=0.0.1 66 | 67 | virtualenv 68 | ---------- 69 | 70 | The use of 71 | `virtualenv `__ 72 | is **highly** recommended as below: 73 | 74 | .. code:: bash 75 | 76 | $ pip install virtualenv 77 | $ virtualenv env 78 | $ source env/bin/activate 79 | -------------------------------------------------------------------------------- /docs/source/instruments.rst: -------------------------------------------------------------------------------- 1 | .. _instruments: 2 | 3 | Instruments 4 | =========== 5 | 6 | The `Instruments `__ 7 | endpoints allow for retrieval of instrument and fundamental 8 | data. These endpoints are `Search Instruments 9 | `__ and 10 | `Get Instrument 11 | `__. 12 | 13 | .. _instruments.get-instrument: 14 | 15 | Get Instrument 16 | -------------- 17 | 18 | pyTD provides access to Get Instruments through the ``get_instrument`` 19 | function. Simply enter a symbol: 20 | 21 | .. ipython:: python 22 | 23 | from pyTD.instruments import get_instrument 24 | 25 | get_instrument("AAPL") 26 | 27 | or a CUSIP ID: 28 | 29 | .. ipython:: python 30 | 31 | get_instrument("68389X105") 32 | 33 | .. _instruments.search-instruments: 34 | 35 | Search Instruments 36 | ------------------ 37 | 38 | Search Instruments is implemented through ``get_instruments``. 39 | 40 | ``projection`` 41 | ~~~~~~~~~~~~~~ 42 | 43 | There are five types of searches which can be performed: 44 | 45 | 1. ``symbol-search`` (default): Retrieve instrument data of a specific symbol 46 | or CUSIP 47 | (similar to ``get_instrument``) 48 | 49 | .. ipython:: python 50 | 51 | from pyTD.instruments import get_instruments 52 | 53 | get_instruments("AAPL") 54 | 55 | 56 | 2. ``symbol-regex``: Retrieve instrument data for all symbols matching regex. 57 | Example: ``symbol=XYZ.*`` will return all symbols beginning with XYZ 58 | 59 | .. ipython:: python 60 | 61 | get_instruments("AAP.*", projection="symbol-regex") 62 | 63 | 64 | 3. ``desc-search``: Retrieve instrument data for instruments whose description 65 | contains the word supplied. Example: ``symbol=FakeCompany`` will return all 66 | instruments with FakeCompany in the description. 67 | 68 | .. ipython:: python 69 | 70 | get_instruments("computer", projection="desc-search") 71 | 72 | 73 | 4. ``desc-regex``: Search description with full regex support. Example: 74 | ``symbol=XYZ.[A-C]`` returns all instruments whose descriptions contain a word 75 | beginning with XYZ followed by a character A through C. 76 | 77 | .. ipython:: python 78 | 79 | get_instruments("COM.*", projection="desc-regex") 80 | 81 | 82 | 5. ``fundamental``: Returns fundamental data for a single instrument specified by exact symbol. 83 | 84 | .. ipython:: python 85 | 86 | get_instruments("AAPL", projection="fundamental").head() 87 | -------------------------------------------------------------------------------- /docs/source/market.rst: -------------------------------------------------------------------------------- 1 | .. _market: 2 | 3 | 4 | Market Data 5 | =========== 6 | 7 | TD Ameritrade provides various endpoints to obtain Market Data for various instruments and markets across asset classes. 8 | 9 | **Endpoints** 10 | 11 | 1. :ref:`Quotes ` 12 | 2. :ref:`Market Movers ` 13 | 3. :ref:`Market Hours ` 14 | 4. :ref:`Option Chains ` 15 | 5. :ref:`Price History ` 16 | 6. :ref:`Fundamentals ` 17 | 18 | 19 | .. _market.quotes: 20 | 21 | Quotes 22 | ------ 23 | 24 | The `Get Quote 25 | `__ 26 | and `Get Quotes 27 | `__ 28 | endpoints provide real-time and delayed quotes. Access is provided by pyTD 29 | through the top-level function ``get_quotes``, which combines functionality of 30 | the two endpoints. 31 | 32 | 33 | .. autofunction:: pyTD.market.get_quotes 34 | 35 | .. _market.quotes-examples: 36 | 37 | Examples 38 | ~~~~~~~~ 39 | 40 | **Single Stock** 41 | 42 | .. ipython:: python 43 | 44 | from pyTD.market import get_quotes 45 | 46 | get_quotes("AAPL").head() 47 | 48 | 49 | **Multiple Stocks** 50 | 51 | .. ipython:: python 52 | 53 | get_quotes(["AAPL", "TSLA"]).head() 54 | 55 | 56 | 57 | .. _market.movers: 58 | 59 | Movers 60 | ------ 61 | 62 | The `Get Movers `__ endpoint provides market movers (up or down) for a specified index. Access is provided by pyTD through the top-level function ``get_movers``. 63 | 64 | **Format** - 'json' (dictionary) or 'pandas' (Pandas DataFrame) 65 | 66 | .. autofunction:: pyTD.market.get_movers 67 | 68 | .. note:: The desired index should be prefixed with ``$``. For instance, the Dow Jones Industrial Average is ``$DJI``. 69 | 70 | .. warning:: This endpoint may return empty outside of Market Hours. 71 | 72 | .. _market.movers-examples: 73 | 74 | Examples 75 | ~~~~~~~~ 76 | 77 | .. ipython:: python 78 | 79 | from pyTD.market import get_movers 80 | 81 | get_movers("$DJI") 82 | 83 | 84 | .. _market.hours: 85 | 86 | Hours 87 | ----- 88 | 89 | The `Get Market Hours 90 | `__ 91 | endpoint provides market hours for various markets, including equities, 92 | options, and foreign exchange (forex). Access is provided by pyTD through the top-level function ``get_market_hours``. 93 | 94 | By default, ``get_market_hours`` returns the market hours of the current date, 95 | but can do so for any past or future date when passed the optional keyword argument ``date``. 96 | 97 | .. autofunction :: pyTD.market.get_market_hours 98 | 99 | .. _market.hours-examples: 100 | 101 | Examples 102 | ~~~~~~~~ 103 | 104 | .. ipython:: python 105 | 106 | from pyTD.market import get_market_hours 107 | 108 | get_market_hours("EQUITY") 109 | 110 | 111 | .. _market.option-chains: 112 | 113 | Option Chains 114 | ------------- 115 | 116 | The `Get Option Chains `__ endpoint provides option chains for optionable equities symbols. Access is provided by pyTD through the top-level function ``get_option_chains``. 117 | 118 | ``get_option_chains`` accepts a variety of arguments, which allow filtering of the results by criteria such as strike price, moneyness, and expiration date, among others. Futher, it is possible to specify certain parameters to be used in calculations for analytical strategy chains. 119 | 120 | 121 | .. autofunction :: pyTD.market.get_option_chains 122 | 123 | .. _market.option-chains-examples: 124 | 125 | Examples 126 | ~~~~~~~~ 127 | 128 | Simple 129 | ^^^^^^ 130 | 131 | .. ipython:: python 132 | 133 | from pyTD.market import get_option_chains 134 | 135 | get_option_chains("AAPL") 136 | 137 | .. _market.price-history: 138 | 139 | Historical Prices 140 | ----------------- 141 | 142 | The `Get Price History `__ endpoint provides historical pricing data for symbols across asset classes. Access is provided by pyTD through the top-level function ``get_price_history``. 143 | 144 | 145 | .. autofunction :: pyTD.market.get_price_history 146 | 147 | .. _market.price-history-examples: 148 | 149 | Examples 150 | ~~~~~~~~ 151 | 152 | .. ipython:: python 153 | 154 | import datetime 155 | from pyTD.market import get_price_history 156 | 157 | start = datetime.datetime(2017, 1, 1) 158 | end = datetime.datetime(2018, 1, 1) 159 | 160 | get_price_history("AAPL", start_date=start, end_date=end).head() 161 | 162 | 163 | .. _market.fundamentals: 164 | 165 | Fundamental Data 166 | ---------------- 167 | 168 | Fundamental data can also be accesed through ``get_fundamentals``, which wraps 169 | ``pyTD.instruments.get_instruments`` for convenience. 170 | 171 | .. ipython:: python 172 | 173 | from pyTD.market import get_fundamentals 174 | 175 | get_fundamentals("AAPL").head() 176 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quick Start Guide 4 | ================= 5 | 6 | .. note:: This Quick Start guide assumes the use of a :ref:`Script 7 | Application `. See :ref:`Authentication` for 8 | more information about using 9 | **installed** applications and **web** applications. 10 | 11 | In this section, we go over everything you need to know to start building 12 | scripts, or bots using pyTD, the Python TD Ameritrade Developer API SDK. 13 | It's fun and easy. Let's get started. 14 | 15 | Prerequisites 16 | ------------- 17 | 18 | :Python Knowledge: You need to know at least a little Python to use pyTD; it's 19 | a Python wrapper after all. pyTD supports `Python 2.7`_, 20 | and `Python 3.4 to 3.7`_. 21 | 22 | :TD Ameritrade Knowledge: A basic understanding of how TD Ameritrade's 23 | Developer APIs work is a must. It is recommended that you read 24 | through the `TD Ameritrade documentation`_ before starting 25 | with pyTD. 26 | 27 | :TD Ameritrade Developer Account: This is a **separate account** from your TD 28 | brokerage accounts(s). 29 | 30 | .. _`Python 2.7`: https://docs.python.org/2/tutorial/index.html 31 | .. _`Python 3.4 to 3.7`: https://docs.python.org/3/tutorial/index.html 32 | .. _`TD Ameritrade documentation`: https://developer.tdameritrade.com/apis 33 | 34 | .. _quickstart.common_tasks: 35 | 36 | 37 | Step 1 - Obtain an Consumer Key and Callback URL 38 | ------------------------------------------------ 39 | 40 | .. seealso:: For a more detailed tutorial on setting up a TD Ameritrade 41 | Developer Account, creating an application, or obtaining an Consumer Key and 42 | Callback URL, see :ref:`How do I get my Consumer Key and Callback URL?`. 43 | 44 | 1. From your TD Ameritrade Developer Account, **create a new application** 45 | using the "Add App" button in your profile. Enter the following information 46 | when prompted: 47 | 48 | * **App Name** - desired application name (can be anything) 49 | * **Callback URL** - the address that your authentication information will be forwarded to complete authentication of your script application (https://localhost:8080 50 | is easiest). See :ref:`What should my Callback URL be?` for more information 51 | on choosing a Callback URL. 52 | * **OAuth User ID** - unique ID that will be used to create your consumer key 53 | (can be anything) 54 | * **App Description** - description of your application (can be anything) 55 | 56 | 2. Once your application has been created, store its **Consumer Key** and **Callback URL** 57 | in the environment variables ``TD_CONSUMER_KEY`` and ``TD_CALLBACK_URL``: 58 | 59 | .. code-block:: bash 60 | 61 | $ export TD_CONSUMER_KEY=TEST@AMER.OAUTHAP # Your Consumer Key 62 | $ export TD_CALLBACK_URL=https://localhost:8080 # Your Callback URL 63 | 64 | .. note:: If you are unfamiliar with environment variables or unable to 65 | set them on your system, see :ref:`Configuring Environment` for more 66 | configuration options. 67 | 68 | 69 | 70 | Step 2 - Run ``pyTD.configure`` 71 | ------------------------------- 72 | 73 | 74 | The easiest (and recommended) way configure ``pyTD`` is using 75 | ``pyTD.configure``. 76 | 77 | 78 | .. code-block:: python 79 | 80 | import pyTD 81 | pyTD.configure() 82 | 83 | This function does the following: 84 | 85 | 1. Creates a **configuration directory** (defaults to ``.tdm`` in your home 86 | directory). The location can be chosen manually by setting the environment 87 | variable ``TD_CONFIG_DIR``. This directory is the location in which pyTD's 88 | :ref:`log`, :ref:`cached tokens ` (if using on-disk 89 | caching), and :ref:`SSL certificate and key` are stored. 90 | 91 | 2. Generates a **self-signed SSL certificate \& key** and places them in the 92 | ``ssl`` directory within your configuration directory. 93 | 94 | .. warning:: If using MacOS, you may not be able to generate the certificate 95 | and key using ``pyTD.configure``. See :ref:`Generating an SSL Certificate and Key ` for 96 | more information and instructions on how to generate the 97 | certificate manually. 98 | 99 | .. note:: 100 | When called with no arguments, ``pyTD.configure`` requires :ref:`setting environment 101 | variables` ``TD_CONSUMER_KEY`` and ``TD_CALLBACK_URL`` to your app's Consumer Key and 102 | Callback URL. These can also be passed to ``pyTD.configure`` instead: 103 | 104 | .. code:: python 105 | 106 | import pyTD 107 | 108 | consumer_key='TEST@AMER.OAUTHAP' 109 | callback_url='https://localhost:8080' 110 | 111 | pyTD.configure(consumer_key=consumer_key, callback_url=callback_url) 112 | 113 | ``pyTD.configure`` will set the environment variables automatically for the 114 | **current session only**. 115 | 116 | .. _`documentation`: https://addisonlynch.github.io/pytd/stable/faq.html#what-is-a-td-ameritrade-developer-account 117 | .. _`Generating an SSL Key/Certificate`: https://addisonlynch.github.io/pytd/stable/configuration.html#generating-an-ssl-key-certificate 118 | .. _`docs`: https://addisonlynch.github.io/pytd/stable/configuration.html#the-all-in-one-solution-pytd-configure 119 | .. _`configuration directory`: https://addisonlynch.github.io/pytd/stable/configuration.html#configuration-directory 120 | 121 | .. _quickstart.authenticate-app: 122 | 123 | Step 3 - Authenticate Your Application 124 | -------------------------------------- 125 | 126 | 127 | The simplest way to authorize and authenticate pyTD is by calling any function which 128 | returns data. For example ``get_quotes`` from ``pyTD.market`` 129 | will automatically prompt you to obtain a new refresh token if you have not 130 | obtained one or your refresh token has expired: 131 | 132 | 133 | .. code-block:: python 134 | 135 | from pyTD.market import get_quotes 136 | 137 | get_quotes("AAPL") 138 | # WARNING:root:Need new refresh token. 139 | # Would you like to authorize a new refresh token? [y/n]: 140 | 141 | Selecting ``y`` will open a browser for you to authorize your application: 142 | 143 | .. figure:: _static/img/authprompt.png 144 | 145 | Select "AUTHORIZE" to redirect to a TD Ameritrade login prompt: 146 | 147 | .. figure:: _static/img/tdlogin.png 148 | 149 | From here, log in to your TD Ameritrade Brokerage Account. Once logged in, the following page will be displayed: 150 | 151 | .. figure:: _static/img/tdallow.png 152 | 153 | Select "Allow" to authorize your application. pyTD will handle receiving the tokens and authorization code behind the scenes, and if retrieval is successful, the results of your original query will display on screen. 154 | 155 | 156 | Step 4 - Go! 157 | ------------ 158 | 159 | You're now all set up to query TD Ameritrade's Developer APIs! 160 | 161 | .. seealso:: For more usage tutorials and examples, see :ref:`Tutorials 162 | ` 163 | -------------------------------------------------------------------------------- /docs/source/testing.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | 4 | Testing 5 | ======= 6 | 7 | 8 | .. _testing.environment: 9 | 10 | Setting Up a Testing Environment 11 | -------------------------------- 12 | 13 | 1. Install the testing :ref:`dependencies` 14 | 15 | .. code:: bash 16 | 17 | $ pip3 install -r requirements-dev.txt 18 | 19 | 2. Run the tests 20 | 21 | In the pyTD root directory, there is a shell script ``test.sh``. This script 22 | first verifies flake8 compliance, then runs all tests with pytest. 23 | 24 | .. _testing.writing-tests: 25 | 26 | Writing Tests 27 | ------------- 28 | 29 | Marking 30 | ~~~~~~~ 31 | 32 | All tests which require HTTP requests which are not mocked should be 33 | marked with ``pytest.mark.webtest``. These tests will be automatically skipped 34 | if pyTD has not been properly configured or does not have a valid access token 35 | (this is verified through ``pyTD.api.default_auth_ok``). 36 | 37 | 38 | Fixtures 39 | ~~~~~~~~ 40 | 41 | A number of fixtures are used to provide instantiated objects (``api``, 42 | tokens, etc.) as well as parametrize tests. These fixtures can be found in the 43 | ``fixtures`` directory. 44 | 45 | .. seealso:: `pytest Fixtures Documentation 46 | `__ 47 | 48 | 49 | .. _testing.dependencies: 50 | 51 | Testing Dependencies 52 | -------------------- 53 | 54 | Tests 55 | ~~~~~ 56 | 57 | - `pytest `__ 58 | - `tox `__ 59 | - `flake8 `__ 60 | 61 | 62 | Documentation 63 | ~~~~~~~~~~~~~ 64 | 65 | - `sphinx `__ 66 | - `sphinx-autobuild `__ 67 | - `sphinx-rtd-theme `__ 68 | - `ipython `__ 69 | - `matplotlib `__ 70 | -------------------------------------------------------------------------------- /docs/source/tutorials/basics.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_basics: 2 | 3 | Basics 4 | ====== 5 | 6 | Basic tutorials. 7 | -------------------------------------------------------------------------------- /github_deploy_key_addisonlynch_pytd.enc: -------------------------------------------------------------------------------- 1 | gAAAAABbzTue7_Slkc3SDo7xdI-e5qH654164ieKRxLiJ1b9I1awfX1EsP6Lxtgl2dOUNWmnjPLp28vx_jyst9Rh_oT9uc6CrQ1EU7-MdQiH13_0C4oHTgrQYFZC7E0i25O2LsEFmhiXcqKb_LK2EBRmD_jZ4O3v-3UVdPe6nHJCidsHR5F0vTUYcQOsLtFB6McA8Jm0ax_LXkFpSHzVRtHJlaubtX7o55tfkvqooE8bRJeaGgNhVYV4DTK4dL8ftd8NgseATeUqd1fcANW89OHW2yzN8XpGVKdam3o6yIgrzL9wIZBC0nSYsfoYdNbsVWz7lJroxe2KtiY5VDyJlvnhu3MWAhQQtlpw-kFouQyrmpjzUC_dTpN8izFVabmTAJeBwLsbRQcMC300p7q05Ir6VYk4aMahtRKXeMixLT6hB3c_9IMqzD5cQKTMAPrh6TC0rOfWQpo87ldKg6FCDm8AcNIBbXLwau_XqhSZ56YJ2maBCdIiyW5bE-W9GaLkW5OEHu__QvPBoXDPXQqFKcuPaPxzJ2TI57DBZmszGoceCBueOZfoPmFn-h7GhafQBu7WDRfMwD7cIN_gEsJRqQD6b3F-yPosZsq1hMnh97yWi5Wvye_wjj1of00MkNavV9ZQSUYPh2ttp2aylT2kmP8eYLq2vqqUVJ9b-bl7shAia-YmW1vcDV1iOGWrctfuGEHiWzVZ3LMlDR0EW1hIxdyIhKeIiUMggPYqCTtlBwZLf5nzFF-4FbV2l1SDhrY8oJIdaPE3asRCevxS8tc4WjkRC0lfKuHrGAIvOXSC-4-6xFDfCcImLGEGawDz0-wjnKZIs7NwxVLpU1-_tXVQAZgiUwizXx7LR1N7bVX_yk7JNNjBVpVMADIyYsSGkapjLGY8IgQnERc2l02GU2yRJoRRSAmEwbuII7NlRRDfPUYCOQWvKzvRK1ar6VZtUe7UdLCTl5teXpz0ucZ8Ktu2APuLugOm9AEKCW_wNR9ryGQ1FojB4Wh2f_-11u0yVt8Bz-UO_OZApkcB165OiERetemo-STsGjmWjxbzbBLQ4EryLiyzEYRa3cDUWcZTBhhMrCwT-m7XORJcOfU-4gXJ5p_aUQaXNxhZCg4YJDmjaAZi0SXd-ciKox0CRQxFyNlEzqrYpUE9QMAOA0kSB9HsE_oU9Qpdrk0aEr7VBfWMUpACovtQgn_n2DpQF8xdD6MxqS_3VUEAnkQPG8Xi4xwXN9ENaFaJ9p1PFEoLdWPVv_Jvyc3K60xKUXJRXUlnVc4U3zE64pXwBnSF3BQrgVJPQObZzCVJcKdRHa8xRgovqll8H6QsoiHkpvd6YHh3WCvMXbq2a5iVvSnPijICna13XJfRuc4OX8doBk3W8jVlrLWj3k6ajkdq9B_5qjwCl_J-bPZIZLoK8SmosB9Icim73jmGDTzbfLQJ_ylRh-_SjOidVCZIEACStdhkTkHpGZwa2JwIb0j6yVF0vhXuvlgXoLnbsUNoRIuqQ-xYY3QYRtLpW82z38PluXfDlL2pMPZGGGoKDVcZOqSW5RAa3gev3ZjSEitS7MgXflOLjrKZKeA-SikbQ4xVN3hpWeuHXXReOaJfjOsTi2CGYMULfjP1coAw-INWoo4BJqpSkL3kxiw9vfJGfozGOdyFwkT17FIVwBvICSsfrBmtoT0_J_suNuVNQNqYHX6M-ec20dq33nm6LEdBMJ_SP4xzV7QYFLMbWhzrSTVqbMdtmj6XGCFipJKVF32yjKzZHfdVkckJeUPjByXbdCsOijTacQWM_BUYMJyv_p9fsXrc8WP1jhfff9ICaQqhUcugK1kETlV4lqidQnPjrZd-o-TdI-a6RXobX_v5-cm0soC5s0GatPS0rnryemzmzpypAEPvv13Z41JzVygl2EstrA6ZhCVxebHh40rMSwRFp4zhJMht2SvIBVH8WdocXrKNTzl6EIAJC8KUj72yfRz6_xm_wOMNkTstkvC35ANDqj8BZY0HejNcocdN_zXzbKq3I4Ru1s4EoqtNrv7Xa-uLfp2-MSYEmMQrYBbu1-P9Ln833uaDILYw6oEOWDH0nKWx_9-HWBDFn2G9JGUTSA8mfnILEx9I74j-2Unv7fBciLWjdMErEGmwOnIMIr0ajg045PWUxvfshMROpWs5oDdcVKfO9Pw5MmSHDJka19g1DeS3fhGmt-ejwpJHO9w6KO1gcE2W1UH5S_N4aXFIcAS8FNnP_7BV0796bpQVS_j49tv2L2gTbx7y-3qHLY_Cnk2y5m4h7jAxIyx5H-PxdHM3Z9qg9U4nU7wtwQOs5Z5AhahNoH18yNE3geod6-F6EG6aY2UjOe42tPrIsxTD3lMkBx6bdmyitsXnDU-Xsj60_vH62rW9RNuZUpkTaGdaitsBRYT1QMDQu_X6tUZiklcOqFXjKKHP6RCQFUwtlIvvE3Y40SAW4BfLGWOFbrFKkjuYNxm4BMFh5QaY4bBoq_LYL8gg1djribTdnLcUucirEGU3CGVlM3wTOtJvzGGBrdeCOlYFnzTBvzbQs8VPJy_4TWxWqUirqyFKjP6Fjkhz4pXyex4OrfQhIkPX35rwv0uvn-fAcEZbXUZv0SO8nt0ko9A8EClCbcewmIByDTfMxGHHVagA_0gx8Nby-6YXQ_bGI_pXNyW-pVOyXTMwZU8fpVtfHbgE3vVYDtWi17ZEsa42fALu9KYDnV1ElgimBwT0ASID4O9RWakcvsD5yphTuz9gBpRFfINozwkUV7WXAqgdJsXygYaCF0PSnKTAVBiyaXmNhLNKAQfFCzQVwJtMHScIN-9EvqrmlFCDhPhWVB68bMH6EoVduaCokTPYoqP3WEkyqN8eloiabdGBpIM4kZSX6aX4O8HJMgpActer1hwpgQZXiShMjYmgWiwjEnQtPRr4nArtCD4RO2KhtDsHRJfT-XMBx2e8faQh0cSCi4LQd6AhUT-787pvanmdPbhpg4gqVOJaKWNd0ggOhlcIz3meaw4U7bPDY5seAltQJlMS89kyZQUuRJYjuJQrSPuiH7yklggMnmmCT--uAAEr2w_jtVLSAWGkEomtsFmLe0n4_CDxXr8-wWdQ3XqqgYC4-QOqSoeNDI8ggrp9hxy50ysWIdATgwIVbYlbmqpHy55XwTOzE8TYwsSPsxlwHuBe20_fea6U64-7kDLU4U56za49aCRV3DH4Mmc3ZciDZRO1FWmL_TctJq628d2pgYdV9CrZHdVtJlji7sDtqO3hRT1NMwjawdtmU7qeruh_hRZjLfyNo6GsKKMgd9s_42V8-g8EKLkR7as6tH1ANytHlQRhENajAryEQ89_ZoPTyfyCGShL04EcsZJV6EGUIWUAK0Mv0PTU3_tNV8iea_SWh7A-IlSfP92QClBQNm2zCNBA-J9UCZC0R5SfMVkXntJtseA1b12bF_M-nvIdLyVi-pk3l4TkiWwVcULH28Pl3KA7jJ-W8eroWNcD4dZu1BTOYsG0fONoHItjOTScpxaNvWqMVs1eUW1gOFwnNogJcMJRb70leRiwXlcyrZ3VXqqtn63bOA-tYq87TC7IANgRu9sJ5tyKm6-a7DKncmUq8EcWdim4MtS9ody8sU1-7cQ0yLF36LQGLDlg1u9RWJ-_9T--nGgzqTF7qjN3KN2YgALGW0JtQ4ExRolnmTRefJzmi424aijscDtWVKw2hlpnNoE8BxBVXgkeczd3qC0XjW7GbP3ic6nAIXb7UovpjBg6ADPPqH9P2-smjCQcUHvQVMo-EM7CSQmcZ1inr_o9FwxQboKtfjaNgiKF9XihTTbkIqc6yToSnWDIAXFXCjBUp7Tfn9nBS3xku-XUuuV253-8ARawtgexVI_qTHCaDaIox-2Nn_DnNjaQKsTPw8mJyElpqa4gABUtBsun70hDzF4Rl-ix3i_eFi7ZZDI9htvrj73WzU1_8B-Zc1S3oBQY-oILn0JnSqIwdG7oa8pEE4g25XdRwPjQXY3LU17No5uC8cuxAI1k9TQWcjmsS1Kug6hjpbsChYgBd7pkP4qZBjTqGLm6ou_82LDsaL05wiExHF9IcnMfARpdhA1u0WG2U1XRTCzntDj2iq3UQ551Vr0YOM9o-f7Ki8scOY16_9VVyFKxEHFGKQl9PEi256m9OnHAsXzDKZtK7EEwuNLgUr0iKHhfEaGvlt4Bx-ZAwWZmpYp7EAi46Ga127CbZv_cVhyDyBOpFTrECH_ybOIfmBWsYaawyG9kZDZnCio-kfmvFBXyGcjGsz5aJUsf4VKF166Fe52BdXnBg1clTuGfxKdJK0mDWSFztTUDwLfi_s_Jy-xEv0WyXeR4WbRqvlLHBoKqz6qsKTttxtyS1NfopjzIZ_JbUTeytQW-5ooCWqbOm_2dEGjEXdWIAZinHbMwWV_6yg2S5GHbjzzDVKD9cnLRanXk3NzE-wdZETX4jWuBaA== -------------------------------------------------------------------------------- /pyTD/__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 os 24 | import logging 25 | 26 | __author__ = 'Addison Lynch' 27 | __version__ = '0.0.1' 28 | 29 | DEFAULT_CONFIG_DIR = '~/.tdm' 30 | CONFIG_DIR = os.path.expanduser(os.getenv("TD_CONFIG_DIR", DEFAULT_CONFIG_DIR)) 31 | 32 | DEFAULT_SSL_DIR = os.path.join(CONFIG_DIR, 'ssl') 33 | PACKAGE_DIR = os.path.abspath(__name__) 34 | 35 | BASE_URL = 'https://api.tdameritrade.com/v1/' 36 | BASE_AUTH_URL = 'https://auth.tdameritrade.com/auth' 37 | 38 | # routing for configure 39 | from pyTD.api import configure # noqa 40 | 41 | # Store logging level 42 | log_level = "INFO" 43 | LEVEL = getattr(logging, os.getenv("TD_LOG_LEVEL", log_level)) 44 | 45 | # Set logging level 46 | logger = logging.getLogger(__name__) 47 | logger.setLevel(LEVEL) 48 | 49 | # create formatter and add it to the handlers 50 | fh = logging.FileHandler(os.path.join(CONFIG_DIR, 'pyTD.log'), delay=True) 51 | file_format = logging.Formatter( 52 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 53 | fh.setFormatter(file_format) 54 | fh.setLevel(logging.DEBUG) 55 | logger.addHandler(fh) 56 | 57 | console_format = logging.Formatter( 58 | '%(levelname)s - %(message)s') 59 | ch = logging.StreamHandler() 60 | ch.setFormatter(console_format) 61 | logger.addHandler(ch) 62 | -------------------------------------------------------------------------------- /pyTD/auth/__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 | import logging 26 | 27 | from functools import wraps 28 | 29 | from pyTD.auth.manager import TDAuthManager 30 | from pyTD.auth.server import TDAuthServer 31 | from pyTD.utils import yn_require 32 | from pyTD.utils.exceptions import AuthorizationError 33 | 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | def auth_check(func): 39 | @wraps(func) 40 | def _authenticate_wrapper(self, *args, **kwargs): 41 | if self.api.auth_valid is True: 42 | return func(self, *args, **kwargs) 43 | else: 44 | if self.api.refresh_valid is False: 45 | logger.warning("Need new refresh token.") 46 | choice = yn_require("Would you like to authorize a new " 47 | "refresh token?") 48 | if choice is True: 49 | self.api.refresh_auth() 50 | else: 51 | raise AuthorizationError("Refresh token " 52 | "needed for access.") 53 | else: 54 | self.api.auth.refresh_access_token() 55 | if self.api.auth_valid is True: 56 | return func(self, *args, **kwargs) 57 | else: 58 | raise AuthorizationError("Authorization could not be " 59 | "completed.") 60 | return _authenticate_wrapper 61 | -------------------------------------------------------------------------------- /pyTD/auth/_static/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pyTD Authorization 5 | 6 | 7 | 8 | 9 |
10 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /pyTD/auth/_static/failed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pyTD Authorization 5 | 6 | 7 | 8 |
9 |

Authorization Failed

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pyTD/auth/_static/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat'); 2 | h1{font-family: "montserrat"; font-size=18px; font-weight: bold;} 3 | 4 | html, 5 | body { 6 | height: 100%; 7 | background-color: #b3b3b3; 8 | } 9 | 10 | body { 11 | display: -ms-flexbox; 12 | display: -webkit-box; 13 | display: flex; 14 | -ms-flex-align: center; 15 | -ms-flex-pack: center; 16 | -webkit-box-align: center; 17 | align-items: center; 18 | -webkit-box-pack: center; 19 | justify-content: center; 20 | padding-top: 40px; 21 | padding-bottom: 40px; 22 | background-color: #f5f5f5; 23 | } 24 | 25 | .form-signin { 26 | background-color: #314b7b; 27 | width: 100%; 28 | max-width: 330px; 29 | padding: 15px; 30 | margin: 0 auto; 31 | } 32 | -------------------------------------------------------------------------------- /pyTD/auth/_static/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pyTD Authorization 5 | 6 | 7 | 8 | 9 |
10 |

Success

11 |
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 | --------------------------------------------------------------------------------