├── .flake8 ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── pyproject.toml ├── tests ├── __init__.py └── test_client.py ├── tox.ini └── tpulse ├── __init__.py ├── settings.py └── sync_client.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | max-complexity = 5 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: [3.7, 3.8, 3.9] 16 | 17 | env: 18 | USING_COVERAGE: "3.9" 19 | 20 | steps: 21 | - name: Checkout sources 22 | uses: actions/checkout@v2 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install black coverage flake8 flit mccabe mypy pylint pytest tox tox-gh-actions httpx fake_useragent pytest_httpx 33 | 34 | - name: Run tox 35 | run: | 36 | python -m tox 37 | 38 | - name: Upload coverage to Codecov 39 | uses: codecov/codecov-action@v1 40 | if: contains(env.USING_COVERAGE, matrix.python-version) 41 | with: 42 | fail_ci_if_error: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | .idea/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | .pypirc 8 | 9 | 10 | # C extensions 11 | *.so 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 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 | .spyderprojectm 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 | .vscode/ 135 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=bad-continuation,duplicate-code,too-few-public-methods,consider-using-f-string,unnecessary-pass,C0103 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Semenov Artur 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tpulse-py 2 | 3 | > Tinkoff social pulse [api](https://www.tinkoff.ru/api/invest-gw/social/v1/) wrapper 4 | 5 | [![PyPI Version][pypi-image]][pypi-url] 6 | [![Build Status][build-image]][build-url] 7 | [![Code Coverage][coverage-image]][coverage-url] 8 | [![Code Quality][quality-image]][quality-url] 9 | [![Downloads](https://pepy.tech/badge/tpulse)](https://pepy.tech/project/tpulse) 10 | [![Downloads](https://pepy.tech/badge/tpulse/month)](https://pepy.tech/project/tpulse) 11 | 12 | 13 | 14 | [pypi-image]: https://img.shields.io/pypi/v/tpulse 15 | [pypi-url]: https://pypi.org/project/tpulse/ 16 | [build-image]: https://github.com/meanother/tpulse-py/actions/workflows/build.yml/badge.svg 17 | [build-url]: https://github.com/meanother/tpulse-py/actions/workflows/build.yml 18 | [coverage-image]: https://codecov.io/gh/meanother/tpulse-py/branch/main/graph/badge.svg 19 | [coverage-url]: https://codecov.io/gh/meanother/tpulse-py 20 | [quality-image]: https://api.codeclimate.com/v1/badges/ca8f259b0ad93f1f28ed/maintainability 21 | [quality-url]: https://codeclimate.com/github/meanother/tpulse-py 22 | 23 | 24 | ## Installation 25 | ```shell 26 | pip install tpulse 27 | ``` 28 | 29 | 30 | ## Usage example 31 | 32 | ### `get_user_info()` 33 | ```python 34 | >>> from tpulse import TinkoffPulse 35 | >>> from pprint import pp 36 | >>> pulse = TinkoffPulse() 37 | >>> 38 | >>> user_info = pulse.get_user_info("tomcapital") 39 | >>> pp(user_info) 40 | {'id': 'bfbc4cc2-7f98-472e-8f5f-a14bdd6fc4db', 41 | 'type': 'personal', 42 | 'nickname': 'TomCapital', 43 | 'status': 'open', 44 | 'image': '22ac448f-e271-463c-beb1-f035c7987f17', 45 | 'block': False, 46 | 'description': 'Эксклюзивная аналитика тут: https://t.me/tomcapital\n' 47 | '\n' 48 | 'Связь: https://t.me/TomCapCat\n' 49 | '\n' 50 | 'growth stocks strategy\n' 51 | '\n' 52 | 'Ты должен изучить правила игры. Затем начать играть лучше, ' 53 | 'чем кто-либо другой.', 54 | 'followersCount': 39704, 55 | 'followingCount': 13, 56 | 'isLead': False, 57 | 'serviceTags': [{'id': 'popular'}], 58 | 'statistics': {'totalAmountRange': {'lower': 3000000, 'upper': None}, 59 | 'yearRelativeYield': -5.68, 60 | 'monthOperationsCount': 98}, 61 | 'subscriptionDomains': None, 62 | 'popularHashtags': [], 63 | 'donationActive': True, 64 | 'isVisible': True, 65 | 'baseTariffCategory': 'unauthorized', 66 | 'strategies': [{'id': 'a48ee1fc-4eaa-47a3-a75c-a362d3c95cdf', 67 | 'title': 'Tactical Investing', 68 | 'riskProfile': 'moderate', 69 | 'relativeYield': 3.93, 70 | 'baseCurrency': 'usd', 71 | 'score': 4, 72 | 'portfolioValues': [...], 73 | 'characteristics': [{'id': 'recommended-base-money-position-quantity', 74 | 'value': '1\xa0100 $', 75 | 'subtitle': 'советуем вложить'}, 76 | {'id': 'slaves-count', 77 | 'value': '111', 78 | 'subtitle': 'подписаны'}]}, 79 | {'id': 'ff41c693-78dd-4c2e-b566-858770d6d2e0', 80 | 'title': 'Aggressive investing', 81 | 'riskProfile': 'aggressive', 82 | 'relativeYield': -8.19, 83 | 'baseCurrency': 'usd', 84 | 'score': 3, 85 | 'portfolioValues': [...], 86 | 'characteristics': [{'id': 'recommended-base-money-position-quantity', 87 | 'value': '1\xa0000 $', 88 | 'subtitle': 'советуем вложить'}, 89 | {'id': 'slaves-count', 90 | 'value': '17', 91 | 'subtitle': 'подписаны'}]}]} 92 | ``` 93 | ### `get_posts_by_user_id()` 94 | ```python 95 | >>> user_posts = pulse.get_posts_by_user_id("bfbc4cc2-7f98-472e-8f5f-a14bdd6fc4db") 96 | >>> pp(user_posts) 97 | ... 98 | >>> pp(user_posts["items"][0]) 99 | {'id': '2ab5457c-aa9d-4a9b-b7ea-7af49459f0f9', 100 | 'text': 'Множество акций испытали массивную коррекцию за последние несколько ' 101 | 'недель, особенно это касается growth-историй (компаний, чей ' 102 | 'потенциал и денежные потоки должны раскрыться в будущем). На ' 103 | 'фондовый рынок обрушилась целая лавина плохих новостей (высказывания ' 104 | 'Пауэлла, тейперинг, Omicron и тд), и, на мой взгляд, мы увидели ' 105 | 'некую чрезмерную реакцию рынка.\n' 106 | '\n' 107 | 'Часто, когда фондовый рынок заранее корректируется и закладывает те ' 108 | 'или иные негативные события в оценку активов, то уже непосредственно ' 109 | 'по факту наступления этих самых событий, рынок, как правило, ' 110 | 'успевает переварить их, и, наоборот, раллирует. Особенно, если ' 111 | 'случилась избыточная или даже паническая реакция на негатив.\n' 112 | '\n' 113 | 'Марко Коланович, главный стратег JPMorgan, оценивает вероятность ' 114 | 'шорт-сквиз ралли в ближайшие недели, как высокую, и я, пожалуй, буду ' 115 | 'придерживаться такой же точки зрения.', 116 | 'likesCount': 42, 117 | 'commentsCount': 10, 118 | 'isLiked': False, 119 | 'inserted': '2021-12-22T15:22:38.016128+03:00', 120 | 'isEditable': False, 121 | 'instruments': [], 122 | 'profiles': [], 123 | 'serviceTags': [], 124 | 'profileId': 'bfbc4cc2-7f98-472e-8f5f-a14bdd6fc4db', 125 | 'nickname': 'TomCapital', 126 | 'image': '22ac448f-e271-463c-beb1-f035c7987f17', 127 | 'postImages': [], 128 | 'hashtags': [], 129 | 'owner': {'id': 'bfbc4cc2-7f98-472e-8f5f-a14bdd6fc4db', 130 | 'nickname': 'TomCapital', 131 | 'image': '22ac448f-e271-463c-beb1-f035c7987f17', 132 | 'donationActive': False, 133 | 'block': False, 134 | 'serviceTags': [{'id': 'popular'}]}, 135 | 'reactions': {'totalCount': 42, 136 | 'myReaction': None, 137 | 'counters': [{'type': 'like', 'count': 42}]}, 138 | 'content': {'type': 'simple', 139 | 'text': '', 140 | 'instruments': [], 141 | 'hashtags': [], 142 | 'profiles': [], 143 | 'images': [], 144 | 'strategies': []}, 145 | 'baseTariffCategory': 'unauthorized', 146 | 'isBookmarked': False, 147 | 'status': 'published'} 148 | ``` 149 | 150 | ### `get_posts_by_ticker()` 151 | ```python 152 | >>> ticker_posts = pulse.get_posts_by_ticker("AAPL") 153 | >>> pp(ticker_posts) 154 | ... 155 | >>> pp(ticker_posts["items"][5]) 156 | {'id': '320b8e15-fe8c-46e9-b29b-12ef278be135', 157 | 'text': '{$AAPL} продажу поставил на 176 $', 158 | 'likesCount': 0, 159 | 'commentsCount': 6, 160 | 'isLiked': False, 161 | 'inserted': '2021-12-23T11:54:50.603445+03:00', 162 | 'isEditable': False, 163 | 'instruments': [{'type': 'share', 164 | 'ticker': 'AAPL', 165 | 'lastPrice': 176.02, 166 | 'currency': 'usd', 167 | 'image': 'US0378331005.png', 168 | 'briefName': 'Apple', 169 | 'dailyYield': None, 170 | 'relativeDailyYield': 0.0, 171 | 'price': 175.34, 172 | 'relativeYield': 0.39}], 173 | 'profiles': [], 174 | 'serviceTags': [], 175 | 'profileId': '436a1012-3c5d-4c84-879b-a4e434f43230', 176 | 'nickname': 'TNEO', 177 | 'image': 'fc85fbc9-ef4a-4045-905d-bd6fb581689c', 178 | 'postImages': [], 179 | 'hashtags': [], 180 | 'owner': {'id': '436a1012-3c5d-4c84-879b-a4e434f43230', 181 | 'nickname': 'TNEO', 182 | 'image': 'fc85fbc9-ef4a-4045-905d-bd6fb581689c', 183 | 'donationActive': False, 184 | 'block': False, 185 | 'serviceTags': []}, 186 | 'reactions': {'totalCount': 0, 'myReaction': None, 'counters': []}, 187 | 'content': {'type': 'simple', 188 | 'text': '', 189 | 'instruments': [{'type': 'share', 190 | 'ticker': 'AAPL', 191 | 'lastPrice': 176.02, 192 | 'currency': 'usd', 193 | 'image': 'US0378331005.png', 194 | 'briefName': 'Apple', 195 | 'dailyYield': None, 196 | 'relativeDailyYield': 0.0, 197 | 'price': 175.34, 198 | 'relativeYield': 0.39}], 199 | 'hashtags': [], 200 | 'profiles': [], 201 | 'images': [], 202 | 'strategies': []}, 203 | 'baseTariffCategory': 'unauthorized', 204 | 'isBookmarked': False, 205 | 'status': 'published'} 206 | ``` 207 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "tpulse" 7 | authors = [{name = "Semenov Artur", email = "juicehq@yandex.ru"}] 8 | license = {file = "LICENSE"} 9 | classifiers = ["License :: OSI Approved :: MIT License"] 10 | dynamic = ["version", "description"] 11 | readme = "README.md" 12 | requires-python = ">=3.7" 13 | dependencies = [ "httpx >= 0.21.1", "fake_useragent >= 0.1.11"] 14 | 15 | [project.urls] 16 | Home = "https://github.com/meanother/tpulse-py" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanother/tpulse-py/78795ab687a5962a1b7ea5c1c14bcaf550a3b451/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from pytest_httpx import HTTPXMock 4 | 5 | from tpulse.sync_client import PostClient, PulseClient, UserClient, ua 6 | 7 | client = PulseClient() 8 | args = "?appName=invest&origin=web&platform=web" 9 | 10 | headers = { 11 | "Content-type": "application/json", 12 | "Accept": "application/json", 13 | "User-agent": ua, 14 | } 15 | 16 | 17 | def test_ua(): 18 | assert type(ua) == str 19 | assert len(ua) > 1 20 | 21 | 22 | def test_init_user(): 23 | user = UserClient() 24 | assert user._client.headers["Content-type"] == "application/json" 25 | assert user._client.headers["Accept"] == "application/json" 26 | assert user._client.headers["User-agent"] == ua 27 | 28 | 29 | def test_init_post(): 30 | post = PostClient() 31 | assert post._client.headers["Content-type"] == "application/json" 32 | assert post._client.headers["Accept"] == "application/json" 33 | assert post._client.headers["User-agent"] == ua 34 | 35 | 36 | def test_get_user_info(httpx_mock: HTTPXMock): 37 | expected = { 38 | "id": "08efcec6", 39 | "type": "personal", 40 | "nickname": "finvestpaper", 41 | "status": "open", 42 | "image": "df91ef92", 43 | "block": False, 44 | "description": "description", 45 | "followersCount": 73, 46 | "followingCount": 11, 47 | "isLead": False, 48 | "serviceTags": [], 49 | "statistics": { 50 | "totalAmountRange": {"lower": 1000000, "upper": 3000000}, 51 | "yearRelativeYield": 500.00, 52 | "monthOperationsCount": 97, 53 | }, 54 | "subscriptionDomains": None, 55 | "popularHashtags": [], 56 | "donationActive": True, 57 | "isVisible": True, 58 | "baseTariffCategory": "unauthorized", 59 | "strategies": [], 60 | } 61 | httpx_mock.add_response( 62 | method="GET", 63 | headers=headers, 64 | url=f"{UserClient.BASE_URL}profile/nickname/finvestpaper{args}", 65 | json={"status": "Ok", "payload": expected}, 66 | ) 67 | actual = client.get_user_info("finvestpaper") 68 | assert actual == expected 69 | 70 | 71 | def test_get_posts_by_user_id(httpx_mock: HTTPXMock): 72 | expected = { 73 | "nextCursor": 171318, 74 | "hasNext": False, 75 | "items": [ 76 | {}, 77 | ], 78 | } 79 | httpx_mock.add_response( 80 | method="GET", 81 | headers=headers, 82 | url=f"{UserClient.BASE_URL}post/instrument/AAPL{args}&limit=30&cursor=999999999", 83 | json={"status": "Ok", "payload": expected}, 84 | ) 85 | actual = client.get_posts_by_ticker("AAPL") 86 | assert actual == expected 87 | 88 | 89 | def test_get_posts_by_ticker(httpx_mock: HTTPXMock): 90 | expected = { 91 | "nextCursor": 4757390, 92 | "hasNext": True, 93 | "items": [], 94 | } 95 | httpx_mock.add_response( 96 | method="GET", 97 | headers=headers, 98 | url=f"{UserClient.BASE_URL}profile/08efcec6/post{args}&limit=30&cursor=999999999", 99 | json={"status": "Ok", "payload": expected}, 100 | ) 101 | actual = client.get_posts_by_user_id("08efcec6") 102 | assert actual == expected 103 | 104 | 105 | @mock.patch("tpulse.sync_client.UserClient", autospec=True) 106 | @mock.patch("tpulse.sync_client.PostClient", autospec=True) 107 | def test_context_manager(mock_user, mock_post): 108 | with PulseClient() as cli: 109 | cli.test() 110 | mock_user.return_value.close.assert_called_once() 111 | mock_post.return_value.close.assert_called_once() 112 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [gh-actions] 2 | python = 3 | 3.7: py37 4 | 3.8: py38 5 | 3.9: py39 6 | 7 | [tox] 8 | isolated_build = True 9 | envlist = python3.7,py38,py39 10 | 11 | [testenv] 12 | deps = 13 | black 14 | coverage 15 | flake8 16 | mccabe 17 | pylint 18 | pytest 19 | httpx 20 | fake_useragent 21 | pytest_httpx 22 | commands = 23 | black tpulse 24 | flake8 tpulse 25 | pylint tpulse 26 | coverage erase 27 | coverage run --include=tpulse/* -m pytest -ra 28 | coverage report -m 29 | coverage xml -------------------------------------------------------------------------------- /tpulse/__init__.py: -------------------------------------------------------------------------------- 1 | """version""" 2 | from tpulse.sync_client import PulseClient as TinkoffPulse # noqa 3 | 4 | __version__ = "0.1.9" 5 | -------------------------------------------------------------------------------- /tpulse/settings.py: -------------------------------------------------------------------------------- 1 | """constants""" 2 | TIMEOUT_SEC = 3 3 | 4 | USER_AGENT = ( 5 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" 6 | " (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" 7 | ) 8 | -------------------------------------------------------------------------------- /tpulse/sync_client.py: -------------------------------------------------------------------------------- 1 | """Sync Client for TCS PULSE api""" 2 | from typing import Dict, Optional 3 | 4 | import httpx 5 | from fake_useragent import UserAgent 6 | from fake_useragent.errors import FakeUserAgentError 7 | 8 | from tpulse import settings 9 | 10 | try: 11 | ua = UserAgent().random 12 | except (IndexError, FakeUserAgentError): 13 | ua = settings.USER_AGENT 14 | 15 | 16 | class ClientBase: 17 | """Base class for API client""" 18 | 19 | def __init__(self, base_url: str): 20 | headers = { 21 | "Content-type": "application/json", 22 | "Accept": "application/json", 23 | "User-agent": ua, 24 | } 25 | data = {"appName": "invest", "origin": "web", "platform": "web"} 26 | self._client = httpx.Client(base_url=base_url, headers=headers, params=data) 27 | 28 | def __enter__(self) -> "ClientBase": 29 | return self 30 | 31 | def __exit__(self, exc_type, exc_value, traceback): 32 | self.close() 33 | 34 | def close(self): 35 | """Close network connections""" 36 | self._client.close() 37 | 38 | def _get(self, url, data, timeout=settings.TIMEOUT_SEC): 39 | """GET request to Dadata API""" 40 | response = self._client.get(url, params=data, timeout=timeout) 41 | response.raise_for_status() 42 | return response.json() 43 | 44 | 45 | class UserClient(ClientBase): 46 | """User class for tpulse api""" 47 | 48 | BASE_URL = "https://www.tinkoff.ru/api/invest-gw/social/v1/" 49 | 50 | def __init__(self): 51 | super().__init__(base_url=self.BASE_URL) 52 | 53 | def user_info(self, name: str) -> Optional[Dict]: 54 | """get user info by username""" 55 | url = "profile/nickname/%s" % name 56 | response = self._get(url, data=None) 57 | return response["payload"] if response["status"] == "Ok" else None 58 | 59 | def user_posts(self, user_id: str, cursor: int, **kwargs) -> Optional[Dict]: 60 | """get user posts by user id""" 61 | url = "profile/%s/post" % user_id 62 | data = {"limit": 30, "cursor": cursor} 63 | data.update(kwargs) 64 | response = self._get(url, data) 65 | return response["payload"] if response["status"] == "Ok" else None 66 | 67 | 68 | class PostClient(ClientBase): 69 | """Ticker class for tpulse api""" 70 | 71 | BASE_URL = "https://www.tinkoff.ru/api/invest-gw/social/v1/" 72 | 73 | def __init__(self): 74 | super().__init__(base_url=self.BASE_URL) 75 | 76 | def posts(self, ticker: str, cursor: int, **kwargs) -> Optional[Dict]: 77 | """get post info by ticker""" 78 | url = "post/instrument/%s" % ticker 79 | data = {"limit": 30, "cursor": cursor} 80 | data.update(kwargs) 81 | response = self._get(url, data) 82 | return response["payload"] if response["status"] == "Ok" else None 83 | 84 | 85 | class PulseClient: 86 | """Sync client for tpulse api""" 87 | 88 | def __init__(self): 89 | self._user = UserClient() 90 | self._post = PostClient() 91 | 92 | def test(self): 93 | """test func""" 94 | pass 95 | 96 | def get_user_info(self, name: str) -> Optional[Dict]: 97 | """Get user info""" 98 | return self._user.user_info(name) 99 | 100 | def get_posts_by_user_id( 101 | self, user_id: str, cursor: int = 999999999, **kwargs 102 | ) -> Optional[Dict]: 103 | """Collect last 30 posts for user""" 104 | return self._user.user_posts(user_id, cursor, **kwargs) 105 | 106 | def get_posts_by_ticker( 107 | self, ticker: str, cursor: int = 999999999, **kwargs 108 | ) -> Optional[Dict]: 109 | """Collect last 30 posts for ticker""" 110 | return self._post.posts(ticker, cursor, **kwargs) 111 | 112 | def __enter__(self) -> "PulseClient": 113 | return self 114 | 115 | def __exit__(self, exc_type, exc_value, traceback): 116 | self.close() 117 | 118 | def close(self): 119 | """Close network connections""" 120 | self._user.close() 121 | self._post.close() 122 | --------------------------------------------------------------------------------