├── .github └── workflows │ ├── lichess_sdk_pipeline.yml │ ├── pythonpublish_on_test.yml │ └── pythonpublish_to_pypi.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION.txt ├── build.sh ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── lichess_client ├── __init__.py ├── abstract_endpoints │ ├── __init__.py │ ├── abstract_account.py │ ├── abstract_boards.py │ ├── abstract_bots.py │ ├── abstract_broadcast.py │ ├── abstract_challenges.py │ ├── abstract_chess_bot.py │ ├── abstract_games.py │ ├── abstract_messaging.py │ ├── abstract_relations.py │ ├── abstract_simulations.py │ ├── abstract_studies.py │ ├── abstract_teams.py │ ├── abstract_tournaments.py │ └── abstract_users.py ├── clients │ ├── __init__.py │ ├── abstract_client.py │ ├── base_client.py │ └── client.py ├── endpoints │ ├── __init__.py │ ├── account.py │ ├── boards.py │ ├── bots.py │ ├── broadcast.py │ ├── challenges.py │ ├── chess_bot.py │ ├── games.py │ ├── messaging.py │ ├── relations.py │ ├── simulations.py │ ├── studies.py │ ├── teams.py │ ├── tournaments.py │ └── users.py ├── helpers │ ├── __init__.py │ └── response_helpers.py └── utils │ ├── __init__.py │ ├── client_errors.py │ ├── enums.py │ └── hrefs.py ├── requirements.txt ├── sample_notebooks ├── How to use an Asynchronous Lichess Python Client.ipynb └── imgs │ └── lichess_token_creation.png ├── setup.py └── tests ├── __init__.py ├── unit ├── __init__.py ├── test_account_endpoint.py ├── test_base_client.py ├── test_boards_endpoint.py ├── test_bots_endpoint.py ├── test_broadcast_endpoint.py ├── test_challenges_endpoint.py ├── test_client.py ├── test_games_endpoint.py ├── test_messages_endpoint.py ├── test_relations_endpoint.py ├── test_response_helpers.py ├── test_simulations_endpoint.py ├── test_studies_endpoint.py ├── test_teams_endpoint.py ├── test_tournaments_endpoint.py └── test_users_endpoint.py └── utils ├── __init__.py └── utils.py /.github/workflows/lichess_sdk_pipeline.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Lichess Python SDK 5 | env: 6 | amasend: ${{ secrets.lichess_amasend_account }} 7 | connector_123: ${{ secrets.connector_123 }} 8 | 9 | on: 10 | push: 11 | branches: [ master, dev ] 12 | pull_request: 13 | branches: [ master, dev ] 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python 3.6 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: 3.6 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | - name: Lint with flake8 31 | run: | 32 | pip install flake8 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pip install pytest 40 | pytest 41 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish_on_test.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package on Test Pypi 5 | 6 | on: 7 | push: 8 | branches: [dev] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.6' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build package 26 | run: | 27 | sh build.sh 28 | - name: Publish distribution 📦 to Test PyPI 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | password: ${{ secrets.test_pypi_password }} 32 | repository_url: https://test.pypi.org/legacy/ 33 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish_to_pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package on Pypi 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.6' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build package 26 | run: | 27 | sh build.sh 28 | - name: Publish distribution 📦 to PyPI 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | password: ${{ secrets.pypi_password }} 32 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # credentials 132 | .tests/credentials.ini 133 | 134 | # other 135 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Lichess Python SDK](https://github.com/amasend/lichess_python_SDK/workflows/Lichess%20Python%20SDK/badge.svg?branch=master) 2 | 3 | This package is meant to be an unofficial Python API Client for lichess.org. 4 | For information about the API please refer to https://lichess.org/api 5 | Every API endpoint uses async Python methods (asyncio). 6 | 7 | # Installation 8 | Pypi: 9 | `pip install async-lichess-sdk` 10 | 11 | Test Pypi: 12 | `pip install -i https://test.pypi.org/simple/ async-lichess-sdk` 13 | 14 | # Documentation 15 | [Auto generated documentation](https://amasend.github.io/lichess_python_SDK/html/index.html) 16 | 17 | # Sample Notebook 18 | [Notebook with all methods](https://github.com/amasend/lichess_python_SDK/blob/master/sample_notebooks/How%20to%20use%20an%20Asynchronous%20Lichess%20Python%20Client.ipynb) 19 | 20 | # Dependencies 21 | To use this package you need to install all of the dependencies located under `requirements.txt`. 22 | ```bash 23 | pip install requirements.txt 24 | ``` 25 | Supported python versions: `python >= 3.6` 26 | # How to build 27 | Building script is located under `build.sh`. 28 | Steps: 29 | ```bash 30 | sh build.sh 31 | pip install -U . 32 | ``` 33 | 34 | # Implemented Lichess Endpoints 35 | * account 36 | * boards 37 | * bots 38 | * broadcast 39 | * challenges 40 | * games 41 | * messages 42 | * relations 43 | * simulations 44 | * studies 45 | * teams 46 | * tournaments 47 | * users 48 | 49 | # Sample usage 50 | ### Client initialization and usage 51 | ```python 52 | import asyncio 53 | from lichess_client import APIClient 54 | 55 | 56 | async def main(): 57 | client = APIClient(token="your_lichess_account_token") 58 | response = await client.account.get_my_profile() 59 | print(response) 60 | 61 | loop = asyncio.get_event_loop() 62 | loop.run_until_complete(main()) 63 | 64 | ..... 65 | 66 | {'metadata': 67 | {'method': , 68 | 'url': 'https://lichess.org/api/account/kid', 69 | 'content_type': 'application/json', 70 | 'timestamp': b'Fri, 13 Mar 2020 19:05:27 GMT'}, 71 | 'entity': 72 | {'code': 200, 73 | 'reason': 'OK', 74 | 'status': , 75 | 'content': {'kid': False} 76 | } 77 | } 78 | ``` 79 | 80 | ### Access to the response properties 81 | ```python 82 | 83 | print(response.metadata.timestamp) 84 | b'Fri, 13 Mar 2020 19:11:32 GMT' 85 | 86 | print(response.entity.content) 87 | {'kid': False} 88 | ``` 89 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.1.0.6 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -r ./dist 4 | 5 | # read a version number and increment it 6 | new_version=$(cat VERSION.txt | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') 7 | echo "New version number is: $new_version" 8 | 9 | # setting a new version number 10 | echo $new_version > VERSION.txt 11 | 12 | # create a python package 13 | python3 setup.py sdist bdist_wheel 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 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 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'async_lichess_sdk' 21 | copyright = '2020, Amadeusz Masny' 22 | author = 'Amadeusz Masny' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.9' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc' 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'sphinx_rtd_theme' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to async_lichess_sdk's documentation! 2 | ============================================= 3 | This package is meant to be an unofficial Python API Client for lichees.org. 4 | For information about the API please refer to https://lichess.org/api 5 | Every API endpoint uses async Python methods (asyncio). 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | Requirements 12 | ============ 13 | * `aiohttp `_ 14 | * `python-chess `_ 15 | 16 | Installation 17 | ============ 18 | You can just install me from a test PyPi 19 | 20 | .. code-block:: bash 21 | 22 | pip install --index-url https://test.pypi.org/simple/ async_lichess_sdk 23 | 24 | or official PyPi 25 | 26 | .. code-block:: bash 27 | 28 | pip install async_lichess_sdk 29 | 30 | Available Modules / Endpoints 31 | ============================= 32 | 33 | APIClient 34 | +++++++++ 35 | .. automodule:: lichess_client.clients.client 36 | :members: 37 | 38 | Account 39 | +++++++++ 40 | .. automodule:: lichess_client.endpoints.account 41 | :members: 42 | 43 | Boards 44 | +++++++++ 45 | .. automodule:: lichess_client.endpoints.boards 46 | :members: 47 | 48 | Bots 49 | +++++++++ 50 | .. automodule:: lichess_client.endpoints.bots 51 | :members: 52 | 53 | Broadcast 54 | +++++++++ 55 | .. automodule:: lichess_client.endpoints.broadcast 56 | :members: 57 | 58 | Challenges 59 | ++++++++++ 60 | .. automodule:: lichess_client.endpoints.challenges 61 | :members: 62 | 63 | Games 64 | ++++++++++ 65 | .. automodule:: lichess_client.endpoints.games 66 | :members: 67 | 68 | Messaging 69 | ++++++++++ 70 | .. automodule:: lichess_client.endpoints.messaging 71 | :members: 72 | 73 | Relations 74 | ++++++++++ 75 | .. automodule:: lichess_client.endpoints.relations 76 | :members: 77 | 78 | Simulations 79 | +++++++++++ 80 | .. automodule:: lichess_client.endpoints.simulations 81 | :members: 82 | 83 | Studies 84 | ++++++++++ 85 | .. automodule:: lichess_client.endpoints.studies 86 | :members: 87 | 88 | Teams 89 | ++++++++++ 90 | .. automodule:: lichess_client.endpoints.teams 91 | :members: 92 | 93 | Tournaments 94 | ++++++++++ 95 | .. automodule:: lichess_client.endpoints.tournaments 96 | :members: 97 | 98 | Users 99 | ++++++++++ 100 | .. automodule:: lichess_client.endpoints.users 101 | :members: 102 | 103 | Helpers 104 | ======== 105 | .. automodule:: lichess_client.helpers.response_helpers 106 | :members: 107 | 108 | Indices and tables 109 | ================== 110 | 111 | * :ref:`genindex` 112 | * :ref:`modindex` 113 | * :ref:`search` 114 | -------------------------------------------------------------------------------- /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=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /lichess_client/__init__.py: -------------------------------------------------------------------------------- 1 | from lichess_client.clients import APIClient 2 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasend/lichess_python_SDK/1e2a545b65111bfc58bb963c44ad56be9b4d0835/lichess_client/abstract_endpoints/__init__.py -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_account.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractAccount(ABC): 5 | """An abstract class for Account API Endpoint""" 6 | 7 | @abstractmethod 8 | def get_my_profile(self): 9 | """ 10 | Public information about the logged in user. 11 | """ 12 | pass 13 | 14 | @abstractmethod 15 | def get_my_email_address(self): 16 | """ 17 | Read the email address of the logged in user. 18 | """ 19 | pass 20 | 21 | @abstractmethod 22 | def get_my_preferences(self): 23 | """ 24 | Read the preferences of the logged in user. 25 | """ 26 | pass 27 | 28 | @abstractmethod 29 | def get_my_kid_mode_status(self): 30 | """ 31 | Read the kid mode status of the logged in user. 32 | """ 33 | pass 34 | 35 | @abstractmethod 36 | def set_my_kid_mode_status(self, *, turned_on: bool): 37 | """ 38 | Set the kid mode status of the logged in user. 39 | 40 | Parameters 41 | ---------- 42 | turned_on: bool, required 43 | The indicator to turn on or turn off the kid mode. 44 | """ 45 | pass 46 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_boards.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from lichess_client.utils.enums import VariantTypes, ColorType, RoomTypes 6 | 7 | 8 | class AbstractBoards(ABC): 9 | """An abstract class for Bots API Endpoint""" 10 | 11 | @abstractmethod 12 | def stream_incoming_events(self): 13 | """ 14 | Stream the events reaching a lichess user in real time. 15 | """ 16 | pass 17 | 18 | @abstractmethod 19 | def create_a_seek(self, 20 | time: int, 21 | increment: int, 22 | variant: 'VariantTypes', 23 | color: 'ColorType', 24 | rated: bool, 25 | rating_range): 26 | """ 27 | Create a public seek, to start a game with a random player. 28 | """ 29 | pass 30 | 31 | @abstractmethod 32 | def stream_game_state(self, game_id: str): 33 | """ 34 | Stream the state of a game being played with the Board API 35 | """ 36 | pass 37 | 38 | @abstractmethod 39 | def make_move(self, game_id: str, move: str, draw: bool): 40 | """Make a move in a game being played with the Board API.""" 41 | pass 42 | 43 | @abstractmethod 44 | def abort_game(self, game_id: str): 45 | """Abort a game being played with the Board API.""" 46 | pass 47 | 48 | @abstractmethod 49 | def resign_game(self, game_id: str): 50 | """Resign a game being played with the Board API.""" 51 | pass 52 | 53 | @abstractmethod 54 | def write_in_chat(self, game_id: str, room: 'RoomTypes', message: str): 55 | """Post a message to the player or spectator chat, in a game being played with the Board API.""" 56 | pass 57 | 58 | @abstractmethod 59 | def handle_draw(self, game_id: str, accept: bool = True): 60 | """Create/accept/decline draw offers.""" 61 | pass 62 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_bots.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractBots(ABC): 5 | """An abstract class for Bots API Endpoint""" 6 | 7 | @abstractmethod 8 | def stream_incoming_events(self): 9 | """ 10 | Stream the events reaching a lichess user in real time. 11 | """ 12 | pass 13 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_broadcast.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractBroadcast(ABC): 5 | """An abstract class for Broadcast API Endpoint""" 6 | 7 | @abstractmethod 8 | def create(self, 9 | name: str, 10 | description: str, 11 | source_url: str = None, 12 | markdown: str = None, 13 | credit: str = None, 14 | start_time: int = None): 15 | """Create a new broadcast to relay external games.""" 16 | pass 17 | 18 | @abstractmethod 19 | def get(self, broadcast_id: str): 20 | """Get information about a broadcast that you created.""" 21 | pass 22 | 23 | @abstractmethod 24 | def update(self, 25 | broadcast_id: str, 26 | name: str, 27 | description: str, 28 | source_url: str = None, 29 | markdown: str = None, 30 | credit: str = None, 31 | start_time: int = None): 32 | """Update information about a broadcast that you created.""" 33 | pass 34 | 35 | @abstractmethod 36 | def push_pgn(self, broadcast_id: str, games: str): 37 | """Update your broadcast with new PGN.""" 38 | pass 39 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_challenges.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from lichess_client.utils.enums import VariantTypes, ColorType 6 | 7 | 8 | class AbstractChallenges(ABC): 9 | """An abstract class for Challenges API Endpoint""" 10 | 11 | @abstractmethod 12 | def stream_incoming_events(self): 13 | pass 14 | 15 | @abstractmethod 16 | def create(self, 17 | username: str, 18 | time_limit: int, 19 | time_increment: int, 20 | rated: bool, 21 | days: int, 22 | color: 'ColorType', 23 | variant: 'VariantTypes', 24 | position: str): 25 | """Challenge someone to play.""" 26 | pass 27 | 28 | @abstractmethod 29 | def accept(self, challenge_id: str): 30 | """Accept an incoming challenge.""" 31 | pass 32 | 33 | @abstractmethod 34 | def decline(self, challenge_id: str): 35 | """Decline an incoming challenge.""" 36 | pass 37 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_chess_bot.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class AbstractChessBot(ABC): 5 | """An abstract class for Chess Bot API Endpoint""" 6 | pass 7 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_games.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import TYPE_CHECKING, List 3 | 4 | if TYPE_CHECKING: 5 | from lichess_client.utils.enums import VariantTypes, ColorType 6 | 7 | 8 | class AbstractGames(ABC): 9 | """An abstract class for Games API Endpoint""" 10 | 11 | @abstractmethod 12 | def export_one_game(self, game_id: str): 13 | """Download one game. Only finished games can be downloaded.""" 14 | pass 15 | 16 | @abstractmethod 17 | def export_games_of_a_user(self, 18 | username: str, 19 | since: int = None, 20 | until: int = None, 21 | max: int = None, 22 | vs: str = None, 23 | rated: bool = None, 24 | variant: 'VariantTypes' = None, 25 | color: 'ColorType' = None, 26 | analysed: bool = None, 27 | ongoing: bool = False): 28 | """Download all games of any user in PGN format.""" 29 | pass 30 | 31 | @abstractmethod 32 | def export_games_by_ids(self, game_ids: List[str]): 33 | """Download games by IDs.""" 34 | pass 35 | 36 | @abstractmethod 37 | def stream_current_games(self, users: List[str]): 38 | """Stream current games between users.""" 39 | pass 40 | 41 | @abstractmethod 42 | def get_ongoing_games(self, limit: int = 9): 43 | """Get the ongoing games of the current user.""" 44 | pass 45 | 46 | @abstractmethod 47 | def get_current_tv_games(self): 48 | """Get basic info about the best games being played for each speed and variant, 49 | but also computer games and bot games.""" 50 | pass 51 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_messaging.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractMessaging(ABC): 5 | """An abstract class for Messaging API Endpoint""" 6 | 7 | @abstractmethod 8 | def send(self, username: str, text: str): 9 | """Send a private message to another player.""" 10 | pass 11 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_relations.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractRelations(ABC): 5 | """An abstract class for Relations API Endpoint""" 6 | 7 | @abstractmethod 8 | def get_users_followed_by_a_user(self, username: str): 9 | """Fetching all users followed by current user. 10 | This is a streaming endpoint.""" 11 | pass 12 | 13 | @abstractmethod 14 | def get_users_who_follow_a_user(self, username: str): 15 | """Fetching all users who follow this user. 16 | This is a streaming endpoint.""" 17 | pass 18 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_simulations.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractSimulations(ABC): 5 | """An abstract class for Simulations API Endpoint""" 6 | 7 | @abstractmethod 8 | def get_current(self): 9 | """Get recently finished, ongoing, and upcoming simuls.""" 10 | pass 11 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_studies.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractStudies(ABC): 5 | """An abstract class for Studies API Endpoint""" 6 | 7 | @abstractmethod 8 | def export_chapter(self, study_id: str, chapter_id: str): 9 | """Download one study chapter in PGN format.""" 10 | pass 11 | 12 | @abstractmethod 13 | def export_all_chapters(self, study_id: str): 14 | """Download all chapters of a study in PGN format.""" 15 | pass 16 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_teams.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractTeams(ABC): 5 | """An abstract class for Teams API Endpoint""" 6 | 7 | @abstractmethod 8 | def get_members_of_a_team(self): 9 | """Not implemented""" 10 | pass 11 | 12 | @abstractmethod 13 | def join_a_team(self, team_id: str): 14 | """Join a team.""" 15 | pass 16 | 17 | @abstractmethod 18 | def leave_a_team(self, team_id: str): 19 | """Leave a team.""" 20 | pass 21 | 22 | @abstractmethod 23 | def kick_a_user_from_your_team(self, team_id: str, user_id: str): 24 | """Kick a member out of one of your teams.""" 25 | pass 26 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_tournaments.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from lichess_client.utils.enums import VariantTypes 6 | 7 | 8 | class AbstractTournaments(ABC): 9 | """An abstract class for Tournaments API Endpoint""" 10 | 11 | @abstractmethod 12 | def get_current(self): 13 | """Get recently finished, ongoing, and upcoming tournaments.""" 14 | pass 15 | 16 | @abstractmethod 17 | def create(self, 18 | clock_time: int, 19 | clock_increment: int, 20 | minutes: int, 21 | name: str, 22 | wait_minutes: int, 23 | start_date: int, 24 | variant: 'VariantTypes', 25 | rated: bool, 26 | position: str, 27 | berserk: bool, 28 | password: str, 29 | team_id: str, 30 | min_rating: int, 31 | max_rating: int, 32 | number_of_rated_games: int): 33 | """Create a public or private tournament to your taste.""" 34 | pass 35 | 36 | @abstractmethod 37 | def export_games(self, tournament_id: str): 38 | """Download games of a tournament.""" 39 | pass 40 | 41 | @abstractmethod 42 | def get_results(self, tournament_id: str, limit: int = 10): 43 | """Players of a tournament, with their score and performance, sorted by rank (best first).""" 44 | pass 45 | 46 | @abstractmethod 47 | def get_tournaments_created_by_a_user(self, username: str, limit: int = 10): 48 | """Get all tournaments created by a given user.""" 49 | pass 50 | -------------------------------------------------------------------------------- /lichess_client/abstract_endpoints/abstract_users.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from lichess_client.utils.enums import VariantTypes 5 | 6 | 7 | class AbstractUsers(ABC): 8 | """An abstract class for Users API Endpoint""" 9 | 10 | @abstractmethod 11 | def get_real_time_users_status(self, users_ids: List[str]): 12 | """ 13 | Read the online, playing and streaming flags of several users. 14 | 15 | This API is very fast and cheap on lichess side. So you can call it quite often (like once every 5 seconds). 16 | 17 | Use it to track players and know when they're connected on lichess and playing games.""" 18 | pass 19 | 20 | @abstractmethod 21 | def get_all_top_10(self): 22 | """Get the top 10 players for each speed and variant.""" 23 | pass 24 | 25 | @abstractmethod 26 | def get_one_leaderboard(self, variant: 'VariantTypes', limit: int = 10): 27 | """ 28 | Get the leaderboard for a single speed or variant (a.k.a. perfType). 29 | There is no leaderboard for correspondence or puzzles. 30 | """ 31 | pass 32 | 33 | @abstractmethod 34 | def get_user_public_data(self, username: str): 35 | """Read public data of a user.""" 36 | pass 37 | 38 | @abstractmethod 39 | def get_rating_history_of_a_user(self, username: str): 40 | """Read rating history of a user, for all perf types. There is at most one entry per day. 41 | Format of an entry is [year, month, day, rating]. month starts at zero (January).""" 42 | pass 43 | 44 | @abstractmethod 45 | def get_user_activity(self, username: str): 46 | """Read data to generate the activity feed of a user.""" 47 | pass 48 | 49 | @abstractmethod 50 | def get_your_puzzle_activity(self, limit: int = None): 51 | """Download your puzzle activity in ndjson format. 52 | Puzzle activity is sorted by reverse chronological order (most recent first)""" 53 | pass 54 | 55 | @abstractmethod 56 | def get_users_by_id(self, users_ids: List[str]): 57 | """Get several users by their IDs. Users are returned in the order same order as the IDs. 58 | The method is POST so a longer list of IDs can be sent in the request body.""" 59 | pass 60 | 61 | @abstractmethod 62 | def get_members_of_a_team(self, team_id: str): 63 | """Download your puzzle activity in ndjson format. 64 | Puzzle activity is sorted by reverse chronological order (most recent first)""" 65 | pass 66 | 67 | @abstractmethod 68 | def get_live_streamers(self): 69 | """Get basic info about currently streaming users.""" 70 | pass 71 | -------------------------------------------------------------------------------- /lichess_client/clients/__init__.py: -------------------------------------------------------------------------------- 1 | from lichess_client.clients.client import APIClient 2 | -------------------------------------------------------------------------------- /lichess_client/clients/abstract_client.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class AbstractClient(ABC): 5 | """An abstract class for a Client""" 6 | pass 7 | -------------------------------------------------------------------------------- /lichess_client/clients/base_client.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import sys 4 | from typing import Any, AsyncIterable 5 | 6 | import chess.pgn 7 | from aiohttp import ClientSession, ClientTimeout 8 | from lichess_client.helpers import Response, ResponseEntity, ResponseMetadata 9 | from lichess_client.utils.enums import RequestMethods, StatusTypes 10 | from lichess_client.utils.hrefs import ACCOUNT_URL, LICHESS_URL 11 | 12 | if sys.version_info >= (3, 7): 13 | from asyncio import get_running_loop 14 | else: 15 | from asyncio import get_event_loop 16 | 17 | 18 | class BaseClient: 19 | """ 20 | ASYNC BaseClient class for handling secure connections with Lichess API via token usage. 21 | 22 | Parameters 23 | ---------- 24 | token: str, required 25 | String with token provided from Lichess.org account site. 26 | 27 | loop: asyncio event loop, optional 28 | Asyncio event loop for async mode operations 29 | """ 30 | 31 | def __init__(self, token: str, *, loop=None) -> None: 32 | self.loop = loop or (get_running_loop() if sys.version_info >= (3, 7) else get_event_loop()) 33 | self._token = token 34 | self._headers = {'Authorization': f"Bearer {self._token}"} 35 | 36 | async def request(self, method: 'RequestMethods', url: str, **kwargs: Any) -> 'Response': 37 | """ 38 | Request async method. 39 | 40 | Parameters 41 | ---------- 42 | method: RequestMethods, required 43 | One of REST method, please refer to lichess_client.utils.enums.RequestMethods 44 | 45 | url: str, required 46 | URL string for REST API endpoint 47 | 48 | Returns 49 | ------- 50 | aiohttp.client_reqrep.ClientResponse with response details 51 | """ 52 | async with ClientSession(headers=self._headers, loop=self.loop) as session: 53 | async with session.request(method=method.value, url=f"{LICHESS_URL}{url}", **kwargs) as resp: 54 | if resp.content_type == 'application/x-chess-pgn': 55 | body = await resp.text() 56 | body = chess.pgn.read_game(io.StringIO(body)) 57 | 58 | elif resp.content_type == 'text/plain': 59 | body = await resp.text() 60 | else: 61 | body = await resp.read() 62 | 63 | try: 64 | body = json.loads(body) 65 | 66 | except json.decoder.JSONDecodeError: 67 | body = 'error' 68 | 69 | response = Response( 70 | metadata=ResponseMetadata( 71 | method=resp.method, 72 | url=str(resp.url), 73 | content_type=resp.content_type, 74 | timestamp=resp.raw_headers[1][1] 75 | ), 76 | entity=ResponseEntity( 77 | code=resp.status, 78 | reason=resp.reason, 79 | status=StatusTypes.ERROR if 'error' in body else StatusTypes.SUCCESS, 80 | content=body 81 | ) 82 | ) 83 | return response 84 | 85 | async def request_stream(self, method: 'RequestMethods', url: str, **kwargs: Any) -> 'Response': 86 | """ 87 | Request streaming async method. 88 | 89 | Parameters 90 | ---------- 91 | method: RequestMethods, required 92 | One of REST method, please refer to lichess_client.utils.enums.RequestMethods 93 | 94 | url: str, required 95 | URL string for REST API endpoint 96 | 97 | Returns 98 | ------- 99 | aiohttp.client_reqrep.ClientResponse with response details 100 | """ 101 | async with ClientSession(headers=self._headers, loop=self.loop) as session: 102 | async with session.request(method=method.value, url=f"{LICHESS_URL}{url}", **kwargs) as resp: 103 | 104 | if resp.content_type == 'application/x-chess-pgn': 105 | body = f"" 106 | 107 | else: 108 | body = [] 109 | 110 | async for data, _ in resp.content.iter_chunks(): # note: streaming content! 111 | if resp.status == 404: 112 | body = 'error' 113 | break 114 | 115 | data = data.decode('utf-8', errors='strict') 116 | 117 | if resp.content_type == 'application/x-chess-pgn': 118 | body = f"{body}{data}" 119 | 120 | else: 121 | buffer = [entry for entry in data.split('\n')[:-1]] 122 | try: 123 | body.extend([json.loads(entry) for entry in buffer if entry != '']) 124 | 125 | except json.decoder.JSONDecodeError: 126 | pass 127 | 128 | # note: we should return a list of fetched games in PGH format 129 | if resp.content_type == 'application/x-chess-pgn': 130 | body = [chess.pgn.read_game(io.StringIO(game)) for game in body.split('\n\n\n')] 131 | body = body[:-1] 132 | 133 | response = Response( 134 | metadata=ResponseMetadata( 135 | method=resp.method, 136 | url=str(resp.url), 137 | content_type=resp.content_type, 138 | timestamp=resp.raw_headers[1][1] 139 | ), 140 | entity=ResponseEntity( 141 | code=resp.status, 142 | reason=resp.reason, 143 | status=StatusTypes.ERROR if 'error' in body else StatusTypes.SUCCESS, 144 | content=body 145 | ) 146 | ) 147 | return response 148 | 149 | async def request_constant_stream(self, 150 | method: 'RequestMethods', 151 | url: str, 152 | **kwargs: Any) -> AsyncIterable['Response']: 153 | """ 154 | Request constant streaming async method. 155 | 156 | Parameters 157 | ---------- 158 | method: RequestMethods, required 159 | One of REST method, please refer to lichess_client.utils.enums.RequestMethods 160 | 161 | url: str, required 162 | URL string for REST API endpoint 163 | 164 | Returns 165 | ------- 166 | aiohttp.client_reqrep.ClientResponse with response details 167 | """ 168 | timeout_settings = ClientTimeout( 169 | total=None, 170 | ) 171 | async with ClientSession(headers=self._headers, loop=self.loop, timeout=timeout_settings) as session: 172 | async with session.request(method=method.value, url=f"{LICHESS_URL}{url}", **kwargs) as resp: 173 | 174 | async for body, _ in resp.content.iter_chunks(): # note: streaming content! 175 | if resp.status == 404: 176 | body = 'error' 177 | 178 | body = body.decode('utf-8', errors='strict').split('\n')[0] 179 | 180 | if body == '': 181 | continue 182 | 183 | else: 184 | yield Response( 185 | metadata=ResponseMetadata( 186 | method=resp.method, 187 | url=str(resp.url), 188 | content_type=resp.content_type, 189 | timestamp=resp.raw_headers[1][1] 190 | ), 191 | entity=ResponseEntity( 192 | code=resp.status, 193 | reason=resp.reason, 194 | status=StatusTypes.ERROR if 'error' in body else StatusTypes.SUCCESS, 195 | content=body 196 | ) 197 | ) 198 | 199 | async def is_authorized(self) -> bool: 200 | """ 201 | API authorization check. 202 | 203 | Returns 204 | ------- 205 | bool (True if authorized else False) 206 | """ 207 | response = await self.request(method=RequestMethods.GET, url=ACCOUNT_URL) 208 | return True if response.entity.status == StatusTypes.SUCCESS else False 209 | -------------------------------------------------------------------------------- /lichess_client/clients/client.py: -------------------------------------------------------------------------------- 1 | from lichess_client.clients.abstract_client import AbstractClient 2 | from lichess_client.clients.base_client import BaseClient 3 | from lichess_client.endpoints import ( 4 | Account, Broadcast, Challenges, ChessBot, Games, Messaging, Relations, Simulations, Studies, Teams, Tournaments, 5 | Users, Bots, Boards) 6 | 7 | 8 | class APIClient(AbstractClient): 9 | """ 10 | ASYNC APIClient class for handling secure connections with Lichees API via token usage. 11 | 12 | Parameters 13 | ---------- 14 | token: str, required 15 | String with token provided from Lichees.org account site. 16 | 17 | loop: asyncio event loop, optional 18 | Asyncio event loop for async mode operations 19 | """ 20 | 21 | def __init__(self, token: str, loop=None) -> None: 22 | self._client = BaseClient(token=token, loop=loop) 23 | 24 | self.account = Account(client=self._client) 25 | self.broadcast = Broadcast(client=self._client) 26 | self.challenges = Challenges(client=self._client) 27 | self.chess_bot = ChessBot(client=self._client) 28 | self.games = Games(client=self._client) 29 | self.messaging = Messaging(client=self._client) 30 | self.relations = Relations(client=self._client) 31 | self.simulations = Simulations(client=self._client) 32 | self.studies = Studies(client=self._client) 33 | self.teams = Teams(client=self._client) 34 | self.tournaments = Tournaments(client=self._client) 35 | self.users = Users(client=self._client) 36 | self.bots = Bots(client=self._client) 37 | self.boards = Boards(client=self._client) 38 | -------------------------------------------------------------------------------- /lichess_client/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | from lichess_client.endpoints.account import Account 2 | from lichess_client.endpoints.broadcast import Broadcast 3 | from lichess_client.endpoints.challenges import Challenges 4 | from lichess_client.endpoints.chess_bot import ChessBot 5 | from lichess_client.endpoints.games import Games 6 | from lichess_client.endpoints.messaging import Messaging 7 | from lichess_client.endpoints.relations import Relations 8 | from lichess_client.endpoints.simulations import Simulations 9 | from lichess_client.endpoints.studies import Studies 10 | from lichess_client.endpoints.teams import Teams 11 | from lichess_client.endpoints.tournaments import Tournaments 12 | from lichess_client.endpoints.users import Users 13 | from lichess_client.endpoints.bots import Bots 14 | from lichess_client.endpoints.boards import Boards 15 | -------------------------------------------------------------------------------- /lichess_client/endpoints/account.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING 3 | 4 | from lichess_client.abstract_endpoints.abstract_account import AbstractAccount 5 | from lichess_client.utils.enums import RequestMethods 6 | from lichess_client.utils.hrefs import ACCOUNT_URL, ACCOUNT_EMAIL_URL, ACCOUNT_KID_URL, ACCOUNT_PREFERENCES_URL 7 | 8 | if TYPE_CHECKING: 9 | from lichess_client.clients.base_client import BaseClient 10 | from lichess_client.helpers import Response 11 | 12 | 13 | class Account(AbstractAccount): 14 | """Class for Account API Endpoint""" 15 | 16 | def __init__(self, client: 'BaseClient') -> None: 17 | self._client = client 18 | 19 | async def get_my_profile(self) -> 'Response': 20 | """ 21 | Public information about the logged in user. 22 | 23 | Returns 24 | ------- 25 | Response object with response content. 26 | 27 | Example 28 | ------- 29 | >>> from lichess_client import APIClient 30 | >>> client = APIClient(token='...') 31 | >>> response = client.account.get_my_profile() 32 | """ 33 | headers = { 34 | 'Content-Type': 'application/json' 35 | } 36 | response = await self._client.request(method=RequestMethods.GET, url=ACCOUNT_URL, headers=headers) 37 | return response 38 | 39 | async def get_my_email_address(self) -> 'Response': 40 | """ 41 | Read the email address of the logged in user. 42 | 43 | Returns 44 | ------- 45 | Response object with response content. 46 | 47 | Example 48 | ------- 49 | >>> from lichess_client import APIClient 50 | >>> client = APIClient(token='...') 51 | >>> response = client.account.get_my_email_address() 52 | """ 53 | headers = { 54 | 'Content-Type': 'application/json' 55 | } 56 | response = await self._client.request(method=RequestMethods.GET, url=ACCOUNT_EMAIL_URL, headers=headers) 57 | return response 58 | 59 | async def get_my_preferences(self) -> 'Response': 60 | """ 61 | Read the preferences of the logged in user. 62 | 63 | Returns 64 | ------- 65 | Response object with response content. 66 | 67 | Example 68 | ------- 69 | >>> from lichess_client import APIClient 70 | >>> client = APIClient(token='...') 71 | >>> response = client.account.get_my_preferences() 72 | """ 73 | headers = { 74 | 'Content-Type': 'application/json' 75 | } 76 | response = await self._client.request(method=RequestMethods.GET, url=ACCOUNT_PREFERENCES_URL, headers=headers) 77 | return response 78 | 79 | async def get_my_kid_mode_status(self) -> 'Response': 80 | """ 81 | Read the kid mode status of the logged in user. 82 | 83 | Returns 84 | ------- 85 | Response object with response content. 86 | 87 | Example 88 | ------- 89 | >>> from lichess_client import APIClient 90 | >>> client = APIClient(token='...') 91 | >>> response = client.account.get_my_kid_mode_status() 92 | """ 93 | headers = { 94 | 'Content-Type': 'application/json' 95 | } 96 | response = await self._client.request(method=RequestMethods.GET, url=ACCOUNT_KID_URL, headers=headers) 97 | return response 98 | 99 | async def set_my_kid_mode_status(self, *, turned_on: bool) -> 'Response': 100 | """ 101 | Set the kid mode status of the logged in user. 102 | 103 | Parameters 104 | ---------- 105 | turned_on: bool, required 106 | The indicator to turn on or turn off the kid mode. 107 | 108 | Returns 109 | ------- 110 | Response object with response content. 111 | 112 | Example 113 | ------- 114 | >>> from lichess_client import APIClient 115 | >>> client = APIClient(token='...') 116 | >>> response = client.account.set_my_kid_mode_status()() 117 | """ 118 | headers = { 119 | 'Content-Type': 'application/json' 120 | } 121 | parameters = {'v': json.dumps(turned_on)} 122 | response = await self._client.request(method=RequestMethods.POST, url=ACCOUNT_KID_URL, 123 | headers=headers, params=parameters) 124 | return response 125 | -------------------------------------------------------------------------------- /lichess_client/endpoints/boards.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING, List 3 | 4 | from lichess_client.abstract_endpoints.abstract_boards import AbstractBoards 5 | from lichess_client.utils.enums import RequestMethods, VariantTypes, ColorType, RoomTypes 6 | from lichess_client.utils.client_errors import RatingRangeError 7 | from lichess_client.utils.hrefs import (BOARDS_STREAM_INCOMING_EVENTS, BOARDS_CREATE_A_SEEK, BOARDS_STREAM_GAME_STATE, 8 | BOARDS_MAKE_MOVE, BOARDS_ABORT_GAME, BOARDS_RESIGN_GAME, BOARDS_WRITE_IN_CHAT, 9 | BOARDS_HANDLE_DRAW) 10 | 11 | if TYPE_CHECKING: 12 | from lichess_client.clients.base_client import BaseClient 13 | from lichess_client.helpers import Response 14 | 15 | __all__ = [ 16 | "Boards" 17 | ] 18 | 19 | 20 | class Boards(AbstractBoards): 21 | """Class for Boards API Endpoint""" 22 | 23 | def __init__(self, client: 'BaseClient') -> None: 24 | self._client = client 25 | 26 | async def stream_incoming_events(self) -> 'Response': 27 | """ 28 | Stream the events reaching a lichess user in real time. 29 | 30 | Returns 31 | ------- 32 | Response object with response content. 33 | 34 | Example 35 | ------- 36 | >>> from lichess_client import APIClient 37 | >>> client = APIClient(token='...') 38 | >>> async for response in client.boards.stream_incoming_events(): 39 | >>> print(response) 40 | """ 41 | headers = { 42 | 'Content-Type': 'application/json' 43 | } 44 | async for response in self._client.request_constant_stream(method=RequestMethods.GET, 45 | url=BOARDS_STREAM_INCOMING_EVENTS, 46 | headers=headers): 47 | yield response 48 | 49 | async def create_a_seek(self, 50 | time: int, 51 | increment: int, 52 | variant: 'VariantTypes' = VariantTypes.STANDARD, 53 | color: 'ColorType' = ColorType.RANDOM, 54 | rated: bool = False, 55 | rating_range: List[int] = None) -> 'Response': 56 | """ 57 | Create a public seek, to start a game with a random player. 58 | 59 | Parameters 60 | ---------- 61 | rated: bool, optional 62 | Whether the game is rated and impacts players ratings. 63 | 64 | time: int, required 65 | Clock initial time in minutes. 66 | 67 | increment: int, required 68 | Clock increment in seconds. 69 | 70 | variant: VariantTypes, optional 71 | Enum: "standard" "chess960" "crazyhouse" "antichess" "atomic" "horde" "kingOfTheHill" 72 | "racingKings" "threeCheck" 73 | The variant of the game. 74 | 75 | color: ColorType, optional 76 | Enum: "random" "white" "black" 77 | The color to play. Better left empty to automatically get 50% white 78 | 79 | rating_range: List[int, int], optional 80 | The rating range of potential opponents. Better left empty. Example: [1500, 1800] 81 | 82 | Returns 83 | ------- 84 | Response object with response content. 85 | 86 | Example 87 | ------- 88 | >>> from lichess_client import APIClient 89 | >>> client = APIClient(token='...') 90 | >>> response = await client.boards.create_a_seek(time=10, increment=0) 91 | """ 92 | 93 | headers = { 94 | 'Content-Type': 'application/x-www-form-urlencoded' 95 | } 96 | data = { 97 | 'rated': json.dumps(rated), 98 | 'time': time, 99 | 'increment': increment, 100 | 'variant': variant.value, 101 | 'color': color.value, 102 | } 103 | if rating_range is not None and len(rating_range) != 2: 104 | raise RatingRangeError('create_a_seek', reason="rating_range should contain only two numbers") 105 | 106 | elif rating_range is not None: 107 | rating_range = [str(entry) for entry in rating_range] 108 | data['ratingRange'] = '-'.join(rating_range) 109 | 110 | response = await self._client.request_stream(method=RequestMethods.POST, 111 | url=BOARDS_CREATE_A_SEEK, 112 | data=data, 113 | headers=headers) 114 | return response 115 | 116 | async def stream_game_state(self, game_id: str) -> 'Response': 117 | """ 118 | Stream the state of a game being played with the Board API 119 | 120 | Parameters 121 | ---------- 122 | game_id: str, required 123 | ID of the current playing game. 124 | 125 | Returns 126 | ------- 127 | Response object with response content. 128 | 129 | Example 130 | ------- 131 | >>> from lichess_client import APIClient 132 | >>> client = APIClient(token='...') 133 | >>> async for response in client.boards.stream_game_state(game_id='5IrD6Gzz'): 134 | >>> print(response) 135 | """ 136 | headers = { 137 | 'Content-Type': 'application/json' 138 | } 139 | async for response in self._client.request_constant_stream(method=RequestMethods.GET, 140 | url=BOARDS_STREAM_GAME_STATE.format(gameId=game_id), 141 | headers=headers): 142 | yield response 143 | 144 | async def make_move(self, 145 | game_id: str, 146 | move: str, 147 | draw: bool = False) -> 'Response': 148 | """ 149 | Make a move in a game being played with the Board API. 150 | The move can also contain a draw offer/agreement. 151 | 152 | 153 | Parameters 154 | ---------- 155 | game_id: str, required 156 | ID of the current playing game. 157 | 158 | move: str, required 159 | Move in UCI format. 160 | 161 | draw: bool, optional 162 | Offer or accept a draw. 163 | 164 | Returns 165 | ------- 166 | Response object with response content. 167 | 168 | Example 169 | ------- 170 | >>> from lichess_client import APIClient 171 | >>> client = APIClient(token='...') 172 | >>> response = await client.boards.make_move(game_id='5IrD6Gzz', move='e2e4') 173 | """ 174 | 175 | headers = { 176 | 'Content-Type': 'application/json' 177 | } 178 | parameters = { 179 | 'offeringDraw': json.dumps(draw) 180 | } 181 | 182 | response = await self._client.request(method=RequestMethods.POST, 183 | url=BOARDS_MAKE_MOVE.format(gameId=game_id, move=move), 184 | params=parameters, 185 | headers=headers) 186 | return response 187 | 188 | async def abort_game(self, game_id: str) -> 'Response': 189 | """ 190 | Abort a game being played with the Board API. 191 | 192 | Parameters 193 | ---------- 194 | game_id: str, required 195 | ID of the current playing game. 196 | 197 | Returns 198 | ------- 199 | Response object with response content. 200 | 201 | Example 202 | ------- 203 | >>> from lichess_client import APIClient 204 | >>> client = APIClient(token='...') 205 | >>> response = await client.boards.abort_game(game_id='5IrD6Gzz') 206 | """ 207 | 208 | response = await self._client.request(method=RequestMethods.POST, 209 | url=BOARDS_ABORT_GAME.format(gameId=game_id)) 210 | return response 211 | 212 | async def resign_game(self, game_id: str) -> 'Response': 213 | """ 214 | Resign a game being played with the Board API. 215 | 216 | Parameters 217 | ---------- 218 | game_id: str, required 219 | ID of the current playing game. 220 | 221 | Returns 222 | ------- 223 | Response object with response content. 224 | 225 | Example 226 | ------- 227 | >>> from lichess_client import APIClient 228 | >>> client = APIClient(token='...') 229 | >>> response = await client.boards.resign_game(game_id='5IrD6Gzz') 230 | """ 231 | 232 | response = await self._client.request(method=RequestMethods.POST, 233 | url=BOARDS_RESIGN_GAME.format(gameId=game_id)) 234 | return response 235 | 236 | async def write_in_chat(self, game_id: str, message: str, room: 'RoomTypes' = RoomTypes.PLAYER): 237 | """ 238 | Post a message to the player or spectator chat, in a game being played with the Board API. 239 | 240 | Parameters 241 | ---------- 242 | game_id: str, required 243 | ID of the current playing game. 244 | 245 | room: RoomTypes, optional 246 | Room where to post a message [player, spectator]. 247 | 248 | message: str, required 249 | User message to post. 250 | 251 | Returns 252 | ------- 253 | Response object with response content. 254 | 255 | Example 256 | ------- 257 | >>> from lichess_client import APIClient 258 | >>> client = APIClient(token='...') 259 | >>> response_1 = await client.boards.write_in_chat(game_id='5IrD6Gzz', message="Hello!") 260 | >>> response_2 = await client.boards.write_in_chat(game_id='5IrD6Gzz', message="Hi all!", room=RoomTypes.SPECTATOR) 261 | """ 262 | 263 | headers = { 264 | 'Content-Type': 'application/x-www-form-urlencoded' 265 | } 266 | data = { 267 | 'room': room.value if isinstance(room, RoomTypes) else room, 268 | 'text': message 269 | } 270 | 271 | response = await self._client.request(method=RequestMethods.POST, 272 | url=BOARDS_WRITE_IN_CHAT.format(gameId=game_id), 273 | data=data, 274 | headers=headers) 275 | return response 276 | 277 | async def handle_draw(self, game_id: str, accept: bool = True) -> 'Response': 278 | """ 279 | Create/accept/decline draw offers. 280 | 281 | Parameters 282 | ---------- 283 | game_id: str, required 284 | ID of the current playing game. 285 | 286 | accept: bool, optional 287 | True: Offer a draw, or accept the opponent's draw offer. 288 | False: Decline a draw offer from the opponent. 289 | 290 | Returns 291 | ------- 292 | Response object with response content. 293 | 294 | Example 295 | ------- 296 | >>> from lichess_client import APIClient 297 | >>> client = APIClient(token='...') 298 | >>> response = await client.boards.handle_draw(game_id='5IrD6Gzz', accept=True) 299 | """ 300 | 301 | response = await self._client.request(method=RequestMethods.POST, 302 | url=BOARDS_HANDLE_DRAW.format(gameId=game_id, 303 | accept="yes" if accept else "no")) 304 | return response 305 | -------------------------------------------------------------------------------- /lichess_client/endpoints/bots.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_bots import AbstractBots 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import BOTS_STREAM_INCOMING_EVENTS 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Bots(AbstractBots): 13 | """Class for Bots API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def stream_incoming_events(self) -> 'Response': 19 | """ 20 | Stream the events reaching a lichess user in real time. 21 | 22 | Returns 23 | ------- 24 | Response object with response content. 25 | 26 | Example 27 | ------- 28 | >>> from lichess_client import APIClient 29 | >>> client = APIClient(token='...') 30 | >>> response = client.bots.get_my_profile() 31 | """ 32 | headers = { 33 | 'Content-Type': 'application/json' 34 | } 35 | response = await self._client.request(method=RequestMethods.GET, 36 | url=BOTS_STREAM_INCOMING_EVENTS, 37 | headers=headers) 38 | return response 39 | -------------------------------------------------------------------------------- /lichess_client/endpoints/broadcast.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_broadcast import AbstractBroadcast 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import BROADCASTS_CREATE, BROADCASTS_GET, BROADCASTS_PUSH_PGN 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Broadcast(AbstractBroadcast): 13 | """Class for Broadcast API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def create(self, 19 | name: str, 20 | description: str, 21 | source_url: str = None, 22 | markdown: str = None, 23 | credit: str = None, 24 | start_time: int = None) -> 'Response': 25 | """ 26 | Create a new broadcast to relay external games. 27 | 28 | Parameters 29 | ---------- 30 | name: str, required 31 | Name of the broadcast. Length must be between 3 and 80 characters. 32 | 33 | description: str, required 34 | Short description of the broadcast. Length must be between 3 and 400 characters. 35 | 36 | source_url: str, optional 37 | URL that Lichess will poll to get updates about the games. It must be publicly accessible from the Internet. 38 | Example: http://myserver.org/myevent/round-10/games.pgn 39 | If the syncUrl is missing, then the broadcast needs to be fed by pushing PGN to it. 40 | 41 | markdown: str, optional 42 | Optional long description of the broadcast. 43 | Markdown is supported. Length must be less than 20,000 characters. 44 | 45 | credit: str, optional 46 | Optional short text to give credit to the source provider. 47 | 48 | start_time: int, optional 49 | Timestamp of broadcast start. Leave empty to manually start the broadcast. 50 | Example: 1356998400070 51 | 52 | Returns 53 | ------- 54 | Response object with response content. 55 | 56 | Example 57 | ------- 58 | >>> from lichess_client import APIClient 59 | >>> client = APIClient(token='...') 60 | >>> response = await client.broadcast.create(name="my broadcast", description="this is my broadcast...") 61 | """ 62 | 63 | headers = { 64 | 'Content-Type': 'application/x-www-form-urlencoded' 65 | } 66 | 67 | data = { 68 | "name": name, 69 | "description": description 70 | } 71 | 72 | if source_url is not None: 73 | data['syncUrl'] = source_url 74 | 75 | if markdown is not None: 76 | data['markdown'] = markdown 77 | 78 | if credit is not None: 79 | data['credit'] = credit 80 | 81 | if start_time is not None: 82 | data['startsAt'] = start_time 83 | 84 | response = await self._client.request(method=RequestMethods.POST, 85 | url=BROADCASTS_CREATE, 86 | data=data, 87 | headers=headers) 88 | return response 89 | 90 | async def get(self, broadcast_id: str) -> 'Response': 91 | """ 92 | Get information about a broadcast that you created. You will need it if you want to update that broadcast. 93 | 94 | Parameters 95 | ---------- 96 | broadcast_id: str, required 97 | 98 | Returns 99 | ------- 100 | Response object with response content. 101 | 102 | Example 103 | ------- 104 | >>> from lichess_client import APIClient 105 | >>> client = APIClient(token='...') 106 | >>> response = await client.broadcast.get(broadcast_id="wje07860") 107 | """ 108 | 109 | headers = { 110 | 'Content-Type': 'application/json' 111 | } 112 | 113 | response = await self._client.request(method=RequestMethods.GET, 114 | url=BROADCASTS_GET.format(broadcastId=broadcast_id), 115 | headers=headers) 116 | return response 117 | 118 | async def update(self, 119 | broadcast_id: str, 120 | name: str, 121 | description: str, 122 | source_url: str = None, 123 | markdown: str = None, 124 | credit: str = None, 125 | start_time: int = None) -> 'Response': 126 | """ 127 | Update information about a broadcast that you created. 128 | This endpoint accepts the same form data as the web form. 129 | All fields must be populated with data. Missing fields will override the broadcast with empty data. 130 | For instance, if you omit startDate, then any pre-existing start date will be removed. 131 | 132 | Parameters 133 | ---------- 134 | broadcast_id: str, required 135 | 136 | name: str, required 137 | Name of the broadcast. Length must be between 3 and 80 characters. 138 | 139 | description: str, required 140 | Short description of the broadcast. Length must be between 3 and 400 characters. 141 | 142 | source_url: str, optional 143 | URL that Lichess will poll to get updates about the games. It must be publicly accessible from the Internet. 144 | Example: http://myserver.org/myevent/round-10/games.pgn 145 | If the syncUrl is missing, then the broadcast needs to be fed by pushing PGN to it. 146 | 147 | markdown: str, optional 148 | Optional long description of the broadcast. 149 | Markdown is supported. Length must be less than 20,000 characters. 150 | 151 | credit: str, optional 152 | Optional short text to give credit to the source provider. 153 | 154 | start_time: int, optional 155 | Timestamp of broadcast start. Leave empty to manually start the broadcast. 156 | Example: 1356998400070 157 | 158 | Returns 159 | ------- 160 | Response object with response content. 161 | 162 | Example 163 | ------- 164 | >>> from lichess_client import APIClient 165 | >>> client = APIClient(token='...') 166 | >>> response = await client.broadcast.update(broadcast_id="wje07860", name="my broadcast", description="this is my updated broadcast...") 167 | """ 168 | 169 | headers = { 170 | 'Content-Type': 'application/x-www-form-urlencoded' 171 | } 172 | 173 | data = { 174 | "name": name, 175 | "description": description 176 | } 177 | 178 | if source_url is not None: 179 | data['syncUrl'] = source_url 180 | 181 | if markdown is not None: 182 | data['markdown'] = markdown 183 | 184 | if credit is not None: 185 | data['credit'] = credit 186 | 187 | if start_time is not None: 188 | data['startsAt'] = start_time 189 | 190 | response = await self._client.request(method=RequestMethods.POST, 191 | url=BROADCASTS_GET.format(broadcastId=broadcast_id), 192 | data=data, 193 | headers=headers) 194 | return response 195 | 196 | async def push_pgn(self, broadcast_id: str, games: str) -> 'Response': 197 | """ 198 | Update your broadcast with new PGN. Only for broadcast without a source URL. 199 | 200 | Parameters 201 | ---------- 202 | broadcast_id: str, required 203 | 204 | games: str, required 205 | The PGN. It can contain up to 64 games, separated by a double new line. 206 | 207 | Returns 208 | ------- 209 | Response object with response content. 210 | 211 | Example 212 | ------- 213 | >>> from lichess_client import APIClient 214 | >>> client = APIClient(token='...') 215 | >>> response = await client.broadcast.push_pgn(broadcast_id="wje07860", games="...") 216 | """ 217 | 218 | response = await self._client.request(method=RequestMethods.POST, 219 | url=BROADCASTS_PUSH_PGN.format(broadcastId=broadcast_id), 220 | data=games) 221 | return response 222 | -------------------------------------------------------------------------------- /lichess_client/endpoints/challenges.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING 3 | 4 | from lichess_client.abstract_endpoints.abstract_challenges import AbstractChallenges 5 | from lichess_client.utils.enums import ColorType, VariantTypes, RequestMethods 6 | from lichess_client.utils.hrefs import CHALLENGES_CREATE, CHALLENGES_ACCEPT, CHALLENGES_DECLINE 7 | 8 | if TYPE_CHECKING: 9 | from lichess_client.clients.base_client import BaseClient 10 | from lichess_client.helpers import Response 11 | 12 | __all__ = [ 13 | "Challenges" 14 | ] 15 | 16 | 17 | class Challenges(AbstractChallenges): 18 | """Class for Challenges API Endpoint""" 19 | 20 | def __init__(self, client: 'BaseClient') -> None: 21 | self._client = client 22 | 23 | async def stream_incoming_events(self): 24 | """ 25 | Stream the events reaching a lichess user in real time. 26 | This method is implemented in "boards" endpoint. 27 | 28 | See also 29 | -------- 30 | lichess_client.APIClient.boards.stream_incoming_events() 31 | """ 32 | raise NotImplementedError("This method is implemented in \"boards\" endpoint." 33 | "Please see: lichess_client.APIClient.boards.stream_incoming_events()") 34 | 35 | async def create(self, 36 | username: str, 37 | time_limit: int = None, 38 | time_increment: int = None, 39 | rated: bool = False, 40 | days: int = None, 41 | color: 'ColorType' = ColorType.RANDOM, 42 | variant: 'VariantTypes' = VariantTypes.STANDARD, 43 | position: str = None) -> 'Response': 44 | """ 45 | Challenge someone to play. The targeted player can choose to accept or decline. 46 | If the challenge is accepted, you will be notified on the event stream that a new game has started. 47 | The game ID will be the same as the challenge ID. 48 | 49 | Parameters 50 | ---------- 51 | username: str, required 52 | 53 | time_limit: int, optional 54 | [1..10800] Clock initial time in seconds. If empty, a correspondence game is created. 55 | 56 | time_increment: int, optional 57 | [0..60] Clock increment in seconds. If empty, a correspondence game is created. 58 | 59 | rated: bool: optional 60 | Game is rated and impacts players ratings 61 | 62 | days: int, optional 63 | [1..15] Days per move, for correspondence games. Time settings must be omitted. 64 | 65 | color: ColorType, optional 66 | Enum: "random" "white" "black" 67 | Which color you get to play, default: "random". 68 | 69 | variant: VariantTypes, optional 70 | Default: "standard" 71 | Enum: "standard" "chess960" "crazyhouse" "antichess" "atomic" "horde" "kingOfTheHill" 72 | "racingKings" "threeCheck" 73 | The variant of the game 74 | 75 | position: str, optional 76 | Default: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 77 | Custom initial position (in FEN). Variant must be standard, and the game cannot be rated. 78 | 79 | Returns 80 | ------- 81 | Response object with response content. 82 | 83 | Example 84 | ------- 85 | >>> from lichess_client import APIClient 86 | >>> client = APIClient(token='...') 87 | >>> response = await client.challenges.create(username="amasend") 88 | """ 89 | 90 | headers = { 91 | 'Content-Type': 'application/x-www-form-urlencoded' 92 | } 93 | 94 | data = { 95 | 'rated': json.dumps(rated), 96 | 'color': color.value if isinstance(color, ColorType) else color, 97 | 'variant': variant.value if isinstance(variant, VariantTypes) else variant 98 | } 99 | if time_limit is not None: 100 | data['clock.limit'] = time_limit 101 | 102 | if time_increment is not None: 103 | data['clock.increment'] = time_increment 104 | 105 | if days is not None and time_limit is None and time_increment is None: 106 | data['days'] = days 107 | 108 | if position is not None: 109 | data['position'] = position 110 | 111 | response = await self._client.request(method=RequestMethods.POST, 112 | url=CHALLENGES_CREATE.format(username=username), 113 | data=data, 114 | headers=headers) 115 | return response 116 | 117 | async def accept(self, challenge_id: str) -> 'Response': 118 | """ 119 | Accept an incoming challenge. 120 | You should receive a gameStart event on the incoming events stream. 121 | 122 | Parameters 123 | ---------- 124 | challenge_id: str, required 125 | ID of the challenge to accept, further game_id will be the same as this challenge_id. 126 | 127 | Returns 128 | ------- 129 | Response object with response content. 130 | 131 | Example 132 | ------- 133 | >>> from lichess_client import APIClient 134 | >>> client = APIClient(token='...') 135 | >>> response = await client.challenges.accept(challenge_id="5IrD6Gzz") 136 | """ 137 | 138 | headers = { 139 | 'Content-Type': 'application/json' 140 | } 141 | 142 | response = await self._client.request(method=RequestMethods.POST, 143 | url=CHALLENGES_ACCEPT.format(challengeId=challenge_id), 144 | headers=headers) 145 | return response 146 | 147 | async def decline(self, challenge_id: str) -> 'Response': 148 | """ 149 | Decline an incoming challenge. 150 | 151 | Parameters 152 | ---------- 153 | challenge_id: str, required 154 | ID of the challenge to decline. 155 | 156 | Returns 157 | ------- 158 | Response object with response content. 159 | 160 | Example 161 | ------- 162 | >>> from lichess_client import APIClient 163 | >>> client = APIClient(token='...') 164 | >>> response = await client.challenges.decline(challenge_id="5IrD6Gzz") 165 | """ 166 | 167 | headers = { 168 | 'Content-Type': 'application/json' 169 | } 170 | 171 | response = await self._client.request(method=RequestMethods.POST, 172 | url=CHALLENGES_DECLINE.format(challengeId=challenge_id), 173 | headers=headers) 174 | return response 175 | -------------------------------------------------------------------------------- /lichess_client/endpoints/chess_bot.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_chess_bot import AbstractChessBot 4 | 5 | if TYPE_CHECKING: 6 | from lichess_client.clients.base_client import BaseClient 7 | 8 | 9 | class ChessBot(AbstractChessBot): 10 | """Class for Chess Bot API Endpoint""" 11 | 12 | def __init__(self, client: 'BaseClient') -> None: 13 | self._client = client 14 | -------------------------------------------------------------------------------- /lichess_client/endpoints/games.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING, Union, List 3 | 4 | from lichess_client.utils.enums import RequestMethods, VariantTypes, ColorType 5 | from lichess_client.abstract_endpoints.abstract_games import AbstractGames 6 | from lichess_client.helpers import Response 7 | from lichess_client.utils.hrefs import (GAMES_EXPORT_ONE_URL, GAMES_EXPORT_USER_URL, GAMES_EXPORT_IDS_URL, 8 | GAMES_STREAM_CURRENT_URL, GAMES_ONGOING_URL, GAMES_CURRENT_TV_URL) 9 | from lichess_client.utils.client_errors import ToManyIDs, LimitError 10 | 11 | if TYPE_CHECKING: 12 | from lichess_client.clients.base_client import BaseClient 13 | 14 | 15 | class Games(AbstractGames): 16 | """Class for Games API Endpoint""" 17 | 18 | def __init__(self, client: 'BaseClient') -> None: 19 | self._client = client 20 | 21 | async def export_one_game(self, game_id: str) -> 'Response': 22 | """ 23 | Download one game. Only finished games can be downloaded. 24 | 25 | Parameters 26 | ---------- 27 | game_id: str, required 28 | ID of the game. 29 | 30 | Returns 31 | ------- 32 | Response object with response content. 33 | 34 | Example 35 | ------- 36 | >>> from lichess_client import APIClient 37 | >>> client = APIClient(token='...') 38 | >>> response = client.users.export_one_game(game_id='q7zvsdUF') 39 | # TODO: add more examples 40 | """ 41 | headers = { 42 | 'Content-Type': 'application/json' 43 | } 44 | parameters = { 45 | 'moves': 'true', 46 | 'pgnInJson': 'false', 47 | 'tags': 'true', 48 | 'clocks': 'true', 49 | 'evals': 'true', 50 | 'opening': 'true', 51 | 'literate': 'true' 52 | } 53 | response = await self._client.request(method=RequestMethods.GET, 54 | url=GAMES_EXPORT_ONE_URL.format(gameId=game_id), 55 | headers=headers, 56 | params=parameters) 57 | return response 58 | 59 | async def export_games_of_a_user(self, 60 | username: str, 61 | since: int = None, 62 | until: int = None, 63 | limit: int = None, 64 | vs: str = None, 65 | rated: bool = None, 66 | variant: Union['VariantTypes', List['VariantTypes']] = None, 67 | color: 'ColorType' = None, 68 | analysed: bool = None, 69 | ongoing: bool = False) -> 'Response': 70 | """ 71 | Download all games of any user in PGN format. 72 | Games are sorted by reverse chronological order (most recent first) 73 | We recommend streaming the response, for it can be very long. https://lichess.org/@/german11 for instance has more than 320,000 games. 74 | The game stream is throttled, depending on who is making the request: 75 | Anonymous request: 15 games per second 76 | OAuth2 authenticated request: 25 games per second 77 | Authenticated, downloading your own games: 50 games per second 78 | 79 | Parameters 80 | ---------- 81 | username: str, required 82 | Name of the user. 83 | 84 | since: int, optional 85 | Default: "Account creation date" 86 | Download games played since this timestamp. 87 | 88 | until: int, optional 89 | Default: "Now" 90 | Download games played until this timestamp. 91 | 92 | limit: int, optional 93 | Default: None 94 | How many games to download. Leave empty to download all games. 95 | 96 | vs: str, optional 97 | [Filter] Only games played against this opponent 98 | 99 | rated: bool, optional 100 | Default: None 101 | [Filter] Only rated (true) or casual (false) games 102 | 103 | variant: Union[VariantTypes, List[VariantTypes]], optional 104 | Default: None 105 | [Filter] Only games in these speeds or variants. 106 | Multiple variants can be specified in a list. 107 | 108 | color: ColorType, optional 109 | Default: None 110 | [Filter] Only games played as this color. 111 | 112 | analysed: bool, optional 113 | [Filter] Only games with or without a computer analysis available. 114 | 115 | ongoing: bool, optional 116 | Default: false 117 | [Filter] Also include ongoing games 118 | 119 | Returns 120 | ------- 121 | Response object with response content. 122 | 123 | Example 124 | ------- 125 | >>> from lichess_client import APIClient 126 | >>> client = APIClient(token='...') 127 | >>> response = client.users.export_games_of_a_user(username='amasend') 128 | # TODO: add more examples 129 | """ 130 | if isinstance(variant, list): 131 | variant = ','.join([entry.value for entry in variant]) 132 | elif isinstance(variant, VariantTypes): 133 | variant = variant.value 134 | 135 | headers = { 136 | 'Content-Type': 'application/json' 137 | } 138 | parameters = { 139 | 'since': 'Account creation date' if since is None else since, 140 | 'until': 'Now' if until is None else until, 141 | 'max': 'null' if limit is None else limit, 142 | 'rated': 'null' if rated is None else rated, 143 | 'perfType': 'null' if variant is None else variant, 144 | 'color': 'null' if color is None else color.value, 145 | 'analysed': 'null' if analysed is None else analysed, 146 | 'ongoing': json.dumps(ongoing), 147 | 'moves': 'true', 148 | 'pgnInJson': 'false', 149 | 'tags': 'true', 150 | 'clocks': 'true', 151 | 'evals': 'true', 152 | 'opening': 'true' 153 | } 154 | 155 | if vs is not None: 156 | parameters['vs'] = vs 157 | 158 | response = await self._client.request_stream(method=RequestMethods.GET, 159 | url=GAMES_EXPORT_USER_URL.format(username=username), 160 | headers=headers, 161 | params=parameters) 162 | return response 163 | 164 | async def export_games_by_ids(self, game_ids: List[str]) -> 'Response': 165 | """ 166 | Download games by IDs. 167 | 168 | Games are sorted by reverse chronological order (most recent first) 169 | 170 | The method is POST so a longer list of IDs can be sent in the request body. At most 300 IDs can be submitted. 171 | 172 | 173 | Parameters 174 | ---------- 175 | game_ids: List[str], required 176 | IDs of the games. 177 | 178 | Returns 179 | ------- 180 | Response object with response content. 181 | 182 | Example 183 | ------- 184 | >>> from lichess_client import APIClient 185 | >>> client = APIClient(token='...') 186 | >>> response = client.users.export_games_by_ids(game_ids=['q7zvsdUF', 'ILwozzRZ']) 187 | """ 188 | if len(game_ids) > 300: 189 | raise ToManyIDs('export_games_by_ids', 190 | reason="Cannot fetch more than 300 games at once. Please specify less than 300 game IDs.") 191 | 192 | headers = { 193 | 'Content-Type': 'text/plain' 194 | } 195 | parameters = { 196 | 'moves': 'true', 197 | 'pgnInJson': 'false', 198 | 'tags': 'true', 199 | 'clocks': 'true', 200 | 'evals': 'true', 201 | 'opening': 'true', 202 | } 203 | response = await self._client.request_stream(method=RequestMethods.POST, 204 | url=GAMES_EXPORT_IDS_URL, 205 | headers=headers, 206 | params=parameters, 207 | data=','.join(game_ids)) 208 | return response 209 | 210 | async def stream_current_games(self, users: List[str]) -> 'Response': 211 | """ 212 | Stream the games played between a list of users, in real time. 213 | Only games where both players are part of the list are included. 214 | Maximum number of users: 300. 215 | The method is POST so a longer list of IDs can be sent in the request body. 216 | 217 | Parameters 218 | ---------- 219 | users: List[str], required 220 | User names. 221 | 222 | Returns 223 | ------- 224 | Response object with response content. 225 | 226 | Example 227 | ------- 228 | >>> from lichess_client import APIClient 229 | >>> client = APIClient(token='...') 230 | >>> response = client.users.stream_current_games(users=['amasend', 'lovlas', 'chess-network']) 231 | """ 232 | raise NotImplementedError("This method is not implemented yet.") 233 | if len(users) > 300: 234 | raise ToManyIDs('stream_current_games', 235 | reason="Cannot fetch more than 300 games at once. Please specify less than 300 users.") 236 | 237 | headers = { 238 | 'Content-Type': 'text/plain' 239 | } 240 | response = await self._client.request_stream(method=RequestMethods.POST, 241 | url=GAMES_STREAM_CURRENT_URL, 242 | headers=headers, 243 | data=','.join(users)) 244 | return response 245 | 246 | async def get_ongoing_games(self, limit: int = 9) -> 'Response': 247 | """ 248 | Get the ongoing games of the current user. 249 | Real-time and correspondence games are included. The most urgent games are listed first. 250 | 251 | Parameters 252 | ---------- 253 | limit: int, optional 254 | Number of games to fetch, default is 9. 255 | 256 | Returns 257 | ------- 258 | Response object with response content. 259 | 260 | Example 261 | ------- 262 | >>> from lichess_client import APIClient 263 | >>> client = APIClient(token='...') 264 | >>> response_1 = client.users.get_ongoing_games(limit=10) 265 | >>> response_2 = client.users.get_ongoing_games() 266 | """ 267 | if limit > 50: 268 | raise LimitError("get_ongoing_games", reason="Max number of games is 50.") 269 | elif limit < 1: 270 | raise LimitError("get_ongoing_games", reason="Min number of games is 1.") 271 | 272 | headers = { 273 | 'Content-Type': 'application/json' 274 | } 275 | parameters = { 276 | 'nb': limit, 277 | } 278 | response = await self._client.request(method=RequestMethods.GET, 279 | url=GAMES_ONGOING_URL, 280 | headers=headers, 281 | params=parameters) 282 | return response 283 | 284 | async def get_current_tv_games(self) -> 'Response': 285 | """ 286 | Get basic info about the best games being played for each speed and variant, 287 | but also computer games and bot games. 288 | 289 | Returns 290 | ------- 291 | Response object with response content. 292 | 293 | Example 294 | ------- 295 | >>> from lichess_client import APIClient 296 | >>> client = APIClient(token='...') 297 | >>> response = client.users.get_current_tv_games() 298 | """ 299 | headers = { 300 | 'Content-Type': 'application/json' 301 | } 302 | response = await self._client.request(method=RequestMethods.GET, 303 | url=GAMES_CURRENT_TV_URL, 304 | headers=headers) 305 | return response 306 | -------------------------------------------------------------------------------- /lichess_client/endpoints/messaging.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_messaging import AbstractMessaging 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import MESSAGES_SEND 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Messaging(AbstractMessaging): 13 | """Class for Messaging API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def send(self, username: str, text: str) -> 'Response': 19 | """ 20 | Send a private message to another player. 21 | 22 | Parameters 23 | ---------- 24 | username: str, required 25 | 26 | text: str, required 27 | 28 | Returns 29 | ------- 30 | Response object with response content. 31 | 32 | Example 33 | ------- 34 | >>> from lichess_client import APIClient 35 | >>> client = APIClient(token='...') 36 | >>> response = await client.messaging.send() 37 | """ 38 | 39 | headers = { 40 | 'Content-Type': "application/x-www-form-urlencoded" 41 | } 42 | 43 | data = { 44 | "text": text 45 | } 46 | 47 | response = await self._client.request(method=RequestMethods.POST, 48 | url=MESSAGES_SEND.format(username=username), 49 | data=data, 50 | headers=headers) 51 | return response 52 | -------------------------------------------------------------------------------- /lichess_client/endpoints/relations.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_relations import AbstractRelations 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import RELATIONS_FOLLOWERS_URL, RELATIONS_FOLLOWING_URL 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Relations(AbstractRelations): 13 | """Class for Relations API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def get_users_followed_by_a_user(self, username: str) -> 'Response': 19 | """ 20 | Fetching all users followed by current user. 21 | This is a streaming endpoint. 22 | 23 | Parameters 24 | ---------- 25 | username: str, required 26 | 27 | Returns 28 | ------- 29 | Response object with response content. 30 | 31 | Example 32 | ------- 33 | >>> from lichess_client import APIClient 34 | >>> client = APIClient(token='...') 35 | >>> response = client.users.get_users_followed_by_a_user(username='amasend') 36 | """ 37 | headers = { 38 | 'Content-Type': 'application/x-ndjson', 39 | } 40 | response = await self._client.request_stream(method=RequestMethods.GET, 41 | url=RELATIONS_FOLLOWING_URL.format(username=username), 42 | headers=headers) 43 | return response 44 | 45 | async def get_users_who_follow_a_user(self, username: str) -> 'Response': 46 | """ 47 | Fetching all users who follow this user. 48 | This is a streaming endpoint. 49 | 50 | Parameters 51 | ---------- 52 | username: str, required 53 | 54 | Returns 55 | ------- 56 | Response object with response content. 57 | 58 | Example 59 | ------- 60 | >>> from lichess_client import APIClient 61 | >>> client = APIClient(token='...') 62 | >>> response = client.users.get_users_who_follow_a_user(username='amasend') 63 | """ 64 | headers = { 65 | 'Content-Type': 'application/x-ndjson', 66 | } 67 | response = await self._client.request_stream(method=RequestMethods.GET, 68 | url=RELATIONS_FOLLOWERS_URL.format(username=username), 69 | headers=headers) 70 | return response 71 | -------------------------------------------------------------------------------- /lichess_client/endpoints/simulations.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_simulations import AbstractSimulations 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import SIMULATIONS_GET 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Simulations(AbstractSimulations): 13 | """Class for Simulations API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def get_current(self) -> 'Response': 19 | """ 20 | Get recently finished, ongoing, and upcoming simuls. 21 | 22 | Returns 23 | ------- 24 | Response object with response content. 25 | 26 | Example 27 | ------- 28 | >>> from lichess_client import APIClient 29 | >>> client = APIClient(token='...') 30 | >>> response = await client.simulations.get_current() 31 | """ 32 | 33 | response = await self._client.request(method=RequestMethods.GET, 34 | url=SIMULATIONS_GET) 35 | return response 36 | -------------------------------------------------------------------------------- /lichess_client/endpoints/studies.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_studies import AbstractStudies 4 | from lichess_client.utils.enums import RequestMethods 5 | from lichess_client.utils.hrefs import STUDIES_EXPORT_ALL_CHAPTERS, STUDIES_EXPORT_CHAPTER 6 | 7 | if TYPE_CHECKING: 8 | from lichess_client.clients.base_client import BaseClient 9 | from lichess_client.helpers import Response 10 | 11 | 12 | class Studies(AbstractStudies): 13 | """Class for Studies API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def export_chapter(self, study_id: str, chapter_id: str) -> 'Response': 19 | """ 20 | Download one study chapter in PGN format. 21 | 22 | Parameters 23 | ---------- 24 | study_id: str, required 25 | 26 | chapter_id: str, required 27 | 28 | Returns 29 | ------- 30 | Response object with response content. 31 | 32 | Example 33 | ------- 34 | >>> from lichess_client import APIClient 35 | >>> client = APIClient(token='...') 36 | >>> response = await client.studies.export_chapter(study_id="...", chapter_id='...') 37 | """ 38 | 39 | response = await self._client.request(method=RequestMethods.GET, 40 | url=STUDIES_EXPORT_CHAPTER.format(studyId=study_id, chapterId=chapter_id), 41 | ) 42 | return response 43 | 44 | async def export_all_chapters(self, study_id: str) -> 'Response': 45 | """ 46 | Download all chapters of a study in PGN format. 47 | 48 | Parameters 49 | ---------- 50 | study_id: str, required 51 | 52 | Returns 53 | ------- 54 | Response object with response content. 55 | 56 | Example 57 | ------- 58 | >>> from lichess_client import APIClient 59 | >>> client = APIClient(token='...') 60 | >>> response = await client.studies.export_all_chapters(study_id="...") 61 | """ 62 | 63 | response = await self._client.request(method=RequestMethods.GET, 64 | url=STUDIES_EXPORT_ALL_CHAPTERS.format(studyId=study_id), 65 | ) 66 | return response 67 | -------------------------------------------------------------------------------- /lichess_client/endpoints/teams.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_teams import AbstractTeams 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.hrefs import TEAMS_JOIN_URL, TEAMS_LEAVE_URL, TEAMS_KICK_USER_URL 6 | from lichess_client.utils.enums import RequestMethods 7 | 8 | if TYPE_CHECKING: 9 | from lichess_client.clients.base_client import BaseClient 10 | 11 | 12 | class Teams(AbstractTeams): 13 | """Class for Teams API Endpoint""" 14 | 15 | def __init__(self, client: 'BaseClient') -> None: 16 | self._client = client 17 | 18 | async def get_members_of_a_team(self) -> None: 19 | """Not implemented 20 | 21 | See also 22 | -------- 23 | client.users.get_members_of_a_team(...) 24 | """ 25 | raise NotImplementedError("Please use client.users.get_members_of_a_team(...)") 26 | 27 | async def join_a_team(self, team_id: str) -> 'Response': 28 | """ 29 | Join a team. If the team join policy requires a confirmation, and the team owner is not the oAuth app owner, 30 | then the call fails with 403 Forbidden. 31 | 32 | Parameters 33 | ---------- 34 | team_id: str, required 35 | 36 | Returns 37 | ------- 38 | Response object with response content. 39 | 40 | Example 41 | ------- 42 | >>> from lichess_client import APIClient 43 | >>> client = APIClient(token='...') 44 | >>> response = client.users.join_a_team(team_id = 'some_team') 45 | """ 46 | headers = { 47 | 'Content-Type': 'application/json' 48 | } 49 | response = await self._client.request(method=RequestMethods.POST, 50 | url=TEAMS_JOIN_URL.format(teamId=team_id), 51 | headers=headers) 52 | return response 53 | 54 | async def leave_a_team(self, team_id: str) -> 'Response': 55 | """ 56 | Leave a team. 57 | 58 | Parameters 59 | ---------- 60 | team_id: str, required 61 | 62 | Returns 63 | ------- 64 | Response object with response content. 65 | 66 | Example 67 | ------- 68 | >>> from lichess_client import APIClient 69 | >>> client = APIClient(token='...') 70 | >>> response = client.users.leave_a_team(team_id = 'some_team') 71 | """ 72 | headers = { 73 | 'Content-Type': 'application/json' 74 | } 75 | response = await self._client.request(method=RequestMethods.POST, 76 | url=TEAMS_LEAVE_URL.format(teamId=team_id), 77 | headers=headers) 78 | return response 79 | 80 | async def kick_a_user_from_your_team(self, team_id: str, user_id: str) -> 'Response': 81 | """ 82 | Kick a member out of one of your teams. 83 | 84 | Parameters 85 | ---------- 86 | team_id: str, required 87 | 88 | user_id: str, required 89 | 90 | Returns 91 | ------- 92 | Response object with response content. 93 | 94 | Example 95 | ------- 96 | >>> from lichess_client import APIClient 97 | >>> client = APIClient(token='...') 98 | >>> response = client.users.kick_a_user_from_your_team(team_id='some_team', user_id='amasend') 99 | """ 100 | headers = { 101 | 'Content-Type': 'application/json' 102 | } 103 | response = await self._client.request(method=RequestMethods.POST, 104 | url=TEAMS_KICK_USER_URL.format(teamId=team_id, userId=user_id), 105 | headers=headers) 106 | return response 107 | -------------------------------------------------------------------------------- /lichess_client/endpoints/tournaments.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING 3 | 4 | from lichess_client.abstract_endpoints.abstract_tournaments import AbstractTournaments 5 | from lichess_client.utils.enums import RequestMethods, VariantTypes 6 | from lichess_client.utils.hrefs import ( 7 | TOURNAMENTS_CURRENT, TOURNAMENTS_CREATE, TOURNAMENTS_EXPORT_GAMES, TOURNAMENTS_EXPORT_RESULTS, 8 | TOURNAMENTS_GET_CREATED_BY_USER) 9 | 10 | if TYPE_CHECKING: 11 | from lichess_client.clients.base_client import BaseClient 12 | from lichess_client.helpers import Response 13 | 14 | __all__ = [ 15 | "Tournaments" 16 | ] 17 | 18 | 19 | class Tournaments(AbstractTournaments): 20 | """Class for Tournaments API Endpoint""" 21 | 22 | def __init__(self, client: 'BaseClient') -> None: 23 | self._client = client 24 | 25 | async def get_current(self): 26 | """ 27 | Get recently finished, ongoing, and upcoming tournaments. 28 | 29 | Returns 30 | ------- 31 | Response object with response content. 32 | 33 | Example 34 | ------- 35 | >>> from lichess_client import APIClient 36 | >>> client = APIClient(token='...') 37 | >>> response = await client.tournaments.get_current() 38 | """ 39 | 40 | headers = { 41 | 'Content-Type': 'application/json' 42 | } 43 | 44 | response = await self._client.request(method=RequestMethods.GET, 45 | url=TOURNAMENTS_CURRENT, 46 | headers=headers) 47 | return response 48 | 49 | async def create(self, 50 | clock_time: int, 51 | clock_increment: int, 52 | minutes: int, 53 | name: str = None, 54 | wait_minutes: int = 5, 55 | start_date: int = None, 56 | variant: 'VariantTypes' = VariantTypes.STANDARD, 57 | rated: bool = True, 58 | position: str = None, 59 | berserk: bool = True, 60 | password: str = None, 61 | team_id: str = None, 62 | min_rating: int = None, 63 | max_rating: int = None, 64 | number_of_rated_games: int = None 65 | ) -> 'Response': 66 | """ 67 | Create a public or private tournament to your taste. 68 | This endpoint mirrors the form on https://lichess.org/tournament/new. 69 | You can create up to 2 tournaments per day. 70 | 71 | Parameters 72 | ---------- 73 | name: str, optional 74 | 75 | clock_time: int, required 76 | [0..60] Clock initial time in minutes 77 | 78 | clock_increment: int, required 79 | [0..60] Clock increment in seconds 80 | 81 | minutes: int, required 82 | [0..360] How long the tournament lasts, in minutes. 83 | 84 | wait_minutes: int, optional 85 | Default: 5 86 | How long to wait before starting the tournament, from now, in minutes 87 | 88 | start_date: int, optional 89 | Timestamp to start the tournament at a given date and time. Overrides the waitMinutes setting. 90 | 91 | variant: VariantTypes, optional 92 | Default: "standard" 93 | Enum: "standard" "chess960" "crazyhouse" "antichess" "atomic" "horde" 94 | "kingOfTheHill" "racingKings" "threeCheck" 95 | The variant to use in tournament games 96 | 97 | rated: bool, optional 98 | Default: true 99 | Games are rated and impact players ratings 100 | 101 | position: str, optional 102 | Default: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 103 | Custom initial position (in FEN) for all games of the tournament. Must be one of 104 | https://github.com/ornicar/scalachess/blob/ab61b7e6d8d4ab602f6366b29b0e5715717e8944/src/main/scala/StartingPosition.scala#L25 105 | 106 | berserk: bool, optional 107 | Default: true 108 | Whether the players can use berserk 109 | 110 | password: str, optional 111 | Make the tournament private, and restrict access with a password 112 | 113 | team_id, str, optional 114 | Restrict entry to members of a team. The team_id is the last part of a team URL, e.g. 115 | https://lichess.org/team/coders has teamId = coders. 116 | Leave as None to let everyone join the tournament. 117 | 118 | min_rating: int, optional 119 | Minimum rating to join. Leave empty to let everyone join the tournament. 120 | 121 | max_rating: int, optional 122 | Maximum rating to join. Based on best rating reached in the last 7 days. 123 | Leave empty to let everyone join the tournament. 124 | 125 | number_of_rated_games: int, optional 126 | Minimum number of rated games required to join. 127 | 128 | Returns 129 | ------- 130 | Response object with response content. 131 | 132 | Example 133 | ------- 134 | >>> from lichess_client import APIClient 135 | >>> client = APIClient(token='...') 136 | >>> response = await client.tournaments.create(clock_time=1, clock_increment=1, minutes=60) 137 | """ 138 | 139 | headers = { 140 | 'Content-Type': 'application/x-www-form-urlencoded' 141 | } 142 | 143 | data = { 144 | "clockTime": clock_time, 145 | "clockIncrement": clock_increment, 146 | "minutes": minutes, 147 | "waitMinutes": wait_minutes, 148 | "variant": variant.value if isinstance(variant, VariantTypes) else variant, 149 | "rated": json.dumps(rated), 150 | "berserkable": json.dumps(berserk), 151 | 152 | } 153 | 154 | if name is not None: 155 | data['name'] = name 156 | 157 | if start_date is not None: 158 | data['startDate'] = start_date 159 | 160 | if position is None: 161 | data['position'] = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 162 | 163 | if password is not None: 164 | data['password'] = password 165 | 166 | if team_id is not None: 167 | data['conditions.teamMember.teamId'] = team_id 168 | 169 | if min_rating is not None: 170 | data['conditions.minRating.rating'] = min_rating 171 | 172 | if max_rating is not None: 173 | data['conditions.maxRating.rating'] = max_rating 174 | 175 | if number_of_rated_games is not None: 176 | data['conditions.nbRatedGame.nb'] = number_of_rated_games 177 | 178 | response = await self._client.request(method=RequestMethods.POST, 179 | url=TOURNAMENTS_CREATE, 180 | data=data, 181 | headers=headers) 182 | return response 183 | 184 | async def export_games(self, tournament_id: str) -> 'Response': 185 | """ 186 | Download games of a tournament. Games are sorted by reverse chronological order (most recent first) 187 | 188 | Parameters 189 | ---------- 190 | tournament_id: str, required 191 | ID of the tournament to export. 192 | 193 | Returns 194 | ------- 195 | Response object with response content. 196 | 197 | Example 198 | ------- 199 | >>> from lichess_client import APIClient 200 | >>> client = APIClient(token='...') 201 | >>> response = client.tournaments.export_games(tournament_id='q7zvsdUF') 202 | """ 203 | 204 | headers = { 205 | 'Content-Type': 'application/json' 206 | } 207 | 208 | parameters = { 209 | 'moves': 'true', 210 | 'pgnInJson': 'false', 211 | 'tags': 'true', 212 | 'clocks': 'true', 213 | 'evals': 'true', 214 | 'opening': 'true', 215 | } 216 | 217 | response = await self._client.request_stream(method=RequestMethods.GET, 218 | url=TOURNAMENTS_EXPORT_GAMES.format(id=tournament_id), 219 | headers=headers, 220 | params=parameters) 221 | return response 222 | 223 | async def get_results(self, tournament_id: str, limit: int = 10) -> 'Response': 224 | """ 225 | Players of a tournament, with their score and performance, sorted by rank (best first). 226 | games of a tournament. Games are sorted by reverse chronological order (most recent first) 227 | 228 | Parameters 229 | ---------- 230 | tournament_id: str, required 231 | ID of the tournament to export. 232 | 233 | limit: int, optional 234 | Default: 10 235 | Max number of players to fetch 236 | 237 | Returns 238 | ------- 239 | Response object with response content. 240 | 241 | Example 242 | ------- 243 | >>> from lichess_client import APIClient 244 | >>> client = APIClient(token='...') 245 | >>> response = client.tournaments.get_results(tournament_id='q7zvsdUF') 246 | """ 247 | 248 | headers = { 249 | 'Content-Type': 'application/json' 250 | } 251 | parameters = { 252 | 'nb': limit 253 | } 254 | response = await self._client.request_stream(method=RequestMethods.GET, 255 | url=TOURNAMENTS_EXPORT_RESULTS.format(id=tournament_id), 256 | headers=headers, 257 | params=parameters) 258 | return response 259 | 260 | async def get_tournaments_created_by_a_user(self, username: str, limit: int = 10) -> 'Response': 261 | """ 262 | Get all tournaments created by a given user. 263 | Tournaments are sorted by reverse chronological order of start date (last starting first). 264 | 265 | Parameters 266 | ---------- 267 | username: str, required 268 | 269 | limit: int, optional 270 | Default: 10 271 | Max number of tournaments to fetch. 272 | 273 | Returns 274 | ------- 275 | Response object with response content. 276 | 277 | Example 278 | ------- 279 | >>> from lichess_client import APIClient 280 | >>> client = APIClient(token='...') 281 | >>> response = client.tournaments.get_tournaments_created_by_a_user(username='amasend') 282 | """ 283 | 284 | headers = { 285 | 'Content-Type': 'application/json' 286 | } 287 | 288 | parameters = { 289 | 'nb': limit 290 | } 291 | response = await self._client.request_stream(method=RequestMethods.GET, 292 | url=TOURNAMENTS_GET_CREATED_BY_USER.format(username=username), 293 | headers=headers, 294 | params=parameters) 295 | return response 296 | -------------------------------------------------------------------------------- /lichess_client/endpoints/users.py: -------------------------------------------------------------------------------- 1 | from typing import List, TYPE_CHECKING 2 | 3 | from lichess_client.abstract_endpoints.abstract_users import AbstractUsers 4 | from lichess_client.utils.enums import RequestMethods, VariantTypes 5 | from lichess_client.utils.hrefs import (USERS_ACTIVITY_URL, USERS_GET_BY_IDS_URL, USERS_LIVE_STREAMERS_URL, 6 | USERS_MY_PUZZLE_ACTIVITY_URL, USERS_PLAYER_TOP_URL, USERS_PLAYER_URL, 7 | USERS_RATING_HISTORY_URL, USERS_STATUS_URL, USERS_TEAM_MEMBERS_URL, 8 | USERS_USER_PUBLIC_DATA_URL) 9 | 10 | if TYPE_CHECKING: 11 | from lichess_client.clients.base_client import BaseClient 12 | from lichess_client.helpers import Response 13 | 14 | 15 | class Users(AbstractUsers): 16 | """Class for Users API Endpoint""" 17 | 18 | def __init__(self, client: 'BaseClient') -> None: 19 | self._client = client 20 | 21 | async def get_real_time_users_status(self, users_ids: List[str]) -> 'Response': 22 | """ 23 | Read the online, playing and streaming flags of several users. 24 | 25 | This API is very fast and cheap on lichess side. So you can call it quite often (like once every 5 seconds). 26 | 27 | Use it to track players and know when they're connected on lichess and playing games. 28 | 29 | Parameters 30 | ---------- 31 | users_ids: List[str], required 32 | List of the users IDs to fetch information about the status. 33 | 34 | Returns 35 | ------- 36 | Response object with response content. 37 | 38 | Example 39 | ------- 40 | >>> from lichess_client import APIClient 41 | >>> client = APIClient(token='...') 42 | >>> response = client.users.get_real_time_users_status( 43 | >>> users_ids=['amasend', 'aliquantus','chess-network', 'lovlas']) 44 | """ 45 | headers = { 46 | 'Content-Type': 'application/json' 47 | } 48 | parameters = {'ids': ','.join(users_ids)} 49 | response = await self._client.request(method=RequestMethods.GET, url=USERS_STATUS_URL, 50 | headers=headers, params=parameters) 51 | return response 52 | 53 | async def get_all_top_10(self) -> 'Response': 54 | """ 55 | Get the top 10 players for each speed and variant. 56 | 57 | Returns 58 | ------- 59 | Response object with response content. 60 | 61 | Example 62 | ------- 63 | >>> from lichess_client import APIClient 64 | >>> client = APIClient(token='...') 65 | >>> response = client.users.get_all_top_10() 66 | """ 67 | headers = { 68 | 'Content-Type': 'application/json', 69 | 'Accept': 'application/vnd.lichess.v3+json' 70 | } 71 | response = await self._client.request(method=RequestMethods.GET, url=USERS_PLAYER_URL, headers=headers) 72 | return response 73 | 74 | async def get_one_leaderboard(self, variant: 'VariantTypes', limit: int = 10) -> 'Response': 75 | """ 76 | Get the leaderboard for a single speed or variant (a.k.a. perfType). 77 | There is no leaderboard for correspondence or puzzles. 78 | 79 | Parameters 80 | ---------- 81 | limit: int, optional 82 | How many users to fetch. 83 | 84 | variant: VariantTypes, required 85 | 86 | 87 | Returns 88 | ------- 89 | Response object with response content. 90 | 91 | See also 92 | -------- 93 | VariantTypes 94 | 95 | Example 96 | ------- 97 | >>> from lichess_client import APIClient 98 | >>> client = APIClient(token='...') 99 | >>> response = client.users.get_one_leaderboard(variant=VariantTypes.BLITZ) 100 | >>> response_2 = client.users.get_one_leaderboard(variant=VariantTypes.ANTICHESS, limit=100) 101 | """ 102 | headers = { 103 | 'Content-Type': 'application/json', 104 | 'Accept': 'application/vnd.lichess.v3+json' 105 | } 106 | response = await self._client.request(method=RequestMethods.GET, 107 | url=USERS_PLAYER_TOP_URL.format(nb=limit, perfType=variant), 108 | headers=headers) 109 | return response 110 | 111 | async def get_user_public_data(self, username: str) -> 'Response': 112 | """ 113 | Read public data of a user. 114 | 115 | Parameters 116 | ---------- 117 | username: str, required 118 | 119 | Returns 120 | ------- 121 | Response object with response content. 122 | 123 | Example 124 | ------- 125 | >>> from lichess_client import APIClient 126 | >>> client = APIClient(token='...') 127 | >>> response = client.users.get_user_public_data(username='amasend') 128 | """ 129 | headers = { 130 | 'Content-Type': 'application/json', 131 | } 132 | response = await self._client.request(method=RequestMethods.GET, 133 | url=USERS_USER_PUBLIC_DATA_URL.format(username=username), 134 | headers=headers) 135 | return response 136 | 137 | async def get_rating_history_of_a_user(self, username: str) -> 'Response': 138 | """ 139 | Read rating history of a user, for all perf types. There is at most one entry per day. 140 | Format of an entry is [year, month, day, rating]. month starts at zero (January). 141 | 142 | Parameters 143 | ---------- 144 | username: str, required 145 | 146 | Returns 147 | ------- 148 | Response object with response content. 149 | 150 | Example 151 | ------- 152 | >>> from lichess_client import APIClient 153 | >>> client = APIClient(token='...') 154 | >>> response = client.users.get_rating_history_of_a_user(username='amasend') 155 | """ 156 | # TODO: Make an ISSUE, endpoint supports 'Content-Type': 'text/html' not 'application/json' 157 | headers = { 158 | 'Content-Type': 'text/html', 159 | } 160 | response = await self._client.request(method=RequestMethods.GET, 161 | url=USERS_RATING_HISTORY_URL.format(username=username), 162 | headers=headers) 163 | return response 164 | 165 | async def get_user_activity(self, username: str) -> 'Response': 166 | """ 167 | Read data to generate the activity feed of a user. 168 | 169 | Parameters 170 | ---------- 171 | username: str, required 172 | 173 | Returns 174 | ------- 175 | Response object with response content. 176 | 177 | Example 178 | ------- 179 | >>> from lichess_client import APIClient 180 | >>> client = APIClient(token='...') 181 | >>> response = client.users.get_user_activity(username='amasend') 182 | """ 183 | headers = { 184 | 'Content-Type': 'application/json' 185 | } 186 | response = await self._client.request(method=RequestMethods.GET, 187 | url=USERS_ACTIVITY_URL.format(username=username), 188 | headers=headers) 189 | return response 190 | 191 | async def get_your_puzzle_activity(self, limit: int = None) -> 'Response': 192 | """ 193 | Download your puzzle activity in ndjson format. 194 | Puzzle activity is sorted by reverse chronological order (most recent first) 195 | 196 | Parameters 197 | ---------- 198 | limit: int, optional 199 | Hom many records to fetch, if None, all records will be fetched (by streaming). 200 | 201 | Returns 202 | ------- 203 | Response object with response content. 204 | 205 | Example 206 | ------- 207 | >>> from lichess_client import APIClient 208 | >>> client = APIClient(token='...') 209 | >>> response = client.users.get_your_puzzle_activity() 210 | >>> response_2 = client.users.get_your_puzzle_activity(limit=50) 211 | """ 212 | headers = { 213 | 'Content-Type': 'application/x-ndjson', 214 | } 215 | parameters = { 216 | 'max': limit if limit is not None else 'null' 217 | } 218 | response = await self._client.request_stream(method=RequestMethods.GET, 219 | url=USERS_MY_PUZZLE_ACTIVITY_URL, 220 | headers=headers, 221 | params=parameters) 222 | return response 223 | 224 | async def get_users_by_id(self, users_ids: List[str]) -> 'Response': 225 | """ 226 | Get several users by their IDs. Users are returned in the order same order as the IDs. 227 | The method is POST so a longer list of IDs can be sent in the request body. 228 | 229 | Parameters 230 | ---------- 231 | users_ids: List[str], required 232 | List of the users IDs to fetch information about the status. 233 | 234 | Returns 235 | ------- 236 | Response object with response content. 237 | 238 | Example 239 | ------- 240 | >>> from lichess_client import APIClient 241 | >>> client = APIClient(token='...') 242 | >>> response = client.users.get_users_by_id(users_ids=['amasend', 'aliquantus','chess-network', 'lovlas']) 243 | """ 244 | headers = { 245 | 'Content-Type': 'text/plain' 246 | } 247 | data = ','.join(users_ids) 248 | response = await self._client.request(method=RequestMethods.POST, url=USERS_GET_BY_IDS_URL, 249 | headers=headers, data=data) 250 | return response 251 | 252 | async def get_members_of_a_team(self, team_id: str) -> 'Response': 253 | """ 254 | Download your puzzle activity in ndjson format. 255 | Puzzle activity is sorted by reverse chronological order (most recent first) 256 | 257 | Parameters 258 | ---------- 259 | team_id: str, required 260 | Team ID 261 | 262 | Returns 263 | ------- 264 | Response object with response content. 265 | 266 | Example 267 | ------- 268 | >>> from lichess_client import APIClient 269 | >>> client = APIClient(token='...') 270 | >>> response = client.users.get_members_of_a_team(team_id='team') 271 | """ 272 | headers = { 273 | 'Content-Type': 'application/x-ndjson', 274 | } 275 | response = await self._client.request_stream(method=RequestMethods.GET, 276 | url=USERS_TEAM_MEMBERS_URL.format(teamId=team_id), 277 | headers=headers) 278 | return response 279 | 280 | async def get_live_streamers(self) -> 'Response': 281 | """ 282 | Get basic info about currently streaming users. 283 | 284 | Returns 285 | ------- 286 | Response object with response content. 287 | 288 | Example 289 | ------- 290 | >>> from lichess_client import APIClient 291 | >>> client = APIClient(token='...') 292 | >>> response = client.users.get_live_streamers() 293 | """ 294 | headers = { 295 | 'Content-Type': 'application/json', 296 | } 297 | response = await self._client.request(method=RequestMethods.GET, 298 | url=USERS_LIVE_STREAMERS_URL, 299 | headers=headers) 300 | return response 301 | -------------------------------------------------------------------------------- /lichess_client/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from lichess_client.helpers.response_helpers import Response, ResponseEntity, ResponseMetadata 2 | -------------------------------------------------------------------------------- /lichess_client/helpers/response_helpers.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union, TYPE_CHECKING 2 | 3 | from lichess_client.utils.enums import RequestMethods, StatusTypes 4 | 5 | if TYPE_CHECKING: 6 | from chess.pgn import Game 7 | 8 | __all__ = [ 9 | "Response", 10 | "ResponseMetadata", 11 | "ResponseEntity" 12 | ] 13 | 14 | 15 | class BaseHelper: 16 | """Base Helper class with defined custom magic methods, also for to dictionary conversion.""" 17 | 18 | def to_dict(self) -> dict: 19 | _dict: dict = {} 20 | for key, val in vars(self).items(): 21 | if hasattr(val, 'to_dict'): 22 | _dict[key] = val.to_dict() 23 | else: 24 | _dict[key] = val 25 | return _dict 26 | 27 | def __repr__(self): 28 | return str(self.to_dict()) 29 | 30 | def __str__(self): 31 | return str(self.to_dict()) 32 | 33 | 34 | class ResponseMetadata(BaseHelper): 35 | """Metadata class for the response object.""" 36 | 37 | def __init__(self, method: str, url: str, content_type: str, timestamp: bytes) -> None: 38 | self.method = RequestMethods[method] 39 | self.url = url 40 | self.content_type = content_type 41 | self.timestamp = timestamp 42 | 43 | 44 | class ResponseEntity(BaseHelper): 45 | """Entity class for the response object.""" 46 | 47 | def __init__(self, code: int, reason: str, status: 'StatusTypes', content: Union[List[dict], dict, 'Game']) -> None: 48 | self.code = code 49 | self.reason = reason 50 | self.status = status 51 | self.content = content 52 | 53 | 54 | class Response(BaseHelper): 55 | """The Response class. Used to store every API response in the unified way.""" 56 | 57 | def __init__(self, metadata: 'ResponseMetadata', entity: 'ResponseEntity') -> None: 58 | self.metadata = metadata 59 | self.entity = entity 60 | -------------------------------------------------------------------------------- /lichess_client/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasend/lichess_python_SDK/1e2a545b65111bfc58bb963c44ad56be9b4d0835/lichess_client/utils/__init__.py -------------------------------------------------------------------------------- /lichess_client/utils/client_errors.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | from typing import Any 4 | 5 | 6 | logger = logging.getLogger('lichess_client') 7 | logger.setLevel(logging.DEBUG) 8 | handler = RotatingFileHandler('lichess_python_client.log', maxBytes=10240, backupCount=3) 9 | handler.setFormatter(logging.Formatter( 10 | fmt="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", datefmt='%Y-%m-%dT%H:%M:%S')) 11 | logger.addHandler(handler) 12 | 13 | 14 | class BaseError(Exception): 15 | def __init__(self, value: Any, reason: str) -> None: 16 | super().__init__(value, reason) 17 | logger.debug(f"Error in: {value} Reason: {reason}") 18 | 19 | 20 | class ToManyIDs(BaseError): 21 | def __init__(self, value: Any, reason: str) -> None: 22 | super().__init__(value, reason) 23 | 24 | 25 | class LimitError(BaseError): 26 | def __init__(self, value: Any, reason: str) -> None: 27 | super().__init__(value, reason) 28 | 29 | 30 | class RatingRangeError(BaseError): 31 | def __init__(self, value: Any, reason: str) -> None: 32 | super().__init__(value, reason) 33 | -------------------------------------------------------------------------------- /lichess_client/utils/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class RequestMethods(Enum): 5 | """HTTP REST methods types""" 6 | GET = 'GET' 7 | POST = 'POST' 8 | PUT = 'PUT' 9 | DELETE = 'DELETE' 10 | 11 | 12 | class StatusTypes(str, Enum): 13 | """API response statuses""" 14 | SUCCESS = 'success' 15 | ERROR = 'error' 16 | 17 | 18 | class VariantTypes(str, Enum): 19 | """Game variant types""" 20 | STANDARD = "standard" 21 | ULTRA_BULLET = "ultraBullet" 22 | BULLET = "bullet" 23 | BLITZ = "blitz" 24 | RAPID = "rapid" 25 | CLASSICAL = "classical" 26 | CHESS960 = "chess960" 27 | CRAZYHOUSE = "crazyhouse" 28 | ANTICHESS = "antichess" 29 | ATOMIC = "atomic" 30 | HORDE = "horde" 31 | KING_OF_THE_HILL = "kingOfTheHill" 32 | RACING_KINGS = "racingKings" 33 | THRESS_CHECK = "threeCheck" 34 | 35 | 36 | class ColorType(str, Enum): 37 | """Color of the user pawns.""" 38 | WHITE = "white" 39 | BLACK = "black" 40 | RANDOM = "random" 41 | 42 | 43 | class RoomTypes(str, Enum): 44 | """Room types to posting user message.""" 45 | PLAYER = "player" 46 | SPECTATOR = "spectator" 47 | 48 | 49 | class SideTypes(Enum): 50 | """Side enum for Game description""" 51 | WHITE = 1 52 | BLACK = -1 53 | 54 | 55 | # TODO: check more statuses 56 | class GameStatusTypes(str, Enum): 57 | """Game status""" 58 | RESIGN = 'resign' 59 | STARTED = 'started' 60 | -------------------------------------------------------------------------------- /lichess_client/utils/hrefs.py: -------------------------------------------------------------------------------- 1 | LICHESS_URL = 'https://lichess.org/' 2 | 3 | ########### 4 | # ACCOUNT # 5 | ########### 6 | ACCOUNT_URL = 'api/account' 7 | ACCOUNT_EMAIL_URL = 'api/account/email' 8 | ACCOUNT_PREFERENCES_URL = 'api/account/preferences' 9 | ACCOUNT_KID_URL = 'api/account/kid' 10 | 11 | ######### 12 | # USERS # 13 | ######### 14 | USERS_STATUS_URL = 'api/users/status' 15 | USERS_PLAYER_URL = 'player' 16 | USERS_PLAYER_TOP_URL = 'player/top/{nb}/{perfType}' 17 | USERS_USER_PUBLIC_DATA_URL = 'api/user/{username}' 18 | USERS_RATING_HISTORY_URL = 'api/user/{username}/rating-history' 19 | USERS_ACTIVITY_URL = 'api/user/{username}/activity' 20 | USERS_MY_PUZZLE_ACTIVITY_URL = 'api/user/puzzle-activity' 21 | USERS_GET_BY_IDS_URL = 'api/users' 22 | USERS_TEAM_MEMBERS_URL = 'team/{teamId}/users' 23 | USERS_LIVE_STREAMERS_URL = 'streamer/live' 24 | 25 | ############# 26 | # RELATIONS # 27 | ############# 28 | RELATIONS_FOLLOWING_URL = 'api/user/{username}/following' 29 | RELATIONS_FOLLOWERS_URL = 'api/user/{username}/followers' 30 | 31 | ######### 32 | # GAMES # 33 | ######### 34 | GAMES_EXPORT_ONE_URL = 'game/export/{gameId}' 35 | GAMES_EXPORT_USER_URL = 'api/games/user/{username}' 36 | GAMES_EXPORT_IDS_URL = 'games/export/_ids' 37 | GAMES_STREAM_CURRENT_URL = 'api/stream/games-by-users' 38 | GAMES_ONGOING_URL = 'api/account/playing' 39 | GAMES_CURRENT_TV_URL = 'tv/channels' 40 | 41 | ######### 42 | # TEAMS # 43 | ######### 44 | TEAMS_JOIN_URL = 'team/{teamId}/join' 45 | TEAMS_LEAVE_URL = 'team/{teamId}/quit' 46 | TEAMS_KICK_USER_URL = '/team/{teamId}/kick/{userId}' 47 | 48 | ######## 49 | # BOTS # 50 | ######## 51 | BOTS_STREAM_INCOMING_EVENTS = 'api/stream/event' 52 | 53 | ########## 54 | # BOARDS # 55 | ########## 56 | BOARDS_STREAM_INCOMING_EVENTS = 'api/stream/event' 57 | BOARDS_CREATE_A_SEEK = 'api/board/seek' 58 | BOARDS_STREAM_GAME_STATE = 'api/board/game/stream/{gameId}' 59 | BOARDS_MAKE_MOVE = 'api/board/game/{gameId}/move/{move}' 60 | BOARDS_ABORT_GAME = 'api/board/game/{gameId}/abort' 61 | BOARDS_RESIGN_GAME = 'api/board/game/{gameId}/resign' 62 | BOARDS_WRITE_IN_CHAT = 'api/board/game/{gameId}/chat' 63 | BOARDS_HANDLE_DRAW = 'api/board/game/{gameId}/draw/{accept}' 64 | 65 | ############## 66 | # CHALLENGES # 67 | ############## 68 | CHALLENGES_CREATE = 'api/challenge/{username}' 69 | CHALLENGES_ACCEPT = 'api/challenge/{challengeId}/accept' 70 | CHALLENGES_DECLINE = 'api/challenge/{challengeId}/decline' 71 | 72 | ############### 73 | # TOURNAMENTS # 74 | ############### 75 | TOURNAMENTS_CURRENT = "api/tournament" 76 | TOURNAMENTS_CREATE = "api/tournament" 77 | TOURNAMENTS_EXPORT_GAMES = "api/tournament/{id}/games" 78 | TOURNAMENTS_EXPORT_RESULTS = "api/tournament/{id}/results" 79 | TOURNAMENTS_GET_CREATED_BY_USER = "api/user/{username}/tournament/created" 80 | 81 | ############### 82 | # BROADCASTS # 83 | ############### 84 | BROADCASTS_CREATE = "broadcast/new" 85 | BROADCASTS_GET = "broadcast/-/{broadcastId}" 86 | BROADCASTS_PUSH_PGN = "broadcast/-/{broadcastId}/push" 87 | 88 | ############### 89 | # SIMULATIONS # 90 | ############### 91 | SIMULATIONS_GET = "api/simul" 92 | 93 | ############ 94 | # MESSAGES # 95 | ############ 96 | MESSAGES_SEND = "inbox/{username}" 97 | 98 | ########### 99 | # STUDIES # 100 | ########### 101 | STUDIES_EXPORT_CHAPTER = "study/{studyId}/{chapterId}.pgn" 102 | STUDIES_EXPORT_ALL_CHAPTERS = "study/{studyId}.pgn" 103 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | python-chess -------------------------------------------------------------------------------- /sample_notebooks/imgs/lichess_token_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasend/lichess_python_SDK/1e2a545b65111bfc58bb963c44ad56be9b4d0835/sample_notebooks/imgs/lichess_token_creation.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('VERSION.txt', 'r') as f: 4 | version = f.read() 5 | 6 | with open("README.md", "r") as fh: 7 | long_description = fh.read() 8 | 9 | PROJECT_URLS = { 10 | 'Bug Tracker': 'https://github.com/amasend/lichess_python_SDK/issues', 11 | 'Documentation': 'https://amasend.github.io/lichess_python_SDK/html/index.html', 12 | 'Source Code': 'https://github.com/amasend/lichess_python_SDK' 13 | } 14 | 15 | setuptools.setup( 16 | name="async_lichess_sdk", 17 | version=version, 18 | author="Amaedeusz Masny", 19 | author_email="amadeuszmasny@gmail.com", 20 | description="Asynchronous Python API client for accessing the lichess.org API.", 21 | long_description=long_description, 22 | long_description_content_type='text/markdown', 23 | install_requires=[ 24 | 'aiohttp', 25 | 'python-chess', 26 | ], 27 | url="https://github.com/amasend/lichees_python_SDK", 28 | packages=setuptools.find_packages(exclude=["tests"]), 29 | license="Apache Software License", 30 | classifiers=[ 31 | "Programming Language :: Python :: 3.6", 32 | "Programming Language :: Python :: 3.7", 33 | "License :: OSI Approved :: Apache Software License", 34 | "Operating System :: OS Independent", 35 | ], 36 | python_requires='>=3.6', 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasend/lichess_python_SDK/1e2a545b65111bfc58bb963c44ad56be9b4d0835/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasend/lichess_python_SDK/1e2a545b65111bfc58bb963c44ad56be9b4d0835/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_account_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from lichess_client import APIClient 5 | from lichess_client.helpers import Response 6 | from lichess_client.utils.enums import StatusTypes 7 | from tests.utils import get_token_from_config, async_test 8 | 9 | 10 | class TestAccountEndpoint(unittest.TestCase): 11 | client = None 12 | token = get_token_from_config('amasend') 13 | 14 | @classmethod 15 | def setUp(cls) -> None: 16 | cls.client = APIClient(token=cls.token) 17 | 18 | @async_test 19 | async def test_01__get_my_profile__fetching_profile_info__response_object_returned_with_success(self): 20 | response = await self.client.account.get_my_profile() 21 | print(response) 22 | 23 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 24 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 25 | 26 | @async_test 27 | async def test_02__get_my_email_address__fetching_email__response_object_returned_with_success(self): 28 | response = await self.client.account.get_my_email_address() 29 | print(response) 30 | 31 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 32 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 33 | 34 | @async_test 35 | async def test_03__get_my_preferences__fetching_preferences__response_object_returned_with_success(self): 36 | response = await self.client.account.get_my_preferences() 37 | print(response) 38 | 39 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 40 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 41 | 42 | @async_test 43 | async def test_04__get_my_kid_mode_status__fetching_kid_status__response_object_returned_with_success(self): 44 | response = await self.client.account.get_my_kid_mode_status() 45 | print(response.metadata.timestamp) 46 | print(response.entity.content) 47 | print(response) 48 | 49 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 50 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 51 | 52 | @async_test 53 | async def test_05__set_my_kid_mode_status__setting_kid_status__response_object_returned_with_success(self): 54 | response_1 = await self.client.account.set_my_kid_mode_status(turned_on=True) 55 | time.sleep(2) 56 | response_2 = await self.client.account.get_my_kid_mode_status() 57 | print(f"Setting kid mode: {response_1}") 58 | print(f"Kid mode set to: {response_2.entity.content['kid']}") 59 | 60 | self.assertIsInstance(response_1, Response, msg="Response in not of type \"Response\"") 61 | self.assertEqual(response_1.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 62 | self.assertTrue(response_2.entity.content['kid'], msg="Kid mode status was not set correctly.") 63 | 64 | await self.client.account.set_my_kid_mode_status(turned_on=False) 65 | time.sleep(2) 66 | response_3 = await self.client.account.get_my_kid_mode_status() 67 | print(f"Kid mode set to: {response_3.entity.content['kid']}") 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /tests/unit/test_base_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from lichess_client.clients.base_client import BaseClient 5 | from tests.utils import async_test, get_token_from_config 6 | 7 | 8 | class TestBaseClient(unittest.TestCase): 9 | event_loop = None 10 | 11 | client: 'BaseClient' = None 12 | token = get_token_from_config(section='amasend') 13 | 14 | @classmethod 15 | def setUp(cls) -> None: 16 | cls.event_loop = asyncio.get_event_loop() 17 | 18 | @async_test 19 | async def test_01__initialization__init_of_BaseClient_class__all_arguments_stored(self): 20 | TestBaseClient.client = BaseClient(token=self.token, loop=self.event_loop) 21 | 22 | self.assertEqual(self.client._token, self.token, msg="Token incorrectly stored.") 23 | self.assertEqual(self.client.loop, self.event_loop, msg="Event loop incorrectly stored.") 24 | 25 | @async_test 26 | async def test_02__is_authorized__check_if_async_request_works__authorized(self): 27 | response = await self.client.is_authorized() 28 | 29 | self.assertTrue(response, msg="Not authorized.") 30 | 31 | @unittest.expectedFailure 32 | @async_test 33 | async def test_03__is_authorized__check_if_async_request_works__not_authorized(self): 34 | client = BaseClient(token="not_so_random_characters", loop=self.event_loop) 35 | response = await client.is_authorized() 36 | 37 | self.assertTrue(response, msg="Not authorized.") 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /tests/unit/test_boards_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | # TODO: write some functional test with pytest (fire in Github workflow) 10 | class TestBoardsEndpoint(unittest.TestCase): 11 | client = None 12 | token = get_token_from_config('amasend') 13 | game_id = 'IxJ26EAH' 14 | 15 | @classmethod 16 | def setUp(cls) -> None: 17 | cls.client = APIClient(token=cls.token) 18 | 19 | @unittest.SkipTest 20 | @async_test 21 | async def test_01__stream_incoming_events__fetching_information_about_incoming_game__response_object_returned_with_success(self): 22 | async for response in self.client.boards.stream_incoming_events(): 23 | print(response) 24 | 25 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 26 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 27 | 28 | @unittest.SkipTest 29 | @async_test 30 | async def test_02__create_a_seek__seeking_the_game__response_object_returned_with_success(self): 31 | response = await self.client.boards.create_a_seek(time=15, increment=15, rated=True) 32 | print(response) 33 | 34 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 35 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 36 | 37 | @unittest.SkipTest 38 | @async_test 39 | async def test_03__stream_game_state__fetching_current_game_state__response_object_returned_with_success(self): 40 | async for response in self.client.boards.stream_game_state(game_id=self.game_id): 41 | print(response) 42 | 43 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 44 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 45 | 46 | @unittest.SkipTest 47 | @async_test 48 | async def test_04__make_move__send_a_move__response_object_returned_with_success(self): 49 | response = await self.client.boards.make_move(game_id=self.game_id, move='g8f6') 50 | print(response) 51 | 52 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 53 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 54 | 55 | @unittest.SkipTest 56 | @async_test 57 | async def test_05__abort_game__aborting_a_game__response_object_returned_with_success(self): 58 | response = await self.client.boards.abort_game(game_id=self.game_id) 59 | print(response) 60 | 61 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 62 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 63 | 64 | @unittest.SkipTest 65 | @async_test 66 | async def test_06__resign_game__resigning_a_game__response_object_returned_with_success(self): 67 | response = await self.client.boards.resign_game(game_id=self.game_id) 68 | print(response) 69 | 70 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 71 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 72 | 73 | @unittest.SkipTest 74 | @async_test 75 | async def test_07__write_in_chat__posting_user_message__response_object_returned_with_success(self): 76 | response = await self.client.boards.write_in_chat(game_id=self.game_id, message="Hello!") 77 | print(response) 78 | from lichess_client.utils.enums import RoomTypes 79 | response = await self.client.boards.write_in_chat(game_id=self.game_id, message="Hi all!", 80 | room=RoomTypes.SPECTATOR) 81 | print(response) 82 | 83 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 84 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 85 | 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /tests/unit/test_bots_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestBotsEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('connector_123') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @unittest.SkipTest 18 | @async_test 19 | async def test_01__stream_incoming_events__fetching_information_about_incoming_game__response_object_returned_with_success(self): 20 | response = await self.client.bots.stream_incoming_events() 21 | print(response) 22 | 23 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 24 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /tests/unit/test_broadcast_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestBroadcastEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | broadcast_id = None 13 | games = None 14 | 15 | @classmethod 16 | def setUp(cls) -> None: 17 | cls.client = APIClient(token=cls.token) 18 | 19 | @async_test 20 | async def test_01__create__broadcast_creation__response_object_returned_with_success(self): 21 | response = await self.client.broadcast.create(name="Test broadcast", description="This is desc.") 22 | print(response) 23 | 24 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 25 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 26 | TestBroadcastEndpoint.broadcast_id = response.entity.content['broadcast']['id'] 27 | 28 | @async_test 29 | async def test_02__get__get_info_about_created_broadcast__response_object_returned_with_success(self): 30 | response = await self.client.broadcast.get(broadcast_id=self.broadcast_id) 31 | print(response) 32 | 33 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 34 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 35 | 36 | @unittest.SkipTest 37 | @async_test 38 | async def test_03__update__updating_broadcast__response_object_returned_with_success(self): 39 | response = await self.client.broadcast.update(broadcast_id=self.broadcast_id, name="Updated broadcast", 40 | description="Ohh a new description!") 41 | print(response) 42 | 43 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 44 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 45 | 46 | @async_test 47 | async def test_04__push_pgn__pushing_games_info__response_object_returned_with_success(self): 48 | response = await self.client.broadcast.push_pgn(broadcast_id=self.broadcast_id, games=self.games) 49 | print(response) 50 | 51 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 52 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/unit/test_challenges_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | # TODO: write some functional test with pytest (fire in Github workflow) 10 | class TestChallengesEndpoint(unittest.TestCase): 11 | client = None 12 | token = get_token_from_config('amasend') 13 | 14 | @classmethod 15 | def setUp(cls) -> None: 16 | cls.client = APIClient(token=cls.token) 17 | 18 | @unittest.SkipTest 19 | @async_test 20 | async def test_01__create_a_challenge__sent_challenge_invitation__response_object_returned_with_success(self): 21 | response = await self.client.challenges.create(username='some_user') 22 | print(response) 23 | 24 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 25 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 26 | 27 | @unittest.SkipTest 28 | @async_test 29 | async def test_02__accept_a_challenge__accepting_a_challenge__response_object_returned_with_success(self): 30 | response = await self.client.challenges.accept(username='some_user') 31 | print(response) 32 | 33 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 34 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 35 | 36 | @unittest.SkipTest 37 | @async_test 38 | async def test_03__decline_a_challenge__declining_a_challenge__response_object_returned_with_success(self): 39 | response = await self.client.challenges.decline(username='some_user') 40 | print(response) 41 | 42 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 43 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | 4 | from lichess_client import APIClient 5 | from lichess_client.clients.base_client import BaseClient 6 | from lichess_client.endpoints.account import Account 7 | from lichess_client.endpoints.broadcast import Broadcast 8 | from lichess_client.endpoints.challenges import Challenges 9 | from lichess_client.endpoints.chess_bot import ChessBot 10 | from lichess_client.endpoints.games import Games 11 | from lichess_client.endpoints.messaging import Messaging 12 | from lichess_client.endpoints.relations import Relations 13 | from lichess_client.endpoints.simulations import Simulations 14 | from lichess_client.endpoints.studies import Studies 15 | from lichess_client.endpoints.teams import Teams 16 | from lichess_client.endpoints.tournaments import Tournaments 17 | from lichess_client.endpoints.users import Users 18 | from lichess_client.endpoints.bots import Bots 19 | from lichess_client.endpoints.boards import Boards 20 | from tests.utils import async_test, get_token_from_config 21 | 22 | 23 | class TestClient(unittest.TestCase): 24 | client: 'APIClient' = None 25 | token = get_token_from_config(section='amasend') 26 | 27 | @async_test 28 | def test__01__initialize__setup_APIClient__all_parameters_set_correctly(self) -> None: 29 | TestClient.client = APIClient(token=self.token, loop=asyncio.get_event_loop()) 30 | 31 | self.assertIsInstance(self.client._client, BaseClient, msg="BaseClient incorrectly set.") 32 | self.assertIsInstance(self.client.account, Account, msg="Account incorrectly set.") 33 | self.assertIsInstance(self.client.broadcast, Broadcast, msg="Broadcast incorrectly set.") 34 | self.assertIsInstance(self.client.challenges, Challenges, msg="Challenges incorrectly set.") 35 | self.assertIsInstance(self.client.chess_bot, ChessBot, msg="ChessBot incorrectly set.") 36 | self.assertIsInstance(self.client.games, Games, msg="Games incorrectly set.") 37 | self.assertIsInstance(self.client.messaging, Messaging, msg="Messaging incorrectly set.") 38 | self.assertIsInstance(self.client.relations, Relations, msg="Relations incorrectly set.") 39 | self.assertIsInstance(self.client.simulations, Simulations, msg="Simulations incorrectly set.") 40 | self.assertIsInstance(self.client.studies, Studies, msg="Studies incorrectly set.") 41 | self.assertIsInstance(self.client.teams, Teams, msg="Teams incorrectly set.") 42 | self.assertIsInstance(self.client.tournaments, Tournaments, msg="Tournaments incorrectly set.") 43 | self.assertIsInstance(self.client.users, Users, msg="Users incorrectly set.") 44 | self.assertIsInstance(self.client.bots, Bots, msg="Bots incorrectly set.") 45 | self.assertIsInstance(self.client.boards, Boards, msg="Boards incorrectly set.") 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/unit/test_games_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes, VariantTypes 6 | from tests.utils import get_token_from_config, async_test 7 | from chess.pgn import Game 8 | 9 | 10 | class TestAccountEndpoint(unittest.TestCase): 11 | client = None 12 | token = get_token_from_config('amasend') 13 | 14 | @classmethod 15 | def setUp(cls) -> None: 16 | cls.client = APIClient(token=cls.token) 17 | 18 | @async_test 19 | async def test_01__export_one_game__fetching_finished_game_details__response_object_returned_with_success(self): 20 | response = await self.client.games.export_one_game(game_id='j5wCnxuX') 21 | print(response) 22 | 23 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 24 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 25 | self.assertIsInstance(response.entity.content, Game, msg="Game was incorrectly loaded.") 26 | 27 | @async_test 28 | async def test_02__export_user_games__fetching_finished_user_games_details__response_object_returned_with_success(self): 29 | response = await self.client.games.export_games_of_a_user(username='amasend') 30 | print(response) 31 | 32 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 33 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 34 | 35 | # TODO: add more tests for export_user_games with different parameters 36 | 37 | @async_test 38 | async def test_03__export_games_by_ids__fetching_list_of_games__response_object_returned_with_success(self): 39 | response = await self.client.games.export_games_by_ids(game_ids=['q7zvsdUF', 'ILwozzRZ', '4OtIh2oh']) 40 | print(response) 41 | 42 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 43 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 44 | 45 | @unittest.expectedFailure 46 | @async_test 47 | async def test_04__stream_current_games__fetching_list_of_games__response_object_returned_with_success(self): 48 | response = await self.client.games.stream_current_games(users=['amasend', 'ProfessorOak15']) 49 | print(response) 50 | 51 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 52 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 53 | 54 | @async_test 55 | async def test_05__get_ongoing_games__fetching_list_of_games__response_object_returned_with_success(self): 56 | response = await self.client.games.get_ongoing_games(limit=2) 57 | print(response) 58 | 59 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 60 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 61 | 62 | @async_test 63 | async def test_06__get_current_tv_games__fetching_list_of_games__response_object_returned_with_success(self): 64 | response = await self.client.games.get_current_tv_games() 65 | print(response) 66 | 67 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 68 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /tests/unit/test_messages_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestMessagesEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @async_test 18 | async def test_01__send__sending_a_message__response_object_returned_with_success(self): 19 | response = await self.client.messaging.send(username="connector_123", text="hi there!") 20 | print(response) 21 | 22 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 23 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /tests/unit/test_relations_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestResponseEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @async_test 18 | async def test_01__get_users_followed_by_a_user__fetching_followed_users__response_object_returned_with_success(self): 19 | response = await self.client.relations.get_users_followed_by_a_user(username='amasend') 20 | print(response) 21 | 22 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 23 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 24 | 25 | @async_test 26 | async def test_02__get_users_who_follow_a_user__fetching_followers__response_object_returned_with_success( 27 | self): 28 | response = await self.client.relations.get_users_who_follow_a_user(username='amasend') 29 | print(response) 30 | 31 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 32 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 33 | -------------------------------------------------------------------------------- /tests/unit/test_response_helpers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client.helpers import ResponseMetadata, ResponseEntity, Response 4 | from lichess_client.utils.enums import RequestMethods, StatusTypes 5 | 6 | 7 | class TestResponseHelpers(unittest.TestCase): 8 | response: 'Response' = None 9 | metadata: 'ResponseMetadata' = None 10 | entity: 'ResponseEntity' = None 11 | 12 | url = 'https://lichees.org/account' 13 | content_type = 'application/json' 14 | timestamp = b'some timestamp' 15 | code = 200 16 | reason = 'SUCCESS' 17 | content = {'key': 'value'} 18 | status = StatusTypes.SUCCESS 19 | 20 | @classmethod 21 | def setUp(cls) -> None: 22 | cls.metadata = ResponseMetadata(method='GET', url=cls.url, 23 | content_type=cls.content_type, timestamp=cls.timestamp) 24 | 25 | cls.entity = ResponseEntity(code=cls.code, reason=cls.reason, status=cls.status, content=cls.content) 26 | 27 | cls.response = Response(metadata=cls.metadata, entity=cls.entity) 28 | 29 | def test_01__metadata__check_all_parameters__all_parameters_are_set_correctly(self): 30 | self.assertEqual(self.metadata.method, RequestMethods.GET, msg='Metadata method was set incorrectly.') 31 | self.assertEqual(self.metadata.url, self.url, msg='Metadata url was set incorrectly.') 32 | self.assertEqual(self.metadata.content_type, self.content_type, 33 | msg='Metadata content type was set incorrectly.') 34 | self.assertEqual(self.metadata.timestamp, self.timestamp, msg='Metadata timestamp was set incorrectly.') 35 | 36 | def test_02__entity__check_all_parameters__all_parameters_are_set_correctly(self): 37 | self.assertEqual(self.entity.status, self.status, msg='Entity status was set incorrectly') 38 | self.assertEqual(self.entity.reason, self.reason, msg='Entity reason was set incorrectly') 39 | self.assertEqual(self.entity.content, self.content, msg='Entity content was set incorrectly') 40 | 41 | def test_03__response__check_all_parameters__all_parameters_are_set_correctly(self): 42 | self.assertIsInstance(self.response.entity, ResponseEntity, 43 | msg='Entity of the response is not of type ResponseEntity') 44 | self.assertIsInstance(self.response.metadata, ResponseMetadata, 45 | msg='Metadata of the response is not of type ResponseMetadata') 46 | 47 | def test_04__to_dict__check_if_all_to_dict_methods_worked_in_chain__all_objects_converted_to_dict(self): 48 | response_dict_representation = self.response.to_dict() 49 | self.assertIsInstance(response_dict_representation, dict, msg="Response.to_dict() not working properly.") 50 | self.assertIsInstance(response_dict_representation['entity'], dict, 51 | msg="ResponseEntity.to_dict() not working properly.") 52 | self.assertIsInstance(response_dict_representation['metadata'], dict, 53 | msg="ResponseMetadata.to_dict() not working properly.") 54 | self.assertIsInstance(self.response, Response, 55 | msg="Response object was converted to dict! Deepcopy not working") 56 | 57 | print(response_dict_representation) 58 | 59 | def test_05__repr__inspect_representation_of_the_response(self): 60 | print(self.response) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/unit/test_simulations_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestSimulationsEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @async_test 18 | async def test_01__get_current__fetching_current_simulations__response_object_returned_with_success(self): 19 | response = await self.client.simulations.get_current() 20 | print(response) 21 | 22 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 23 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /tests/unit/test_studies_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestStudiesEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | study_id = "IeZmXDxM" 13 | chapter_id = "gTHhjLX4" 14 | 15 | @classmethod 16 | def setUp(cls) -> None: 17 | cls.client = APIClient(token=cls.token) 18 | 19 | @async_test 20 | async def test_01__export_chapter__downloading_one_study_chapter__response_object_returned_with_success(self): 21 | response = await self.client.studies.export_chapter(study_id=self.study_id, chapter_id=self.chapter_id) 22 | print(response) 23 | 24 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 25 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 26 | 27 | @async_test 28 | async def test_02__export_all_chapters__downloading_a_study__response_object_returned_with_success(self): 29 | response = await self.client.studies.export_all_chapters(study_id=self.study_id) 30 | print(response) 31 | 32 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 33 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 34 | 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /tests/unit/test_teams_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestTeamsEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @unittest.expectedFailure 18 | @async_test 19 | async def test_01__get_members_of_a_team__fetching_team_members__not_implemented_raises(self): 20 | response = await self.client.teams.get_members_of_a_team() 21 | 22 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 23 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 24 | 25 | @unittest.expectedFailure 26 | @async_test 27 | async def test_02__join_a_team__joining_to_a_team__response_object_returned_with_success(self): 28 | response = await self.client.teams.join_a_team(team_id='some_team') 29 | print(response) 30 | 31 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 32 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 33 | 34 | @unittest.expectedFailure 35 | @async_test 36 | async def test_03__leave_a_team__leaving_a_team__response_object_returned_with_success(self): 37 | response = await self.client.teams.leave_a_team(team_id='some_team') 38 | print(response) 39 | 40 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 41 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 42 | 43 | @unittest.expectedFailure 44 | @async_test 45 | async def test_04__kick_a_user_from_your_team__kicking_user_from_team__response_object_returned_with_success(self): 46 | response = await self.client.teams.kick_a_user_from_your_team(team_id='some_team', user_id='amasend') 47 | print(response) 48 | 49 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 50 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /tests/unit/test_tournaments_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestTournamentsEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @unittest.SkipTest 18 | @async_test 19 | async def test_01__get_current__fetching_current_tournament_info__response_object_returned_with_success(self): 20 | response = await self.client.tournaments.get_current() 21 | print(response) 22 | 23 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 24 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 25 | 26 | @unittest.SkipTest 27 | @async_test 28 | async def test_02__create__posting_a_tournament__response_object_returned_with_success(self): 29 | response = await self.client.tournaments.create(clock_time=1, clock_increment=1, minutes=60) 30 | print(response) 31 | 32 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 33 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 34 | 35 | @async_test 36 | async def test_03__export_games__download_tournament_games_info__response_object_returned_with_success(self): 37 | response = await self.client.tournaments.export_games(tournament_id='QITRjufu') 38 | print(response) 39 | 40 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 41 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 42 | 43 | @async_test 44 | async def test_04__get_results__download_tournament_results__response_object_returned_with_success(self): 45 | response = await self.client.tournaments.get_results(tournament_id='QITRjufu') 46 | print(response) 47 | 48 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 49 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 50 | 51 | @async_test 52 | async def test_05__get_tournaments_created_by_a_user__download_details__response_object_returned_with_success(self): 53 | response = await self.client.tournaments.get_tournaments_created_by_a_user(username='amasend') 54 | print(response) 55 | 56 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 57 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /tests/unit/test_users_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lichess_client import APIClient 4 | from lichess_client.helpers import Response 5 | from lichess_client.utils.enums import StatusTypes, VariantTypes 6 | from tests.utils import get_token_from_config, async_test 7 | 8 | 9 | class TestUsersEndpoint(unittest.TestCase): 10 | client = None 11 | token = get_token_from_config('amasend') 12 | 13 | @classmethod 14 | def setUp(cls) -> None: 15 | cls.client = APIClient(token=cls.token) 16 | 17 | @async_test 18 | async def test_01__get_real_time_users_status__fetching_several_users_status__response_object_returned_with_success(self): 19 | response = await self.client.users.get_real_time_users_status(users_ids=['amasend', 'lovlas']) 20 | print(response) 21 | 22 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 23 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 24 | 25 | @async_test 26 | async def test_02__get_all_top_10__fetching_profile_info__response_object_returned_with_success(self): 27 | response = await self.client.users.get_all_top_10() 28 | print(response) 29 | 30 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 31 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 32 | 33 | @async_test 34 | async def test_03__get_one_leaderboard__fetching_leaderboard__response_object_returned_with_success(self): 35 | response = await self.client.users.get_one_leaderboard(limit=135, variant=VariantTypes.BLITZ) 36 | print(response) 37 | 38 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 39 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 40 | 41 | self.assertEqual(135, len(response.entity.content.get('users', [])), 42 | msg="The number of users fetched does not match the requested one.") 43 | 44 | @async_test 45 | async def test_04__get_user_public_data__fetching_user_data__response_object_returned_with_success(self): 46 | response = await self.client.users.get_user_public_data(username='amasend') 47 | print(response) 48 | 49 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 50 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 51 | 52 | @async_test 53 | async def test_05__get_rating_history_of_a_user__fetching_user_rating_histpry__response_object_returned_with_success(self): 54 | response = await self.client.users.get_rating_history_of_a_user(username='amasend') 55 | print(response) 56 | 57 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 58 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 59 | 60 | @async_test 61 | async def test_06__get_user_activity__fetching_user_activity__response_object_returned_with_success(self): 62 | response = await self.client.users.get_user_activity(username='amasend') 63 | print(response) 64 | 65 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 66 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 67 | 68 | @async_test 69 | async def test_07__get_your_puzzle_activity__fetching_user_puzzles_activity__response_object_returned_with_success(self): 70 | response = await self.client.users.get_your_puzzle_activity() 71 | print(response) 72 | 73 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 74 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 75 | 76 | response_2 = await self.client.users.get_your_puzzle_activity(limit=2) 77 | self.assertEqual(2, len(response_2.entity.content), msg="The number of entries is incorrect.") 78 | 79 | @async_test 80 | async def test_08__get_users_by_id__fetching_users__response_object_returned_with_success(self): 81 | response = await self.client.users.get_users_by_id(users_ids=['amasend', 'lovlas']) 82 | print(response) 83 | 84 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 85 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 86 | 87 | @async_test 88 | async def test_09__get_members_of_a_team__fetching_team_members__response_object_returned_with_success(self): 89 | response = await self.client.users.get_members_of_a_team(team_id='team') 90 | print(response) 91 | 92 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 93 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 94 | 95 | @async_test 96 | async def test_10__get_live_streamers__fetching_live_streamers__response_object_returned_with_success(self): 97 | response = await self.client.users.get_live_streamers() 98 | print(response) 99 | 100 | self.assertIsInstance(response, Response, msg="Response in not of type \"Response\"") 101 | self.assertEqual(response.entity.status, StatusTypes.SUCCESS, msg="Request was unsuccessful.") 102 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.utils.utils import async_test, get_token_from_config 2 | -------------------------------------------------------------------------------- /tests/utils/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import configparser 3 | import os 4 | from asyncio import coroutine, get_event_loop 5 | 6 | 7 | def async_test(f): 8 | """Async wrapper for unit tests. Wraps tests functions to be handled by asyncio event loop""" 9 | def wrapper(*args, **kwargs): 10 | if inspect.iscoroutinefunction(f): 11 | future = f(*args, **kwargs) 12 | else: 13 | coroutine_ = coroutine(f) 14 | future = coroutine_(*args, **kwargs) 15 | get_event_loop().run_until_complete(future) 16 | return wrapper 17 | 18 | 19 | def get_token_from_config(section: str) -> str: 20 | """Read token from configuration file or environment.""" 21 | try: 22 | config = configparser.ConfigParser() 23 | config.read('credentials.ini') 24 | return config.get(section, 'token') 25 | 26 | except: 27 | return os.environ[section] 28 | --------------------------------------------------------------------------------