├── .github └── workflows │ └── pytest.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── fugle_realtime ├── __init__.py ├── base_client.py ├── http_client │ ├── __init__.py │ ├── historical.py │ ├── http_client.py │ └── intraday.py └── websocket_client │ ├── __init__.py │ ├── intraday.py │ ├── websocket_client.py │ └── ws.py ├── poetry.lock ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt └── tests ├── __init__.py ├── test_fugle_realtime.py ├── test_http_client.py └── test_websocket_client.py /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 5 15 | matrix: 16 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements-dev.txt 28 | - name: Run Tests 29 | run: | 30 | pytest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [0.4.2](https://github.com/fugle-dev/fugle-realtime-python/compare/v0.4.1...v0.4.2) (2023-04-08) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * access the correct API endpoints on Windows ([8eca856](https://github.com/fugle-dev/fugle-realtime-python/commit/8ae0dacf96de9e2ab0e5ba7185f3c4a4cf76d2a8)) 9 | 10 | ### [0.4.1](https://github.com/fugle-dev/fugle-realtime-python/compare/v0.4.0...v0.4.1) (2022-12-26) 11 | 12 | ### [0.4.0](https://github.com/fugle-dev/fugle-realtime-python/compare/v0.3.2...v0.4.0) (2022-12-08) 13 | 14 | ### Features 15 | 16 | * add support for historical api ([#10](https://github.com/fugle-dev/fugle-realtime-python/issues/10)) ([4bfe8a9](https://github.com/fugle-dev/fugle-realtime-python/commit/4bfe8a99a6cf9faaffe29eef232d7b63fd823b42)) 17 | 18 | ### [0.3.2](https://github.com/fugle-dev/fugle-realtime-python/compare/v0.3.1...v0.3.2) (2022-11-21) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * missing error handler for websocket client ([8eca856](https://github.com/fugle-dev/fugle-realtime-python/commit/8eca8563e238a9963fb0c3c183defe46f880fed7)) 24 | 25 | ### [0.3.1](https://github.com/fugle-dev/fugle-realtime-python/compare/v0.3.0...v0.3.1) (2021-12-02) 26 | 27 | ## 0.3.0 (2021-10-07) 28 | 29 | 30 | ### Features 31 | 32 | * add support for http client ([7bee4f6](https://github.com/fugle-dev/fugle-realtime-python/commit/7bee4f6aa118786450b19c5271cc24e4d12b294b)) 33 | * add support for websocket client ([abec483](https://github.com/fugle-dev/fugle-realtime-python/commit/abec483c24914137f4c2144da8da3695e2369e0b)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Fortuna Intelligence Co., Ltd. 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 all 13 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fugle Realtime 2 | 3 | [![PyPI version][pypi-image]][pypi-url] 4 | [![Python version][python-image]][python-url] 5 | [![Build Status][action-image]][action-url] 6 | 7 | > Fugle Realtime API client library for Python 8 | 9 | ## Install 10 | 11 | ```sh 12 | $ pip install fugle-realtime 13 | ``` 14 | 15 | ## Usage 16 | 17 | The library a Python client that supports HTTP API and WebSocket. 18 | 19 | ### HTTP API 20 | 21 | ```py 22 | from fugle_realtime import HttpClient 23 | 24 | api_client = HttpClient(api_token='demo') 25 | ``` 26 | 27 | #### intraday.meta 28 | 29 | ```py 30 | api_client.intraday.meta(symbolId='2884') 31 | ``` 32 | 33 | #### intraday.quote 34 | 35 | ```py 36 | api_client.intraday.quote(symbolId='2884') 37 | ``` 38 | 39 | #### intraday.chart 40 | 41 | ```py 42 | api_client.intraday.chart(symbolId='2884') 43 | ``` 44 | 45 | #### intraday.dealts 46 | 47 | ```py 48 | api_client.intraday.dealts(symbolId='2884', limit=50) 49 | ``` 50 | 51 | #### intraday.volumes 52 | 53 | ```py 54 | api_client.intraday.volumes(symbolId='2884') 55 | ``` 56 | 57 | #### historical.candles 58 | 59 | ```py 60 | api_client.historical.candles('2884', '2022-02-07', '2022-02-11', None) 61 | api_client.historical.candles('2884', None, None, 'open,high,low,close,volume,turnover,change') 62 | ``` 63 | 64 | ### Simple WebSocket Demo 65 | 66 | ```py 67 | import time 68 | from fugle_realtime import WebSocketClient 69 | 70 | def handle_message(message): 71 | print(message) 72 | 73 | def main(): 74 | ws_client = WebSocketClient(api_token='demo') 75 | ws = ws_client.intraday.quote(symbolId='2884', on_message=handle_message) 76 | ws.run_async() 77 | time.sleep(3) 78 | ws.close() 79 | 80 | if __name__ == '__main__': 81 | main() 82 | ``` 83 | 84 | ## Reference 85 | 86 | [Fugle Realtime API](https://developer.fugle.tw) 87 | 88 | ## License 89 | 90 | [MIT](LICENSE) 91 | 92 | [pypi-image]: https://img.shields.io/pypi/v/fugle-realtime 93 | [pypi-url]: https://pypi.org/project/fugle-realtime 94 | [python-image]: https://img.shields.io/pypi/pyversions/fugle-realtime 95 | [python-url]: https://pypi.org/project/fugle-realtime 96 | [action-image]: https://img.shields.io/github/actions/workflow/status/fugle-dev/fugle-realtime-python/pytest.yml?branch=master 97 | [action-url]: https://github.com/fugle-dev/fugle-realtime-py/actions/workflows/pytest.yml 98 | -------------------------------------------------------------------------------- /fugle_realtime/__init__.py: -------------------------------------------------------------------------------- 1 | from .http_client import HttpClient 2 | from .websocket_client import WebSocketClient 3 | 4 | __version__ = '0.4.2' 5 | -------------------------------------------------------------------------------- /fugle_realtime/base_client.py: -------------------------------------------------------------------------------- 1 | class BaseClient(object): 2 | def __init__(self, api_token=None, api_version=None, url=None): 3 | self.config = {'api_token': api_token, 'api_version': api_version, 'url': url } 4 | 5 | def set_api_token(self, api_token): 6 | self.config['api_token'] = api_token 7 | 8 | def set_api_version(self, api_version): 9 | self.config['api_version'] = api_version 10 | -------------------------------------------------------------------------------- /fugle_realtime/http_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .http_client import HttpClient -------------------------------------------------------------------------------- /fugle_realtime/http_client/historical.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | import os 3 | import requests 4 | 5 | 6 | class Historical: 7 | def __init__(self, config): 8 | self.config = config 9 | 10 | # from is reserved keyword in Python 11 | def candles(self, symbolId, start, end, fields): 12 | params = {} 13 | if symbolId is not None: params['symbolId'] = symbolId 14 | if start is not None: params['from'] = start 15 | if end is not None: params['to'] = end 16 | if fields is not None: params['fields'] = fields 17 | 18 | return requests.get(self.compile_url('/candles', params)).json() 19 | 20 | def compile_url(self, path, params): 21 | params['apiToken'] = self.config['api_token'] 22 | base_url = self.config['url'] + '/marketdata/' + self.config['api_version'] 23 | endpoint = path if (path.startswith('/')) else '/' + path 24 | query = '?' + urlencode(params) 25 | return base_url + endpoint + query 26 | -------------------------------------------------------------------------------- /fugle_realtime/http_client/http_client.py: -------------------------------------------------------------------------------- 1 | from ..base_client import BaseClient 2 | from .intraday import Intraday 3 | from .historical import Historical 4 | 5 | FUGLE_REALTIME_HTTP_URL = 'https://api.fugle.tw' 6 | 7 | 8 | class HttpClient(BaseClient): 9 | def __init__(self, api_token='demo', api_version='v0.3'): 10 | super().__init__(api_token, api_version, url=FUGLE_REALTIME_HTTP_URL) 11 | 12 | @property 13 | def intraday(self): 14 | return Intraday(self.config) 15 | 16 | @property 17 | def historical(self): 18 | return Historical(self.config) 19 | -------------------------------------------------------------------------------- /fugle_realtime/http_client/intraday.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | import os 3 | import requests 4 | 5 | 6 | class Intraday: 7 | def __init__(self, config): 8 | self.config = config 9 | 10 | def meta(self, **params): 11 | return requests.get(self.compile_url('/intraday/meta', params)).json() 12 | 13 | def quote(self, **params): 14 | return requests.get(self.compile_url('/intraday/quote', params)).json() 15 | 16 | def chart(self, **params): 17 | return requests.get(self.compile_url('/intraday/chart', params)).json() 18 | 19 | def dealts(self, **params): 20 | return requests.get(self.compile_url('/intraday/dealts', params)).json() 21 | 22 | def volumes(self, **params): 23 | return requests.get(self.compile_url('/intraday/volumes', params)).json() 24 | 25 | def compile_url(self, path, params): 26 | params['apiToken'] = self.config['api_token'] 27 | base_url = self.config['url'] + '/realtime/' + self.config['api_version'] 28 | endpoint = path if (path.startswith('/')) else '/' + path 29 | query = '?' + urlencode(params) 30 | return base_url + endpoint + query 31 | -------------------------------------------------------------------------------- /fugle_realtime/websocket_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .websocket_client import WebSocketClient 2 | -------------------------------------------------------------------------------- /fugle_realtime/websocket_client/intraday.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | from .ws import WSClient 3 | 4 | 5 | class Intraday: 6 | def __init__(self, config): 7 | self.config = config 8 | 9 | def meta(self, on_message=None, on_open=None, on_close=None, on_error=None, **params): 10 | return self.createSocket(self.compile_url('/intraday/meta', params), on_message, on_open, on_close, on_error) 11 | 12 | def quote(self, on_message=None, on_open=None, on_close=None, on_error=None, **params): 13 | return self.createSocket(self.compile_url('/intraday/quote', params), on_message, on_open, on_close, on_error) 14 | 15 | def chart(self, on_message=None, on_open=None, on_close=None, on_error=None, **params): 16 | return self.createSocket(self.compile_url('/intraday/chart', params), on_message, on_open, on_close, on_error) 17 | 18 | def createSocket(self, url, on_message, on_open, on_close, on_error): 19 | return WSClient(url, on_message, on_open, on_close, on_error) 20 | 21 | def compile_url(self, path, params): 22 | params['apiToken'] = self.config['api_token'] 23 | baseUrl = self.config['url'] + '/' + self.config['api_version'] 24 | endpoint = path if (path.startswith('/')) else '/' + path 25 | query = '?' + urlencode(params) 26 | return baseUrl + endpoint + query 27 | -------------------------------------------------------------------------------- /fugle_realtime/websocket_client/websocket_client.py: -------------------------------------------------------------------------------- 1 | from ..base_client import BaseClient 2 | from .intraday import Intraday 3 | 4 | FUGLE_REALTIME_WS_URL = 'wss://api.fugle.tw/realtime' 5 | 6 | 7 | class WebSocketClient(BaseClient): 8 | def __init__(self, api_token='demo', api_version='v0.3'): 9 | super().__init__(api_token, api_version, url=FUGLE_REALTIME_WS_URL) 10 | 11 | @property 12 | def intraday(self): 13 | return Intraday(self.config) 14 | -------------------------------------------------------------------------------- /fugle_realtime/websocket_client/ws.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import websocket 3 | 4 | 5 | class WSClient: 6 | def __init__(self, url: str, on_message=None, on_open=None, on_close=None, on_error=None): 7 | self.ws = websocket.WebSocketApp( 8 | url, 9 | on_open=self._default_on_open, 10 | on_close=self._default_on_close, 11 | on_error=self._default_on_error, 12 | on_message=self._default_on_message, 13 | ) 14 | 15 | self._url = url 16 | self._handle_message = on_message 17 | self._handle_open = on_open 18 | self._handle_close = on_close 19 | self._handle_error = on_error 20 | self._run_thread = None 21 | 22 | def run(self): 23 | self.ws.run_forever() 24 | 25 | def run_async(self): 26 | self._run_thread = threading.Thread(target=self.run) 27 | self._run_thread.start() 28 | 29 | def close(self): 30 | self.ws.close() 31 | if self._run_thread: 32 | self._run_thread.join() 33 | 34 | def _default_on_message(self, ws, message): 35 | if self._handle_message: 36 | self._handle_message(message) 37 | 38 | def _default_on_open(self, ws): 39 | if self._handle_open: 40 | self._handle_open() 41 | 42 | def _default_on_close(self, ws, close_status_code, close_msg): 43 | if self._handle_close: 44 | self._handle_close() 45 | 46 | def _default_on_error(self, ws, error): 47 | if self._handle_error: 48 | self._handle_error(error) 49 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "attrs" 5 | version = "22.2.0" 6 | description = "Classes Without Boilerplate" 7 | category = "dev" 8 | optional = false 9 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 10 | files = [ 11 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 12 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 13 | ] 14 | 15 | [package.extras] 16 | dev = ["coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] 17 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 18 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] 19 | tests-no-zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] 20 | 21 | [[package]] 22 | name = "certifi" 23 | version = "2022.12.7" 24 | description = "Python package for providing Mozilla's CA Bundle." 25 | category = "main" 26 | optional = false 27 | python-versions = "*" 28 | files = [ 29 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 30 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 31 | ] 32 | 33 | [[package]] 34 | name = "charset-normalizer" 35 | version = "2.0.6" 36 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 37 | category = "main" 38 | optional = false 39 | python-versions = ">=3.5.0" 40 | files = [ 41 | {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, 42 | {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, 43 | ] 44 | 45 | [package.extras] 46 | unicode-backport = ["unicodedata2"] 47 | 48 | [[package]] 49 | name = "colorama" 50 | version = "0.4.4" 51 | description = "Cross-platform colored terminal text." 52 | category = "dev" 53 | optional = false 54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 55 | files = [ 56 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 57 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 58 | ] 59 | 60 | [[package]] 61 | name = "coverage" 62 | version = "5.5" 63 | description = "Code coverage measurement for Python" 64 | category = "dev" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 67 | files = [ 68 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 69 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 70 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 71 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 72 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 73 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 74 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 75 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 76 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 77 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 78 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 79 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 80 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 81 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 82 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 83 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 84 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 85 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 86 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 87 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 88 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 89 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 90 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 91 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 92 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 93 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 94 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 95 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 96 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 97 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 98 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 99 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 100 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 101 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 102 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 103 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 104 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 105 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 106 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 107 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 108 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 109 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 110 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 111 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 112 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 113 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 114 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 115 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 116 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 117 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 118 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 119 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 120 | ] 121 | 122 | [package.extras] 123 | toml = ["toml"] 124 | 125 | [[package]] 126 | name = "exceptiongroup" 127 | version = "1.0.4" 128 | description = "Backport of PEP 654 (exception groups)" 129 | category = "dev" 130 | optional = false 131 | python-versions = ">=3.7" 132 | files = [ 133 | {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, 134 | {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, 135 | ] 136 | 137 | [package.extras] 138 | test = ["pytest (>=6)"] 139 | 140 | [[package]] 141 | name = "idna" 142 | version = "3.2" 143 | description = "Internationalized Domain Names in Applications (IDNA)" 144 | category = "main" 145 | optional = false 146 | python-versions = ">=3.5" 147 | files = [ 148 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 149 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 150 | ] 151 | 152 | [[package]] 153 | name = "importlib-metadata" 154 | version = "4.8.1" 155 | description = "Read metadata from Python packages" 156 | category = "dev" 157 | optional = false 158 | python-versions = ">=3.6" 159 | files = [ 160 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 161 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 162 | ] 163 | 164 | [package.dependencies] 165 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 166 | zipp = ">=0.5" 167 | 168 | [package.extras] 169 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 170 | perf = ["ipython"] 171 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-perf (>=0.9.2)"] 172 | 173 | [[package]] 174 | name = "iniconfig" 175 | version = "1.1.1" 176 | description = "iniconfig: brain-dead simple config-ini parsing" 177 | category = "dev" 178 | optional = false 179 | python-versions = "*" 180 | files = [ 181 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 182 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 183 | ] 184 | 185 | [[package]] 186 | name = "packaging" 187 | version = "21.0" 188 | description = "Core utilities for Python packages" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=3.6" 192 | files = [ 193 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 194 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 195 | ] 196 | 197 | [package.dependencies] 198 | pyparsing = ">=2.0.2" 199 | 200 | [[package]] 201 | name = "pluggy" 202 | version = "0.13.1" 203 | description = "plugin and hook calling mechanisms for python" 204 | category = "dev" 205 | optional = false 206 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 207 | files = [ 208 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 209 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 210 | ] 211 | 212 | [package.dependencies] 213 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 214 | 215 | [package.extras] 216 | dev = ["pre-commit", "tox"] 217 | 218 | [[package]] 219 | name = "pyparsing" 220 | version = "2.4.7" 221 | description = "Python parsing module" 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 225 | files = [ 226 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 227 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 228 | ] 229 | 230 | [[package]] 231 | name = "pytest" 232 | version = "7.2.0" 233 | description = "pytest: simple powerful testing with Python" 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=3.7" 237 | files = [ 238 | {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, 239 | {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, 240 | ] 241 | 242 | [package.dependencies] 243 | attrs = ">=19.2.0" 244 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 245 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 246 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 247 | iniconfig = "*" 248 | packaging = "*" 249 | pluggy = ">=0.12,<2.0" 250 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 251 | 252 | [package.extras] 253 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 254 | 255 | [[package]] 256 | name = "pytest-cov" 257 | version = "2.12.1" 258 | description = "Pytest plugin for measuring coverage." 259 | category = "dev" 260 | optional = false 261 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 262 | files = [ 263 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 264 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 265 | ] 266 | 267 | [package.dependencies] 268 | coverage = ">=5.2.1" 269 | pytest = ">=4.6" 270 | toml = "*" 271 | 272 | [package.extras] 273 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 274 | 275 | [[package]] 276 | name = "pytest-mock" 277 | version = "3.6.1" 278 | description = "Thin-wrapper around the mock package for easier use with pytest" 279 | category = "dev" 280 | optional = false 281 | python-versions = ">=3.6" 282 | files = [ 283 | {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, 284 | {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, 285 | ] 286 | 287 | [package.dependencies] 288 | pytest = ">=5.0" 289 | 290 | [package.extras] 291 | dev = ["pre-commit", "pytest-asyncio", "tox"] 292 | 293 | [[package]] 294 | name = "requests" 295 | version = "2.26.0" 296 | description = "Python HTTP for Humans." 297 | category = "main" 298 | optional = false 299 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 300 | files = [ 301 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 302 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 303 | ] 304 | 305 | [package.dependencies] 306 | certifi = ">=2017.4.17" 307 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 308 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 309 | urllib3 = ">=1.21.1,<1.27" 310 | 311 | [package.extras] 312 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 313 | use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] 314 | 315 | [[package]] 316 | name = "toml" 317 | version = "0.10.2" 318 | description = "Python Library for Tom's Obvious, Minimal Language" 319 | category = "dev" 320 | optional = false 321 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 322 | files = [ 323 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 324 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 325 | ] 326 | 327 | [[package]] 328 | name = "tomli" 329 | version = "2.0.1" 330 | description = "A lil' TOML parser" 331 | category = "dev" 332 | optional = false 333 | python-versions = ">=3.7" 334 | files = [ 335 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 336 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 337 | ] 338 | 339 | [[package]] 340 | name = "typing-extensions" 341 | version = "3.10.0.2" 342 | description = "Backported and Experimental Type Hints for Python 3.5+" 343 | category = "dev" 344 | optional = false 345 | python-versions = "*" 346 | files = [ 347 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 348 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 349 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 350 | ] 351 | 352 | [[package]] 353 | name = "urllib3" 354 | version = "1.26.6" 355 | description = "HTTP library with thread-safe connection pooling, file post, and more." 356 | category = "main" 357 | optional = false 358 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 359 | files = [ 360 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 361 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 362 | ] 363 | 364 | [package.extras] 365 | brotli = ["brotlipy (>=0.6.0)"] 366 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] 367 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 368 | 369 | [[package]] 370 | name = "websocket-client" 371 | version = "1.2.1" 372 | description = "WebSocket client for Python with low level API options" 373 | category = "main" 374 | optional = false 375 | python-versions = ">=3.6" 376 | files = [ 377 | {file = "websocket-client-1.2.1.tar.gz", hash = "sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d"}, 378 | {file = "websocket_client-1.2.1-py2.py3-none-any.whl", hash = "sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec"}, 379 | ] 380 | 381 | [package.extras] 382 | optional = ["python-socks", "wsaccel"] 383 | test = ["websockets"] 384 | 385 | [[package]] 386 | name = "zipp" 387 | version = "3.5.0" 388 | description = "Backport of pathlib-compatible object wrapper for zip files" 389 | category = "dev" 390 | optional = false 391 | python-versions = ">=3.6" 392 | files = [ 393 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 394 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 395 | ] 396 | 397 | [package.extras] 398 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 399 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 400 | 401 | [metadata] 402 | lock-version = "2.0" 403 | python-versions = "^3.7" 404 | content-hash = "6b7f4a0b1a97abf1632fd40d2e4cb9c628d7634d585e756d1b1d9893ee78e74a" 405 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fugle-realtime" 3 | version = "0.4.2" 4 | description = "Fugle Realtime API client library for Python" 5 | authors = ["Fortuna Intelligence Co., Ltd. "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/fugle-dev/fugle-realtime-py#readme" 9 | repository = "https://github.com/fugle-dev/fugle-realtime-py" 10 | documentation = "https://developer.fugle.tw" 11 | keywords = ["fugle", "realtime", "stock"] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.7" 15 | requests = "^2.26.0" 16 | websocket-client = "^1.2.1" 17 | 18 | [tool.poetry.dev-dependencies] 19 | pytest = "^7.2.0" 20 | pytest-mock = "^3.6.1" 21 | pytest-cov = "^2.12.1" 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | attrs==21.2.0 ; python_version >= "3.7" and python_version < "4.0" 2 | certifi==2021.5.30 ; python_version >= "3.7" and python_version < "4.0" 3 | charset-normalizer==2.0.6 ; python_version >= "3.7" and python_version < "4.0" 4 | colorama==0.4.4 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" 5 | coverage==5.5 ; python_version >= "3.7" and python_version < "4" 6 | exceptiongroup==1.0.4 ; python_version >= "3.7" and python_version < "3.11" 7 | idna==3.2 ; python_version >= "3.7" and python_version < "4.0" 8 | importlib-metadata==4.8.1 ; python_version >= "3.7" and python_version < "3.8" 9 | iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0" 10 | packaging==21.0 ; python_version >= "3.7" and python_version < "4.0" 11 | pluggy==0.13.1 ; python_version >= "3.7" and python_version < "4.0" 12 | pyparsing==2.4.7 ; python_version >= "3.7" and python_version < "4.0" 13 | pytest-cov==2.12.1 ; python_version >= "3.7" and python_version < "4.0" 14 | pytest-mock==3.6.1 ; python_version >= "3.7" and python_version < "4.0" 15 | pytest==7.2.0 ; python_version >= "3.7" and python_version < "4.0" 16 | requests==2.26.0 ; python_version >= "3.7" and python_version < "4.0" 17 | toml==0.10.2 ; python_version >= "3.7" and python_version < "4.0" 18 | tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11" 19 | typing-extensions==3.10.0.2 ; python_version >= "3.7" and python_version < "3.8" 20 | urllib3==1.26.6 ; python_version >= "3.7" and python_version < "4" 21 | websocket-client==1.2.1 ; python_version >= "3.7" and python_version < "4.0" 22 | zipp==3.5.0 ; python_version >= "3.7" and python_version < "3.8" 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \ 2 | --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8 \ 3 | --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee 4 | charset-normalizer==2.0.6; python_full_version >= "3.6.0" and python_version >= "3" \ 5 | --hash=sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f \ 6 | --hash=sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6 7 | idna==3.2; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" \ 8 | --hash=sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a \ 9 | --hash=sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3 10 | requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \ 11 | --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \ 12 | --hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7 13 | urllib3==1.26.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" \ 14 | --hash=sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4 \ 15 | --hash=sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f 16 | websocket-client==1.2.1; python_version >= "3.6" \ 17 | --hash=sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d \ 18 | --hash=sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fugle-dev/fugle-realtime-python/8f17289d27f0c946072a3dc6417fb7d292646196/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_fugle_realtime.py: -------------------------------------------------------------------------------- 1 | from fugle_realtime import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == '0.4.2' 6 | -------------------------------------------------------------------------------- /tests/test_http_client.py: -------------------------------------------------------------------------------- 1 | from fugle_realtime import HttpClient 2 | import requests 3 | 4 | 5 | class TestHttpClient(object): 6 | 7 | def test_default_config(self): 8 | client = HttpClient() 9 | assert client.config['api_token'] == 'demo' 10 | assert client.config['api_version'] == 'v0.3' 11 | 12 | def test_config_with_api_token(self): 13 | client = HttpClient(api_token='api-token') 14 | assert client.config['api_token'] == 'api-token' 15 | assert client.config['api_version'] == 'v0.3' 16 | 17 | def test_config_with_api_version(self): 18 | client = HttpClient(api_version='v0.2') 19 | assert client.config['api_token'] == 'demo' 20 | assert client.config['api_version'] == 'v0.2' 21 | 22 | def test_set_api_token(self): 23 | client = HttpClient() 24 | client.set_api_token('api-token') 25 | assert client.config['api_token'] == 'api-token' 26 | 27 | def test_set_api_version(self): 28 | client = HttpClient() 29 | client.set_api_version('v0.2') 30 | assert client.config['api_version'] == 'v0.2' 31 | 32 | def test_intraday_meta(self, mocker): 33 | mocker.patch('requests.get') 34 | client = HttpClient() 35 | client.intraday.meta(symbolId='2884') 36 | requests.get.assert_called_once_with( 37 | 'https://api.fugle.tw/realtime/v0.3/intraday/meta?symbolId=2884&apiToken=demo') 38 | 39 | def test_intraday_quote(self, mocker): 40 | mocker.patch('requests.get') 41 | client = HttpClient() 42 | client.intraday.quote(symbolId='2884') 43 | requests.get.assert_called_once_with( 44 | 'https://api.fugle.tw/realtime/v0.3/intraday/quote?symbolId=2884&apiToken=demo') 45 | 46 | def test_intraday_chart(self, mocker): 47 | mocker.patch('requests.get') 48 | client = HttpClient() 49 | client.intraday.chart(symbolId='2884') 50 | requests.get.assert_called_once_with( 51 | 'https://api.fugle.tw/realtime/v0.3/intraday/chart?symbolId=2884&apiToken=demo') 52 | 53 | def test_intraday_dealts(self, mocker): 54 | mocker.patch('requests.get') 55 | client = HttpClient() 56 | client.intraday.dealts(symbolId='2884', limit=50) 57 | requests.get.assert_called_once_with( 58 | 'https://api.fugle.tw/realtime/v0.3/intraday/dealts?symbolId=2884&limit=50&apiToken=demo') 59 | 60 | def test_intraday_volumes(self, mocker): 61 | mocker.patch('requests.get') 62 | client = HttpClient() 63 | client.intraday.volumes(symbolId='2884') 64 | requests.get.assert_called_once_with( 65 | 'https://api.fugle.tw/realtime/v0.3/intraday/volumes?symbolId=2884&apiToken=demo') 66 | 67 | def test_historical_candles(self, mocker): 68 | mocker.patch('requests.get') 69 | client = HttpClient() 70 | client.historical.candles('2884', '2022-02-07', '2022-02-11', 'open,high,low,close,volume,turnover,change') 71 | requests.get.assert_called_once_with( 72 | 'https://api.fugle.tw/marketdata/v0.3/candles?symbolId=2884&from=2022-02-07&to=2022-02-11&fields=open%2Chigh%2Clow%2Cclose%2Cvolume%2Cturnover%2Cchange&apiToken=demo') 73 | -------------------------------------------------------------------------------- /tests/test_websocket_client.py: -------------------------------------------------------------------------------- 1 | from fugle_realtime import WebSocketClient 2 | import websocket 3 | 4 | class TestHttpClient(object): 5 | 6 | def test_config(self): 7 | client = WebSocketClient() 8 | assert client.config['api_token'] == 'demo' 9 | assert client.config['api_version'] == 'v0.3' 10 | 11 | def test_config_with_api_token(self): 12 | client = WebSocketClient(api_token = 'api-token') 13 | assert client.config['api_token'] == 'api-token' 14 | assert client.config['api_version'] == 'v0.3' 15 | 16 | def test_config_with_api_version(self): 17 | client = WebSocketClient(api_version = 'v0.2') 18 | assert client.config['api_token'] == 'demo' 19 | assert client.config['api_version'] == 'v0.2' 20 | 21 | def test_set_api_token(self): 22 | client = WebSocketClient() 23 | client.set_api_token('api-token') 24 | assert client.config['api_token'] == 'api-token' 25 | 26 | def test_set_api_version(self): 27 | client = WebSocketClient() 28 | client.set_api_version('v0.2') 29 | assert client.config['api_version'] == 'v0.2' 30 | 31 | def test_intraday_meta(self, mocker): 32 | mocker.patch('websocket.WebSocketApp') 33 | client = WebSocketClient() 34 | ws = client.intraday.meta(symbolId='2884') 35 | assert ws._url == 'wss://api.fugle.tw/realtime/v0.3/intraday/meta?symbolId=2884&apiToken=demo' 36 | 37 | def test_intraday_quote(self, mocker): 38 | mocker.patch('websocket.WebSocketApp') 39 | client = WebSocketClient() 40 | ws = client.intraday.quote(symbolId='2884') 41 | assert ws._url == 'wss://api.fugle.tw/realtime/v0.3/intraday/quote?symbolId=2884&apiToken=demo' 42 | 43 | def test_intraday_chart(self, mocker): 44 | mocker.patch('websocket.WebSocketApp') 45 | client = WebSocketClient() 46 | ws = client.intraday.chart(symbolId='2884') 47 | assert ws._url == 'wss://api.fugle.tw/realtime/v0.3/intraday/chart?symbolId=2884&apiToken=demo' 48 | --------------------------------------------------------------------------------